Ventotto
Dependency Injection and Dependency Injection
The Play Framework implements the Guice Dependency Injection framework. Getting started with it has been a daunting process, one littered with errors. However, after a 3 weeks of using it now, I am starting to develop an understanding for its features.
One of the interesting takeaways was the intricacies surrounding the Guice
Framework, which initially were not obvious to me. I think my initial
confusion was around the @Inject
tag. The first instance of needing to use
the tag was confusingly during the writing of a test. The confusion in this
case came because I still needed to pass in the item I wanted to inject.
@Inject
public CSVRepository(Environment env, String apprenticeCSV, String restaurantCSV, String scheduleCSV, String employeesCSV, String guestsCSV) {
this.apprenticeCSV = env.getFile(apprenticeCSV).getAbsolutePath();
this.scheduleCSV = env.getFile(scheduleCSV).getAbsolutePath();
this.restaurantCSV = env.getFile(restaurantCSV).getAbsolutePath();
this.employeesCSV = env.getFile(employeesCSV).getAbsolutePath();
this.guestsCSV = env.getFile(guestsCSV).getAbsolutePath();
}
The constructor for our CSVRepository needed the Environment to be passed in, so that we could access the files used for storage. Without the inject tag in place, the tests would blow up. Despite an environment being passed in to the constructor.
Storage storage = new CSVRepository(Environment.simple(), "test/resources/apprentices.csv", "test/resources/restaurants.csv", "test/resources/schedule.csv", "test/resources/employees.csv", "test/resources/guests.csv");
What is not clear from the guice error message, is that the failure is not
really linked to the test, but to running the application in production. It is
essentially a compilation error that is raised. So, the @Inject
annotation
really says is “find the Environment.class
and inject an instance of it
here”. The Guice/Play Framework pre-defines how some of the key classes should
be instantiated for injection, they are annotated as such within the the
framework source code.
Given that understand, it then become a question of how when injecting things
into our controller, we should inform the framework what to use. It turns out
there are a number of ways to do it. The key concept is that of a binding, and
also providers. The Play Framework has a special configuration location within
module files that extend the AbstractModule
.
public class Module extends AbstractModule{
@Override
protected void configure() {
bind(Storage.class).to(PostgresRepository.class);
}
@Provides
CSVRepository provideCSVRepository() {
CSVRepository csv = new CSVRepository(Environment.simple(), "conf/resources/apprentices.csv", "conf/resources/restaurants.csv", "conf/resources/schedule.csv", "conf/resources/employees.csv", "conf/resources/guests.csv");
return csv;
}
}
In the case shown above the @Provides
signifies that this method should be
implemented should a CSVRepository
ever need to be injected. It knows this
via the return type of the method.
The binding above that says that wherever Storage.class
is listed to be
injected, then a PostgresRepository
should be injected in it’s place.
With those concepts in place, that covers the basic features of the Guice Injection framework. What these features lead to is what guice calls a dependency injection graph. So as one main item is injected, all the sub-classes and their sub-classes are individually instantiated and injected.
The benefits of using a framework like this are quite clean, and concise code. The alternatives to these injections are lots of factory methods which explicitly instantiate classes. Whilst the factory pattern is commonly used and understood, it is also another source of bugs and requires maintenance.