The Dependency Injection Meta-Container
Posted by indroneel on April 29, 2007
Of late, the dependency injection pattern has gained prominence as a common feature provided by many object-oriented frameworks. Included in this list are new entrants like JBoss Seam and Google Guice as alternatives to the more popular Spring framework. Existing platform, such as Struts and EJB, are also making a move towards this paradigm.
The prevalence of dependency injection has resulted in certain interesting possibilities that are mostly to do with interoperability, transparency and integration across frameworks and containers. In this article we shall take a closer look at these possibilities and propose solutions for the same.
Pushing the Bounds
Dependency injection is a bounded phenomenon. Components that participate in dependency injection form a closed group. Within the group, the features of auto-wiring (and at times, declarative-wiring1) make the necessary components implicitly available to each other.
To access these components from outside the group, one must fall back to the service locator pattern. A good example is that of service beans deployed in a Spring container (the closed group) and invoked from within Struts actions2 (contextual lookup and access).
We can say that the bounds of dependency injection are always defined by the presence of service locator (anti) pattern. From an application developer’s perspective, the impact of the latter is diminished and localized to integration points between frameworks, but not completely removed.
Given the benefits, it is only natural to envisage a common dependency injection scope for components across multiple application tiers in a seamless fashion. Service lookup operations, if any, are encapsulated as part of the framework’s static structure. Application developers are exempted from writing the glue-code between frameworks in different tiers.
The Single Container Approach
In this approach, a single dependency injection container is used to manage components across multiple application tiers. A good example would be the Spring framework that provide a common environment for elements in the presentation and the business layers. Others like Apache Tapestry makes use of HiveMind as the dependency injection container for both view and domain entities.
Different tiers in a well-designed application usually have distinctly different sets of requirements with some overlap. Under this approach, the dependency injection container used must cater to the requirements for each and every tier. This results in a non-trivial container implementation (such as Spring).
Unfortunately for simpler applications, a heavy-weight framework/platform usually represent extra baggage. A good example would be a JSP/servlet-based application, deployed on top of a full-fledged J2EE application server, that makes little use of the enterprise features provided (e.g. EJB, messaging and load-balancing) other than the servlet container.
The Multi-Container Approach
In this approach, frameworks at each application tier make use of their own dependency injection container. For each such framework, integration with other containers are provided in the form of supporting modules, either as part of the standard distribution or from third-party sources. Note that these integration pieces usually encapsulate the service locator pattern and are reusable across multiple application scenarios.
Systems that make use of multiple containers include Tapestry with HiveMind (for view entities) integrated with Spring bean container (for domain entities) or a Struts 2.03 based presentation layer integrated with Spring at the business layer.
The advantages of a multi-container approach are as follows:
- Better separation of concerns between different layers within an application without losing out on an overall dependency injection scope.
- Inclusion of frameworks that are specialized for the requirements of specific application layers.
- The option of having lightweight frameworks in application scenarios that warrant the same.
Even with these attractive features, there are still some limitations with a multi-container approach that deserve consideration:
- Most frameworks make use of a third-party container to incorporate the features of dependency injection. The effectiveness of the framework under different scenarios has a dependency on the features provided by the corresponding container.
- The ability to mix-and-match solutions to define an enterprise technology stack is constrained by the availability and effectiveness of integration pieces between different dependency injection containers.
- Lightweight containers tend to become heavy over a period of time. This may become an issue with frameworks and platforms, that make use of these containers, in catering to simpler application scenarios.
The concept of a meta-container derives from the advantages and limitations of a multi-container approach. Simply stated, the meta-container is an object creation factory that also populates the object-graph paths for each instance it creates. In populating this graph and from an application perspective, the meta-container makes exclusive use of the dependency injection pattern.
The meta-container is a transient entity that is created and destroyed for each transaction/operation performed and request handled. A singleton factory is used to create these container instances. Only a basic set of objects are created by the meta-container, the rest are retrieved from ancillary dependency injection containers registered with the meta-container factory.
Usage and Scope
Frameworks that wish to incorporate the features of dependency injection may consider embedding a meta-container as part of their static structure. Application-specific objects, that are deployed on top of the framework, can be instantiated using this meta-container.
It is assumed that these application-specific objects are lightweight in nature with little overheads during their creation and finalization. Features that require advanced capabilities (such as transaction management and AOP) can be implemented as components deployed on external frameworks that provide support for those capabilities. These frameworks are plugged into the meta-container factory via. the external object provider interface. The deployed components are injected, as needed, into lightweight objects created by the meta-container.
From the discussions so far, it follows that there are alternate ways of visualizing the concept of a meta-container. These include,
- semi-finished container: absence of features to do with life-cycle maintenance of the objects created.
- gateway/facade for multiple dependency injection containers.
- glue-code implementation between frameworks and multiple dependency injection containers.
Common infrastructure: Because of its extreme simplicity, the same meta-container implementation can be used to provide the features of dependency injection in multiple frameworks and platforms.
Interoperability: One of the USP of a meta-container is out-of-the-box support for multiple external containers at different application tiers. Framework developers are assured of a high degree of integration with the most popular platforms available.
Separation of concerns: The meta-container presents a common interface to multiple dependency injection containers. Integration with external platforms being taken care of, framework developers can focus on the core capabilities offered by their respective solutions.
Cross-container injection: Instances created by the meta-container can be injected with objects from multiple external containers. This also means that the same framework instance (encapsulating a meta-container) can be integrated with multiple external containers simultaneously.
Transience: Instances created by a meta-container have an extremely short life-span. Even the meta-container is created on a per request/transaction/operation basis. In addition, features like life-cycle management and aspects are not directly available to meta-container generated instances.
Bidirectional injection: Wiring between instances created by the meta-container and external objects is unidirectional. External objects cannot have references to instances created by the meta-container. Also note that the meta-container does not provide support to directly wire components that are deployed on different external containers.
Support for existing frameworks: It is not possible to provide a pluggable integration to replace existing dependency injection engines in frameworks (such as Tapestry and Struts). Any such integration will require a significant change in the corresponding framework implementation.