Dependency Injection
Content
Forewords:
One goal of foundationj is to facilitate the development of a modular application which supports collaborative development. We expect such an application to be composed of multiple modules contributed by developers who know little about each other.
This implies that the following requirements need to be incorporated into the architecture of the application:
- The system must be easy to understand : Only public abstract layers should be visible and specific implementations should be well encapsulated into modular components.
- Contributed code must be reusable : The dependencies required to add a functionality to the program must be minimised so that the writen code remains easily adaptable to other contexts (Contributors may wish to adapt their code to multiple frameworks).
- Configuration should be both simple and flexible: Contributed classes should be externally configurable. However, configuration should be as simple as possible. Addition of new functionalities or implementation changes should not lead to complex tweaks of some configuration files. Also, configuration issues should be detectable early in the development process, if possible at compile time.
- The maintenance of the whole system must be facilitated : A modular application design should restrict the propagation of changes to their module unit as much as possible and should ease the understanding of how the system works. However, since modules are expected to be contributed by independent teams or individuals, existing code must anticipate the addition of new components and new components must also provide a level of self-description (This idea will be clarified in the paragraphs below).
In order to help meeting the above requirements, foundationj provides a component-based architectural design paradigm as well as runtime support to help articulate individual components together.
The paragraphs below describe important design structures that foundationj provides support for.
Constructor injection
Constructor injection (a form of dependency injection) is a design pattern which is used to remove dependencies from a class to specific implementations. As you will see, this has a lot of advantages.
To illustrate this we will use a simple example:
In this example we are asked to provide a Shop implementation using the Shop Api in blue. The shop interface looks like this:
public interface Shop{
// Returns the types of goods sold by this Shop
public String goods();
// Sell something to the customer
public void sell(Customer customer);
}
Our shop is selling bikes so we also depend on the BikeFactory API. Our sell method can be implemented like so:
// class fields
private BikeFactory bikeFactory; // Builds the bikes
private ShopManager manager; // Takes care of the customers, handles treasury, etc...
// Type of goods
public String goods(){
return "BIKES";
};
// sell method
public void sell(Customer customer){
List<BikeModel> availableModels = bikeFactory.getAvailableModels();
BikeModel chosenModel = customer.chooseModel(availableModels);
if(chosenModel!=null){ // Customer might not like our models...
Bike bike = bikeFactory.build(chosenModel);
manager.makeTransaction(customer, bike);
}
}
Now the important question is how do we obtain a reference to our BikeFactory and ShopManager? Without dependency injection, we need to create these instances ourselves. In this example we decided to implement the ShopManager interface ourselves (BikeShopManager) so we can create a new BikeShopManager in our BikeShop constructor. After digging into the distinct components of the application, we found a Bike library which provides an implemention for the BikeFactory. The Bike Factory of this library does not produce all bike components itself and instead use subcontractors to obtain these components. The Bike Library thus depends on another library which supplies the bike components.
Given these details, the constructor of our BikeShop looks like this:
public class BikeShop implements Shop{
private final BikeFactory bikeFactory;
private final ShopManager manager;
public BikeShop(){
bikeFactory = new BikeFactoryImpl(new BikeComponentSupplierImpl()); // From libraries
manager = new BikeShopManager(); // From our own implementation
}
}
And our dependency graph now looks like this:
The necessity for us to create an instance of the BikeFactory has unfortunately several disadvantages:
- This ties our BikeShop to a specific library: In order to change the BikeFactory used by our BikeShop, we need to modify the code, there is no way of configuring our class externally.
- It also increases the risk of having a class which is unstable. In this particular case we created 2 dependencies: to the bike library and to the BikeComponent library. Changes in any of these libraries may require to update the code of our BikeShop class. Having more dependencies means higher code maintenance requirements.Increasing dependencies also increase complexity and obscurs the comprehension of how changes in the code propagate to other parts of the application.
- Finally, when we designed our BikeShop class, we needed to be aware of the libraries that were available in the full application. In a complex application where several teams develop their own plugins, this becomes difficult and time consuming to keep track of all possibilities.
Constructor injection can solve these issues altogether: Instead of creating the BikeFactory instance ourselves, we ‘declare’ the BikeFactory as an argument to our constructor. The task of finding a proper BikeFactory instance becomes the responsibility of the framework:
public class BikeShop{
private final BikeFactory bikeFactory;
private final ShopManager manager;
// Constructor 1
public BikeShop(BikeFactory injectedBikeFactory){
manager = new BikeShopManager(); // our own implementation, although CI could be used for this as well
this.bikeFactory = injectedBikeFactory; // just reference the provided factory
}
// Constructor 2
public BikeShop(List<BikeFactory> injectedbikeFactories){
manager = new BikeShopManager(); // our own implementation
this.bikeFactory = manager.chooseFactory(injectedBikeFactories); // let our manager choose among available factories
}
}
Notice that with this example we also illustrate that it is possible to define several constructors. Here we added a constructor accepting a List of BikeFactories (constructor 2). We may anticipate that several implementations will be available in the application assembly. Since we decided that our shop should only rely on one unique factory for bike supplies, we made our ShopManager choose which factory is the most appropriate to rely upon.
This simple change has several consequences:
- We removed the dependencies to the specific implementation of BikeFactory. Changes in these libraries will not affect our class anymore. Our class is more stable.
- When designing the class we no longer need to worry about finding libraries providing implementations for the BikeFactory we need. Class design is facilitated.
- Finally, the BikeFactory used by our class at runtime can easily be changed. Control of which BikeFactory will be used is moved outside of the class. Note that in this particular example, we made our ShopManager choose one of the factories so there is still a level of control in our implementation but the point is that the provided BikeFactories can be changed entirely without affecting our code. Our implementation becomes configurable and more reusable.
Since the definition of the BikeFactory implementations to be provided to the BikeShop instance is made outside the class, the next chapter explains where and how this configuration can be controlled.
Containers and context
When a foundationj application is launched, the boot process creates a container. Given a collection of classes, called components, a container is capable of resolving dependencies in order to instantiate the components when they are needed (As we saw above, dependencies are defined as arguments in constructors). For example, if we create a container and register the following classes: BikeShop, BikeFactoryImpl, BikeComponentSupplierImpl, the container will resolve dependencies and create the BikeShop instance using a BikeFactoryImpl instance, itself provided with an instance of BikeComponentSupplierImpl. Other examples can be found here.
NB : Complex dependency graphs can be resolved by a container, however, all dependencies must be satisfied and there should be no cyclic dependency.
Although the flexibility of this solution is obvious, one might think that since components have to be registered individually in code, this solution leads to a master class hard-coding the registration (and which ends up depending on everything..).
Of course, this does not have to be the case. In foundationj, we use annotations to perform the configuration:
In fact, annotations fulfill three main purposes in foundationj:
- Discovery: They define which classes are component ‘entry points’ and they enable the discovery of these classes during the assembly scanning process at boot time.
- Scope: They specify a hierarchy of containers in order to facilitate the ‘life cycle’ management of components at runtime (see the next chapter).
- Verifications: Finally, they can be used by the application designer to enforce a number of conventions to be respected by plugin contributors. Notably, annotations activate code checks at compile time in order to catch configuration issues early in the development process.
In our BikeShop example, the API designer has annotated the Shop interface with @Modular (TODO link to javadoc entry). This annotation specifies that the annotated interface should be implemented by a component ‘entry class’ and that implementing classes should be themselves annotated with @Module:
//------------------------//
// API //
//------------------------//
@Modular
public class Shop{
[...]
}
//------------------------//
// Impl //
//------------------------//
@Module
public class BikeShop implements Shop{
[...]
}
With these annotations in place, compilation checks are performed by an annotation processor (TODO link to Module processor) which also generates entries in the META-INF directory of the compiled jar files. These entries are then used at boot time when scanning for the Modular interfaces and all their implementations.
For example when compiling our code, the generated ShopImpl.jar will contain a folder ‘foundationj’ with 1 text file named ‘Shop’ (the name of the modular interface that we implement) and with one line: ‘our.package.name.BikeShop.class’. At boot time, if the ShopImpl.jar is located on the classpath, the boot process will ‘know’ that the BikeShop.class is an available implementation for the Shop interface should any other module depend on Shop.
With this system, The assembly dictates runtime composition:
Given the following structure where all components are compiled within their own jar:
The boot process will create a container configured like so:
and BikeShop constructor 1 will be used at runtime:
// Constructor 1 used at runtime
public BikeShop(BikeFactory injectedBikeFactory){ // <= BikeFactoryImpl instance provided at runtime
manager = new BikeShopManager();
this.bikeFactory = injectedBikeFactory;
}
If we add another jar in the classpath with an alternative implementation:
The boot process will create a container configured with both implementations:
and BikeShop constructor 2 will be used at runtime:
// Constructor 2 used at runtime
public BikeShop(List<BikeFactory> injectedbikeFactories){ // <= List with BikeFactoryImpl and AltBikeFactory provided
manager = new BikeShopManager(); // our own implementation
this.bikeFactory = manager.chooseFactory(injectedBikeFactories);
}
This system has 3 advantages:
- The expected components entries are clearly identifiable as they can be found by looking for annotated interfaces in the API, so this should make life easier for developers.
- When a class is annotated with @Module, compilation fails if the class implements 0 or more than one @Modular interface. This allows the API designer to provide guidance on the use of the API.
- The configuration is self-descriptive and simply assembly dependent.
Scopes and Application Control
The application assembly and discovery process is not the only mean to control how containers are configured.
As shown in the previous section, we can use foundationj annotations to define the components of an application and to wire them up together. Now, we can also group these components into multiple cohesive units (containers) that we can start (instantiate) and stop (garbage collect) so that the lifetime of components from the same unit are managed altogether. We can also create a hierarchy for these containers with the following relationships rules:
- Children containers can obtain instances from the parent but not the opposite.
- When a parent is stopped all its children are stopped beforehand.
With these rules in place, the management of the various phases of the application can be greatly simplified while remaining flexible.
To illustrate this, we will describe a fictional application and will move down its container hierarchy to introduce some important properties.
Imagine an application which can simulate a village, with a street map, multiple building types, factories, inhabitants etc… This application would give the possibility to a user to explore this village by navigating a map and to participate in its economy by buying items from shops.
A possible container hierarchy for this application could be the following one:
In this diagram, each container is represented by a rounded rectangle with a dashed contour. The list of classes shown in the top right corners corresponds to the components entry class that the container manages. The interfaces that these classes implement are denoted with an icon. For example, in the VISITOR container, the shop interface is illustrated with the yellow cart icon showing that there are 2 shop implementations BikeShop and ToolShop. The roles of each container is also shown in the bottom section of each box.
The other elements of this diagram will be discussed in the following dedicated sections:
- The @Scope annotation: Discusses how the hierarchy is defined
- A hierarchy for components life expectancy: Explains how the container hierarchy simplifies the management of components life cycles.
- The hierarchical singleton pattern: Explains how dependency resolution works in the hierarchy and how the scope policy affects this behaviour
- Starting containers and listening for events: Gives examples to learn how to start containers and listen for life cycle events
- @ScopeOption: Explains how to provide a stateful component instance to a given container.
- @Service: Describes annotations providing additional communication links between containers.
(I recommend right-clicking on the diagram and selecting ‘Open link in a new tab’, this way you can go back and forth between the diagram and the ext in this tab) .
The @Scope annotation
In code, the hierarchy is specified using the @Scope annotation. @Scope possesses 3 elements:
- name : This is simply the name of the container (or Scope)
- parent : This should resolve to the name of the parent container. parent and name are sufficient to define the structure of the tree. For example, the VISITOR container defines “APPLICATION” as parent. APPLICATION defines an empty string since this container is the root of the hierarchy.
- policy: This specifies whether several containers with the same scope may coexist: For example, the APPLICATION scope’s policy is SINGLETON meaning that only one APPLICATION container may exist at any given time. In contrast, the scope’s policy of VILLAGE is SIBLING. This means that several VILLAGEs may be instantiated and ran in parallel by the application.
The target of the @Scope annotation are interfaces annotated with @Modular (or @Core, see the javadoc).
@Modular
@Scope(name="VILLAGE", parent="APPLICATION", policy=ScopePolicy.SIBLING)
public interface Shop{ ... }
and the implementing class(es) inherit the scope properties of the interface
@Module // Inherits the Scope!
public class BikeShop{ ... }
It is the API designer’s responsibility to define the container hierarchy of the application. It is possible that future versions of foundationj will allow implementations to override the Scope of the implemented interface, but this still needs to be given some thoughts.
A Hierarchy for components life expectancy
As mentioned earlier, the advantage of creating a hierarchy is to simplify the management of the distinct phases of the application (and its intelligibility). The container tree is organised in such a way that long-lived containers are placed at the top of the hierarchy and shorter lived containers further down the hierarchy.
In our example, the root is APPLICATION. This is the root for 2 reasons: 1) because it is the application entry point - it provides a front page to allow users to choose a village simulation to explore - and 2) because it manages the DataStore which will need to be available for the entire life span of the application. The VILLAGE container comes next because the state of a simulated village must be maintained at least for as long as visitors are present in the village. So visitors have a shorter life span than the village, and the views (map or shop views) which are specific to each visitor have an even shorter life span.
By grouping components into these units, the management of the distinct phases of the application remains clear. Only a few nodes are sufficient to control the application. If a VISITOR decides to exit the VILLAGE, all the views it had previously created will be shut down first before the VISITOR container turns off. Similarly, if a VILLAGE simulation stops, all the VISITOR will exit the VILLAGE first. This hierarchy system makes it as easy as turning off a switch in an electrical circuit.
The Hierarchical Singleton pattern
As we said earlier, children containers can obtain instances from parents. The DataStore instance created by the APPLICATION container will therefore be available to its children containers. It is this property which can be exploited to create a singleton pattern:
Lets look at the VILLAGE container to illustrate this. The simulation is managed by the VillageModelImpl instance. Its constructor looks like this:
// VillageModel Constructor
public VillageModelImpl(List<Shop> shops, DataStore dataStore){ // injected dependencies
... // initialise the model
}
The first argument is a list of Shop instances so the model can delegate internal shop management to injected implementations. In this case the list will contain a new BikeShop instance and a new ToolShop instance.
The second argument is the DataStore so that the model can obtain some persisted data like the location of the shops, number of inhabitants etc… The important thing is that the injected DataStore instance will be obtained from the parent container.
This is important because, with this design, if we decide to start another VILLAGE container, a new VillageModelImpl would be created using new instances of BikeShop and ToolShop. However, the DataStore instance would be the same as the one provided to the first model since it is obtained from the parent container. In other words, the DataStore instance is a Singleton instance at the level of the application. If instead we wanted to run different models on distinct DataStore instances, the DataStore interface should be scoped inside VILLAGE.
- NB1 : There is no limit in the level / depth at which component instances can be obtained from parents. The DataStore can be obtained by children, grand children etc of the APPLICATION container.
- NB2 : When a container is instantiated, only one instance of each component is created. For simplicity in our example, the simulated village will have one shop per type of goods sold (So in this case 1 BikeShop and 1 ToolShop). If multiple shops of the same type were required, the design would have to be changed to replace Shops by ShopFactories instead. The principle would remain the same only with one additional layer of abstraction.
The container hierarchy allows for more flexibility than the classic singleton pattern. For example, lets say that we start 2 village simulations, and that in each village several distinct visitors are spending time exploring the villages. A village can accept several visitors but a specific visitor cannot explore 2 villages at the same time, each visitor should be given the possibility to see one unique village only. In other words, we need ‘scoped singleton instances’ of VILLAGE. Each VILLAGE instance should be shared by downstream containers but not by sibling containers. This is exactly what the tree hierarchy enables:
Village)) v2[visitor 2] -- 'Can See' --> mnt((Mountain
Village)) v3[visitor 3] -- 'Can See' --> mnt((Mountain
Village)) sea --> app{Application} mnt --> app
Visitor 1 can ‘see’ the seashore village but not the mountain village. Similarly, visitor 2 and visitor 3 can ‘see’ the mountain village but not the seashore village. This structure also simplifies instantiation and garbage collection: if the seashore village turns off, visitor 1 will turn off as well, but these events will not affect the mountain village nor the visitors 2 and 3.
- NB1: Scope design really depends on the purpose of the application, if interactions between visitors was the primary focus of the application a distinct hierarchy would be more appropriate. The good thing is that changing the scope hierarchy in foundationj does only require to change the values of the @Scope annotation in the interfaces, implementations remain unchanged.
- NB2: It is up to the application architect to decide on the granularity of the containers. possibilities are quite broad.
Starting containers and listening for events
We are now going to describe how containers are started and how we can listen for container events.
In the large container tree figure above, the 3 top most containers include a class highlighted in blue. AppliControl in APPLICATION, VillageControl in VILLAGE and VisitorControl in VISITOR. These classes extend the abstract class AppController in foundationj-wiring
An AppController has 2 important properties:
- At runtime, it is injected with the ScopeManager instance which exposes methods to list active containers and to start or stop containers using their name and runtime identifiers.
- It implements ScopeEventListener and is automatically registered to listen for all scopes events. The extending class can thus intercept scopes when they are about to start/stop or when they have started/stopped.
Here is an example, imagine the following sequence of events in our application:
long activities... VILLAGE -->>+ VISITOR_2 : STARTS Note right of VILLAGE: 2 Visitors active... Note right of VISITOR_2: Does some
brief activities... VILLAGE -->> VISITOR_2 : INTERCEPT STOP deactivate VISITOR_2 Note right of VILLAGE: 1 Visitor left... VILLAGE -->> VISITOR_1 : INTERCEPT STOP deactivate VISITOR_1 Note right of VILLAGE: No Visitor left... APPLICATION -->> VILLAGE : STOPS deactivate VILLAGE Note right of APPLICATION: Stopping Application deactivate APPLICATION
To make this work, one AppController class is required per scope. We will give the code for the VILLAGE AppController: VillageControl.class
Lets say that once a user has chosen a village to visit, a new page appears where he/she can enter her name and start the visit. This page would be provided by a VillageUI in the VILLAGE scope (not shown in the tree figure):
@Modular
@Scope(name="VISITOR", parent="APPLICATION", policy=ScopePolicy.SIBLING)
public interface VillageUI{
// Marker interface for the UI
}
When the user has entered her/his name it fires an event to registered listeners. Here is the listener interface:
@Modular
@SameScopeAs(VillageUI.class) // As the name of this annotation suggests... :)
public interface VillageUIListener{
public void visitSessionRequested(String name);
}
One of these listeners is our VillageControl:
@Module
public class VillageControl extends AppController implements VillageUIListener{ // Obtains the Scope from the interface
// Just keeps track of active visitors in the village
private final List<String> activeVisitors = new ArrayList<>();
// Constructor
public VillageControl(ScopeManager mgr, SomeUI ui){
super(mgr) // Registers this instance as a ScopeEventListener
ui.registerListener(this); // Registers this instance as a VillageUIListener
}
...
We can implement listener method so that we start a new VISITOR instance when we receive the name of the visitor
//------------------------------------------//
// VillageUIListener Method //
//------------------------------------------//
@Override
public void visitRequested(String name){
final RuntimeScope villageScope = scopeMgr.getScope(this); // Obtain a ref to our own container
// First we create a new container instance
// We need to provide the Scope name, an id (VISITOR has a SIBLING policy so multiple instances can coexist)
// and the last argument is the parent scope
final RuntimeScope visitorScope = scopeMgr.newScopeInstance("VISITOR", name, villageScope);
// Then we can start the container
scopeMgr.startScope(visitorScope);
};
As you can see, starting a container only requires knowledge about the name of the scope we would like to start.
Now we can have a look at the methods from the ScopeEventListener interface:
//------------------------------------------//
// ScopeEventListener Method //
//------------------------------------------//
@Override
public void beforeStop(RuntimeScope willStop){
if(willStop.parent().equals(ourScope)){
// do something ...
}
};
@Override
public void afterStop(RuntimeScope hasStopped){
if(hasStopped.parent().equals(ourScope)){
// remove the scope id from the list of activeVisitors
activeVisitors.remove(hasStopped.id());
}
};
@Override
public void beforeStart(RuntimeScope willStart){
if(willStart.parent().equals(ourScope)){
// do something
}
};
@Override
public void afterStart(RuntimeScope hasStarted){
if(hasStarted.parent().equals(ourScope)){
// Add the scope id to the list of activeVisitors
activeVisitors.add(hasStopped.id());
}
};
}
These methods will be called for any of the RuntimeScope (container) present in the application. So if necessary, it is possible to intercept events for containers that have not been started by our class.
NB : The AppControllers are the only classes that need to obtain a reference to the container since they control the containers that need to be started or stopped. All other components should obtain their dependencies via construction injection as we explained in the first section of this page
@ScopeOption
Sometimes it may be useful to provide a stateful object to the container we create. In our example, when a visitor enters a shop, the VisitorControl would start a new SHOP_VIEW instance. This view however would need to determine which Shop in particular it is supposed to provide a view for.
One way would be for the DetailedShopUI class to depend on both the visitor’s Locator and the village’s VillageModel, then figure out which Shop resides at the current location of the visitor. This sounds a bit convoluted. An alternative is to use @ScopeOption to specify that a specific instance needs to be provided upon instantiation:
private final Shop shop;
public DetailedShopUI(@ScopeOption Shop){
this.shop = shop;
}
Then when the VisitorControl start a new SHOP_VIEW container, it needs to provide a specific Shop instance:
// trigger method
public void visitorHasEnteredShop(Shop shop){
[...]
scopeMgr.startScope(shopViewScope, shop); // we add the instance when we start the scope
}
@Service
The last annotation that we are going to mention in this page is the @Service annotation.
It may occur that a component is required in multiple containers but that it should not be a parent singleton as it would maintain some state specific to the container where it is used. For example, the MAP_VIEW and SHOP_VIEW containers may depend on some sort of rendering engine to display their content (not shown in the tree figure).
The declaration of the service would look something like this:
@Service // no need for @Scope of course since it is not bound to any particular Scope.
public interface Scene3D {
[...]
}
And the consumer code:
@Module
public class DetailedShopUI{
// Constructor declares the dependency as usual
public DetailedShopUI(Scene3D scene){
[...]
}
}
The alternative to the service described here would be to use a factory pattern: A Scene3DFactory could be added to the parent container and children would use the factory to create a new 3DScene on demand. Two advantage of the @Service system however are 1) that it allows to place the component in the container where it is actually used and 2) that a service can be obtained by containers which are located in disconnected trees. In fact PickCells uses this pattern.
Last Note
foundationj specifies other annotations which have not been listed here, please have a look at the foundationj-wiring repository, the Readme file contains a summary table of available annotations in foundationj and their purpose.