Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Do It Yourself Dependency Injection

4.52/5 (17 votes)
17 Apr 2017CPOL6 min read 18.9K  
Dependency injection frameworks are bad practice. So how can we still use dependency injection? Just do it yourself!
If you are a coder who instinctively senses that something is wrong with DI frameworks this article is for you. I will present to you my personal recipe to implement all practical DI aspects. This article considers a few typical classes and interfaces, and two types of lifecyles: Singleton and Scoped. The recipe I show here uses the natural capabilities of the Java language.

Introduction

For some time now IOC and dependency injection are singled out as the most important coding design pattern to follow. You will encounter questions about it in every code interview. Developers are listing DI frameworks they've used on their resumes. Recruiters will ask you whether you have experience with particular DI frameworks. The majority opinion is that in order to follow this very important coding architectural principal you need to use some sort of framework. It's such a shame that the majority got it all wrong. The simple commonly misunderstood fact is that implementing DI and using a DI framework is not the same thing.

Its quite difficult to promote this point of view. I find that I constantly have to explain myself over and over again whatever project I'm working on. Experienced engineers keep looking at me like I'm crazy. You'd think I just told them they live in the matrix. But I'm not the only one, there are others. A small yet insistent group who refuse to adopt the trend.

If you are a coder who instinctively senses that something is wrong with DI frameworks this article is for you. I will present to you my personal recipe to implement all practical DI aspects, at least those that I have encountered and consider necessary. Fair warning, after reading this you may receive a slight shock. The solution is so simple you will not be able to stop wondering how is it that all the hype surrounding DI frameworks came to be.

Background

The following short list of points pretty much summarizes the reasons you would have for not using a DI framework:

  • Dependency injection is used to encourage loose coupling of code. Ironically, using a DI framework will couple your entire application to the framework.
  • Using a DI framework heavily can cause performance problems not easily resolvable. Unexpected relationships between object can cause entire object chains to be created unnecessarily.
  • Using a DI framework does not guarantee the correctness of "wiring" your objects. The only way to check the "wiring" is correct is at run time which is a waste of precious development time.
  • Some DI framework use annotations which introduces behavior into interfaces and classes where it should not be, violating some OOP principles and best practices.
  • Some DI frameworks promote other anti patterns like setter injection.
  • Implementing DI manually uses natural type safety at compile time.
  • Manual DI is more performant then using a framework.
  • With Manual DI you have complete control of your application, object creation and lifetime management, debuggable and testable with standard development tools.

For further reading you may find some of this online articles useful:

The code

Lets list the important features provided by DI frameworks we need to implement:

  1. Map interfaces to their appropriate concrete class implementations.
  2. Create instances while providing them with all necessary dependencies.
  3. Control object lifecycle. Bind object to an application scope, or make it a singleton.
  4. Provide a placeholder for proper cleanup and disposal of resources.

Consider the following typical classes and interfaces:

Java
public interface DB extends AutoCloseable {	
	List<Record> fetchRecords(String someParameter);		
}
Java
public class DBImpl implements DB {
	
	private final String connectionString;
	public DBImpl(String connectionString) {
		this.connectionString = connectionString;		
	}

	@Override
	public List<Record> fetchRecords(String someParameter) {
		// fetch records from database, transform to POJOSs and return results
		return new ArrayList<>();
	}
	@Override
	public void close() throws Exception {
		// typically close the db connection here		
	}

}
Java
public interface ReportService {
	
	void generateReport(String filename,String otherParameter);

}
Java
public class ReportServiceImpl implements ReportService {
	
	private final DB db;
	public ReportServiceImpl(DB db) {
		this.db=db;		
	}

	@Override
	public void generateReport(String filename, String otherParameter) {
		List<Record> records=db.fetchRecords(otherParameter);
		// create the report from the records		
	}

}

Our first attempt at a manual DI implementation is:

Java
public interface DIYDI1 {
	
	DB getDB();	
	ReportService getReportService();

}
Java
public class DIYDI1Impl implements DIYDI1 {
	
	private static DIYDI1 instance=new DIYDI1Impl();
	public static DIYDI1 getInstance() {
		return instance;
	}

	@Override
	public DB getDB() {		
		return new DBImpl("db connection string");
	}

	@Override
	public ReportService getReportService() {	
		return new ReportServiceImpl(getDB());
	}
				
}

To get an instance of ReportService we can simply use:

Java
DIYDI1Impl.getInstance().getReportService();

This clearly satisfies requirements 1 and 2. The solution however is very impractical because each time an object is needed it will be recreated from scratch including all its dependencies. If for example we had another service that uses the database it would be impossible to share a database connection between the services. If the connection is expensive to create there can be severe performance penalties.

To overcome this limitation we need to introduce some sort of object lifecycle management. We will consider two types of lifecycles we want to support:

  • Singleton
  • Scoped

Singleton is a familiar lifecycle. Typically they are initialized when the application starts. Sometimes lazy initialization is used the first time the singleton is accessed.

A scoped lifecycle is a bit trickier. It is simply a block of code executed between the start and end of some application event. Some scope examples are: handling an http request, a Quartz job or reading a message off a JMS queue. We need to be able to bind the lifetime of an object to the scope such that it will be created one time at the most during the scope.

This is typical for database connections which are relatively expensive to create. Reusing the same connection several times throughout the scope improves performance significantly. It is also common for connections to be closed at the end of the scope. Our solution needs to support this. It should track all objects created throughout the scope and cleanup or close resources at the end of the scope.

Another interesting scope property is that each scope usually involves only one thread.  In our implementation we will create a new DIYDI object at the start of each scope.  It should be accessed only by the scope thread and therefore does not need to be thread safe.

Let's add some more service examples to our code:

Java
public interface InMemoryDataService {	
	
	Record getData(String recordID);
	
}
Java
public class InMemoryDataServiceImpl implements InMemoryDataService  {
	
	private final ConcurrentMap <String, Record> _cachedRecords;
	public InMemoryDataServiceImpl(DB db) {
		_cachedRecords=new ConcurrentHashMap<>();
		//this is where we would  access the database and fill our map with cached records
		//small note about this inMemory data service. Only expose Record Object to client
		// code if they are immutable		
	}

	@Override
	public Record getData(String recordID) {		
		return _cachedRecords.get(recordID);
	}

}
Java
public interface AlertService {
	
	void sendAlert(String customerID,String recordID);

}
Java
public class AlertServiceImpl implements AlertService {
	
	private final DB db;
	private final InMemoryDataService inMemoryDataService;
	public AlertServiceImpl(DB db,InMemoryDataService inMemoryDataService) {
		this.db=db;
		this.inMemoryDataService=inMemoryDataService;		
	}

	@Override
	public void sendAlert(String customerID, String recordID) {
		Record record=inMemoryDataService.getData(recordID);
		Customer customer=getCustomer(customerID,db);
		sendAlert(customer, record);	
		
	}



	private Customer getCustomer(String customerID, DB db2) {
		//use the database to retrieve customer data
		return null;
	}
	
	private void sendAlert(Customer customer, Record record) {
		//now that we have actual customer data and record data we can send it to the 
		//customer				
	}

}

Also let's add another layer of abstraction to our database access method by declaring a configuration interface to hold our connection string. Our database access class can then depend on configuration to obtain the connection string:

Java
public interface Configuration {
	String getConnectionString();
}
Java
public class ConfigurationImpl implements Configuration {
	
	public ConfigurationImpl(Object externalArgs) {
		//read the connection string from a file or command line		
	}
	
	private String connectionString;

	@Override
	public String getConnectionString() {	
		return connectionString;
	}
	

}
Java
public class DBImpl implements DB {

	public DBImpl(Configuration configuration) {
		this(configuration.getConnectionString());
	}

}

Finally our revised DIYDI interface and implementation which includes object lifecycle management:

Java
public interface DIYDI2 extends AutoCloseable {
	
	void initSingletons(Object externalArgs);
	Configuration getConfiguration();
	DB getDB();	
	ReportService getReportService();
	InMemoryDataService getInMemoryDataService();
	AlertService getAlertService();

}
Java
public class DIYDI2Impl implements DIYDI2 {
	
	private static Configuration configuration;
	private static  InMemoryDataService inMemoryDataService;
	private DB db;
	private ReportService reportSerivce;
	private AlertService alertService;

	@Override
	public void initSingletons(Object externalArgs) {
		configuration=new ConfigurationImpl(externalArgs);
		inMemoryDataService=new InMemoryDataServiceImpl(getDB());		
	}
	
	@Override
	public Configuration getConfiguration() {
		return configuration;
	}
	@Override
	public InMemoryDataService getInMemoryDataService() {
		return inMemoryDataService;
	}

	@Override
	public DB getDB() {
		if (db==null)
			db=new DBImpl(getConfiguration());
		return db;
	}

	@Override
	public ReportService getReportService() {
		if (reportSerivce==null)
			reportSerivce=new ReportServiceImpl(getDB());
		return reportSerivce;
	}


	@Override
	public AlertService getAlertService() {
		if (alertService==null)
			alertService=new AlertServiceImpl(getDB(), getInMemoryDataService());
		return alertService;
	}

	@Override
	public void close() {
		if (db!=null)
			try {
				db.close();
			} catch (Exception e) {		
				e.printStackTrace();
			}
		
	}

}

We can see that singleton services are backed by a static field. They will be initialized via the initSingletons method during application startup. Scoped services are backed by member fields, which ensure that the service will only be created once during the scope. They will be created only if they are used during the scope. The DIYDI object also implements AutoCloseable. The close method will be called at the end of scope which will close the database connection if it was opened and potentially free up other scoped resources.

This is an example of how the class may be used within a single scope:

Java
try (DIYDI2 diydi2=new DIYDI2Impl()) {
	ReportService reportService=diydi2.getReportService();
	reportService.generateReport("SomeFile.txt", "paramValue");
	AlertService alertService=diydi2.getAlertService();
	alertService.sendAlert("YourName", "1234");						
}

During this scope the ReportService and AlertService will share a database connection. Typically you will not use the DIYDI object like this. The details depend on the natural scope and API's exposed by your application.  It is common to have a base class which can be instantiated by your applications natural scope factory. It will handle creating and closing the DIYDI object. Then you can subclass it or compose it with other executor like classes to control the flow of the application.

Conclusion

Hopefully by now you can appreciate how easy it is to implement dependency injection by yourself. In software development an important guiding principle is "Keeping It Simple". The recipe I have shown here uses only the natural  capabilities of the Java language. This is much simpler then introducing a framework for creating all objects. It also eliminates possible complications that can happen when we don't always fully understand the DI framework we are using, which are usually discovered much later in the development lifecycle. But most importantly it brings back control over the application back to where it belongs: Developers.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)