piątek, 21 marca 2014

Scoping Hierarchical Dependency Injection

Dependency injection is now widely known and used patten for structuring the application (and it is superior to NOT using it and new-ing objects at any time). It makes the application easily testable and just as importantly makes reasoning about it simpler (every dependency used by a component is clearly defined).

Lets take a look at relatively simple DI case in a desktop application. We have only constructor injection available and two possible creation modes for a class: singleton and new-instance. The former makes the DI container manage a single instance of the class for all objects requesting it. The latter leads to creation of a new instance for each request.

This is not a very useful tool. We can create the object graph only at a single point in time - by getting the root of the dependency hierarchy at application initialization moment (with no cheating - direcly using the container - afterwards). We need some approach to getting objects from the container dynamically.

Now factories come handy fulfilling this requirement. By declaring a factory dependency, we state that at some point in time current class will create a new object (as factories make no sense for singleton-scoped objects), and with it - a subsystem of its dependencies. This is an elegant way to requesting instances dynamically from the container without using it directly in the application code (which would make the class doing it harder to test and reason with). In the case of Java's Guice, the idea of a factory is called 'provider' and is described here https://code.google.com/p/google-guice/wiki/InjectingProviders . In .NET, Ninject framework has an extension with excellent description of the concept: https://github.com/ninject/ninject.extensions.factory/wiki [Ninject Factory wiki]

Getting the object from a factory will under the hood use the DI container to create a new object and configure its dependency tree (by injecting singletons, new object instances or other factories).

[As Google Guice points out, a provider might not always create a new object. It can be made to return some object scoped wider than new instance and narrower than singleton, that exists in global space (so it may exist at most one such object) at a time it is requested, for example bound to current web session. However, I think it might be less readable than letting the current object constructor-inject the session-scoped service. In later point in this discussion, we will talk about multiple scopes co-existing simultaneously]

Using factories is a great boost in freedom and possibilities in designing the application. Now, some services might be created on demand and they can in turn use some dependencies from the container. But factories defined this way may only use some global singleton objects or create new objects in a new-instance mode. We may still improve on this idea.

Imagine the case where some objects should have the same lifespan (or scope) as others. They in turn could create other objects dynamically, with separate scopes. Such created scope would be in a parent-child relationship with the scope of the object that created it. What is even more interesting, the new-instance and singleton scopes I described earlier have now a different meaning. The new-instance corresponds to creating a child scope. And that newly created object is now a singleton from a point of view of all objects created along with it (they are singletons to each other) and from a point of view of all objects in all child scopes that might be subsequently created.

Multiple child scopes in an application

This approach of hierarchical dependency injection is useful for stateful applications, where entire subtrees consisting of components and services might be created on demand, living and storing state for a limited time. They can be easily discarded by services in parent scopes when no longer needed.
The practical implementation of this idea is possible by using child injectors (called child kernels in Ninject). Each child injector can access objects defined for itself and parent injector. That allows the application to create separate scopes for object sub-trees, even multiple copies of object groups based on the same classes.

 The whole idea has some similiarities to actor model, where an actor can be created to do specific task and be stopped afterwards by its parent. Every service higher in hierarchy then seems to be stable and eternal for that actor/component. The hierarchical dependency injection might, after some improvements, make application easier to comprehend and make the binding declarations less flat and more intuitive.

Brak komentarzy:

Prześlij komentarz