Why dependency container?
Fano Framework is designed to be extensible. It uses class composition heavily. Instead of classes with deep inheritance, many complex classes in this framework depend on one or more classes that do simple thing.
Because of this, creating instance of a class may require us to create other classes instance that it requires. Some classes may share same dependency to same class, some classes may not. Some classes need same instance of other class instance, while some other may requires new instance.
This task grows in complexity when scope of problem grows. So we need to provide a way to manage dependencies between software components in application.
Container, factory and its service
Dependency container, or simply container, is software component that manages
any software components (a.k.a, service). In Fano Framework, dependency container
implementation must implements
The services are registered into dependency container during service registration which later can be queried to retrieve service instance.
During service registration, a service name is associated with a factory class
that must implements
IDependencyFactory interface that will be responsible to create the required service.
For a service to be able to work with
IDependencyContainer implementation, it must implements
IDependency does not declare any methods. So following declaration is suffice.
TMyClass = class(TInterfacedObject, IDependency);
Fano Framework comes with base class
TInjectableObject that implements
To register a service, dependency container provides two method
factory(). Both methods expects two parameters, shortstring value of service name and instance of
IDependencyFactory responsible for creating service. You can use any value for service identifier string. If same identifier is already registered, it will overwrite previous
See Single vs multiple instance section for explanation of both methods.
For example, following code registers service factory
TSimpleRouterFactory with name
router. This factory class will create
var container : IDependencyContainer; ... container.add('router', TSimpleRouterFactory.create());
You can also use alternative way
Retrieve service instance from dependency container
Later, to get instance of
router from container,
var inst : IDependency; ... inst := container.get('router');
In Fano Framework,
get() method of
IDependencyContainer always returns
IDependency interface instance. So you need to convert it to its correct type
before you can use it.
var router : IRouteMatcher; ... router := inst as IRouteMatcher;
var router : IRouteMatcher; ... router := container.get('router') as IRouteMatcher;
or if use GUID string as service name
var router : IRouteMatcher; ... router := container.get(GUIDToString(IRouteMatcher)) as IRouteMatcher;
get() can not find service, it raises
If service is registered but with nil factory class,
EInvalidFactory exception is raised.
You can also use simplified array-like syntax, for example
router := container['router'] as IRouteMatcher;
router := container.services['router'] as IRouteMatcher;
Test if service is registered
Dependency container provides
has() method which return boolean value that can be used to check if particular service is registered or not.
if (container.has('router')) then begin //router is registered, do something end;
When a service is registered using
add() method, it is registered as single instance service. So every time a service is queried, container returns
In following example
router2 will point to same instance.
container.add('router', TSimpleRouteCollectionFactory.create()); ... var router1, router2 : IRouteMatcher; ... router1 := container.get('router') as IRouteMatcher; router2 := container.get('router') as IRouteMatcher;
When a service is registered using
factory() method, it is registered as multiple instance services. So every time a service is queried, container returns
different instance. In code below,
router2 will point to different instance.
container.factory('router', TSimpleRouteCollectionFactory.create()); ... var router1, router2 : IRouteMatcher; ... router1 := container.get('router') as IRouteMatcher; router2 := container.get('router') as IRouteMatcher;
Adding service alias
To register new name for an existing service, dependency container provides method
This method expects two parameters. First parameter is new name for existing service, and second parameter identifies existing service to be aliased.
For example, if you have service registered as follows,
You can create alias for above service as follows,
Then you can retrieve instance using
var router : IRouter; routeMatcher : IRouteMatcher; ... router := container.get(GUIDToString(IRouter)) as IRouter; routeMatcher := container.get(GUIDToString(IRouteMatcher)) as IRouteMatcher;
routeMatcher will point to same class instance.
However, if you have service registered as multipe instances,
routeMatcher will point to different instance.
Built-in dependency container
Fano Framework comes with built-in dependency container,
TDependencyContainer, this is basic dependency container which store service registration in hash list.
Of course, you are free to implements your own dependency container, as long as it implements
Circular dependency issue
Circular dependency issue arises when a service depends on itself, directly or indirectly. For example, A needs B, B needs C, C needs A.
This causes unterminated recursion. Fano Framework will raise
ECircularDependency exception to prevent such condition.
Following example shows circular dependency condition, which will trigger
function THomeCtrlFactory.build(const cntr : IDependencyContainer) : IDependency; begin result := cntr['homeCtrl']; end; ... container.add('homeCtrl', THomeCtrlFactory.create()); ... router.get('/', container['homeCtrl'] as IRequestHandler);
If you are in circular dependency situation, you need to rethink about your application architecture.
Auto-wire dependency is not yet implemented.