When to use Dependency Injection
- Injecting Configuration Data
- Injecting the Same Dependency into Multiple Components
- Injecting Different Implementations of the Same Dependency
- Injecting Same Implementation in Different Configurations
- Using Container Services
- When Not to use Dependency Injection
- Obtaining Local Variable Instances from the Container
Jakob Jenkov |
Dependency injection is a powerful technique that can be applied in many situations across all layers of an application. But this does not mean that dependency injection should be used every time a class depends on another class.
In short dependency injection is very effective at assembling loosely coupled components, and at configuring these components. Especially if the association between the components lasts throughout the lifetime of the components.
More specifically, dependency injection is effective in these situations:
- You need to inject configuration data into one or more components.
- You need to inject the same dependency into multiple components.
- You need to inject different implementations of the same dependency.
- You need to inject the same implementation in different configurations.
- You need some of the services provided by the container.
These situations have one thing in common. They often signal that the components wired together represent different or independent concepts or responsibilities, or belong to different abstraction layers in the system. For instance, database configuration (driver, url, user, password) and a DataSource implementation are different concepts. Similarly a DataSource and a DAO class represent different concepts and belong to different abstraction layers. Each of these situations are described in more detail below.
Injecting Configuration Data
If a component needs external configuration data, dependency injection is an effective way to supply that data to the component. For instance, a DataSource implementation usually needs at least four parameters: A driver class name, a database url, a user name, and a password. Here is an example that uses Butterfly Container's configuration script:
dbDriver = "org.h2.Driver"; dbUrl = "jdbc:h2:tcp://localhost/~/test"; dbUser = "sa"; dbPassword = ""; dataSource = 1 com.jenkov.mrpersister.jdbc.SimpleDataSource( dbDriver, dbUrl, dbUser, dbPassword);
Injecting the Same Dependency into Multiple Components
If you need to inject the same dependency into multiple components, it is often easier to have a container do it, than doing it by hand. For instance, the same DataSource implementation is often needed by several DAO classes. You will often even inject the same instance of the DataSource implementation into all DAO classes. Here is an example that uses Butterfly Container's configuration script:
userDao = 1 com.myapp.dao.UserDao(dataSource); productDao = 1 com.myapp.dao.ProductDao(dataSource);
The dataSource factory definition is reused from the configuration injection example. The dataSource factory was defined as a singleton, so the same instance of SimpleDataSource is injected into both userDao and productDao.
Injecting Different Implementations of the Same Dependency
Sometimes you need to inject different implementations of some dependency into one or more components. For instance, if you have a button component in a desktop GUI app, you may want to inject different implementations of mouse listeners. In SWT you have a standard SelectionListener interface that each button in your application depends on. When a Button is clicked, its SelectionListener's will be notified. You will most likely need a different implementation of the SelectionListener interface for each button. Here is an example that uses Butterfly Container's configuration script:
button = * org.eclipse.swt.widgets.Button($0, $1) .addSelectionListener($2); addButton = 1 button(parentComposite, mode, com.myapp.gui.AddListener()); validateButton = 1 button(otherComposite, mode, com.myapp.gui.ValidateListener());
First a reusable button factory is defined. This is done to avoid having to write "org.eclipse.swt.widgets.Button" and "addSelectionListener" for every button in the script. This factory will inject parameter 0 and 1 into the Button constructor, and parameter 2 into the addSelectionListener() method.
Second two different button factories, addButton and validateButton, are defined. These factories call the button factory with the parameters parentComposite, mode and a SelectionListener implementation.
Don't worry about the parentComposite, otherComposite and mode parameters. They are SWT specific details. It is not necessary to understand what they are, to understand this example. What is important in this example is that two different implementations of the SelectionListener interface are injected into two instances of the same component, the SWT Button class.
Injecting Same Implementation in Different Configurations
Dependency injection is also an effective way to supply different instances of the same component, but in different configurations, to other components. It may sound a bit abstract, so here is a simple example using Butterfly Container's configuration script:
url = * java.net.URL($0); server1 = * url("http://server1.mydomain.com"); server2 = * url("http://server2.mydomain.com"); client = com.jenkov.ServiceClient($0); client1 = client(server1); client2 = client(server2); client3 = client(url("http://server3.mydomain.com"));
This example defines three client factories. Each client depends on a URL instance. Two URL instances, server1 and server2, are defined earlier and injected into client1 and client2. The last client factory, client3, also has a URL instance injected into it, but the URL instance is defined locally, as a call to the url factory. There is no significant difference between the injections into client1, client2 and client3. It was merely to show different container configuration options.
The important thing to notice in the above example is that all three clients depend on the same component, the java.net.URL class, but each client have a differently configurated instance injected.
Using Container Services
Sometimes you may not need too much dependency injection, but rather some of the services offered by a dependency injection container. An often used service is life cycle management. Using a dependency injection container in these situations is perfectly valid. It unifies instance life cycle managment and frees you to focus on other challenges.
Here is a small example of a singleton that has its life cycle phases managed by Butterfly Container:
dataSource = 1 com.thirdparty.PoolingDataSource(); dispose{ $dataSource.close(); }
A singleton, dataSource, is defined as an instance of PoolingDataSource(). Then the "dispose" life cycle phase is defined inside the dispose{...} clause. The dispose phase is defined to call the close() method on the singleton. The dispose phase is executed when the Butterfly Container is shut down. This way the dataSource singleton is closed properly, and releases all pooled database connections it holds, if the container is shut down.
Notice that the dataSource instance doesn't have any dependencies injected into it. Still it may make sense to let a container manage it. The example is of course a bit artificial. Most components or services will need some kind of dependencies or configuration to be able to do anything meaningful. But, these configurations might be supplied to the service in a way different from dependency injection. For instance, the PoolingDataSource class might be a third party component, and the developers might have chosen to let it read its configuration from the system properties, using System.getProperty(..) calls. As you know, system properties are supplied to Java applications on the command line.
When Not to use Dependency Injection
Dependency injection is not effective if:
- You will never need a different implementation.
- You will never need a different configuration.
If you know you will never change the implementation or configuration of some dependency, there is no benefit in using dependency injection. This is true for both member variables and local variables. For instance, if you are building a string using a StringBuilder, you will most likely never change to a different implementation of StringBuilder, or an instance with a different configuration. There is no reason to inject the StringBuffer instance then. Just instantiate it directly inside the component. Here is an example:
public class MyComponent { public String toString(){ StringBuilder builder = new StringBuilder(); builder.append("..."); return builder.toString(); } }
In this example there is absolutely no reason to make the StringBuilder used in the toString() method an instance member and inject it. You will never need a different implementation, nor a differently configured StringBuilder.
You should keep in mind that even if you application may never need a different implementation of some component, it can still be useful to be able to inject different mock implementations during unit testing.
Obtaining Local Variable Instances from the Container
The two conditions in the beginning of the previous section are true for many local variables, but it would wrong to say that is is true for all local variables. If a method needs a local instance of a component that is complex to assemble, you can still benefit from letting the container assemble it.
Here is a simple example of a class that needs a local component instance for every time its service() method is called:
public interface Factory { public MyLocalComponent instance(Object ... parameters); }
public class MySmartComponent { protected Factory factory = null; public MySmartComponent(Factory factory){ this.factory = factory; } public void service(){ MyLocalComponent component = this.factory.instance(); //... do something with local component; } }
The MySmartComponent has a Factory member. The Factory interface is defined by the same developer who defined the MySmartComponent. It is not a Butterfly Component interface. However, Butterfly Container can inject a container factory into MySmartComponent, and at the same time adapt it to the custom Factory interface. Here is how it looks in Butterfly Container's configuration script:
myLocalComponent = * com.myapp.MyLocalComponent(); mySmartComponent = * com.myapp.MySmartComponent(#myLocalComponent);
Two factories are defined, myLocalComponent and mySmartComponent. The mySmartComponent factory is defined to have the myLocalComponent factory injected into its constructor. The # character signals that it is the factory itself that should be injected, and not the factory product. Now the MySmartComponent class can obtain component instances from the myLocalFactory factory at will.
Tweet | |
Jakob Jenkov |