I’m going to write a quick walthrough post on what is DI, what problems it is trying to solve, and how to manage it concisely in your project.
What does DI mean
In a previous blog post I discussed how I was moving the database backend from a H2 instance to a fully fledged Pgsql with connection pooling. However I was going to keep the H2 conf for testing, so I end up with a static Factory class that can provide the desired DataSource implementation: Factory::getPostgresDataSource
and Factory::getH2DataSource
.
This DataSource impl is used in the app code. Let’s assume for a moment that my app runs with a normal p.s.v. main()
funcion in the App
class. That is, we have something like this:
public class App {
public static void main(String... args) {
DataSource ds = Factory.getH2DataSource();
// comment previous and uncomment next to switch to Postgres
//DataSource ds = Factory.getPostgresDataSource();
...etc...
}
}
In this case switching from a DataSource implementation to the other would not be such a pain after all.
However, things get complicated when it is not the frontend App
class that uses the DataSource object, but some internal class. For example, a common pattern in enterprise Java is to have a Repository class that abstracts data manipulation for the frontend App
class — there are several reasons why this makes sense, like for example the mantainability according to SOLID design.
So let’s assume instead that App
has a reference to Repository
and that it is Repository
that needs a DataSource
instance. Something like:
public class App {
public static void main(String... args) {
Repository repo = new Repository();
...etc...
}
}
public class Repository {
private DataSource ds;
public Repository() {
ds = Factory.getH2DataSource();
...constructor...
}
}
What happened now, is that the DataSource
is not manageable anymore from the frontend. Granted, if we want to change the DataSource
impl to be the Factory::getPostgresDataSource
instead of the original, we can go to the Repository
class and tweak the ds = …
line. The problem is that if there are more other "middlemen" in the app structure other than Repository
, that need to use a DataSource
instance, things will go awry pretty soon: if by mistake we are using, say, DataSourceImpl1
in one part of the code and DataSourceImpl2
in another part of the code, what is going to happen?
So the only solution is to centralize somewhere the "configuration" of what DataSource
implementation we want to use, say, in in the App
class itself like we were doing originally, and to inject the chosen DataSource
implementation from there into Repository
. In other words: we have to change the Repository
code so that the DataSource
dependency is injected in the constructor by the caller, instead of being created over there:
public class App {
public static void main(String... args) {
DataSource ds = Factory.getH2DataSource();
Repository repo = new Repository(ds);
...etc...
}
}
public class Repository {
private DataSource ds;
public Repository(DataSource ds) {
this.ds = ds;
.. constructor...
}
}
This is basically what Dependency Injection is all about: to make code more manageable, we centralize all chosen configuration of interfaces and inject it in cascade through the object constructors.
DI and Unit Testing
When working in structured manner (the aforementioned SOLID method), DI is very useful for unit testing. We can test separately pieces of code and mock the needed resources. In my case, I want that Repository
uses the Postgres d.s. in the real world scenario, but the H2 d.s. in unittests. Unit testing then is relatively easy:
public class RepositoryTest {
private DataSource ds;
private Repository repo;
@Before
public void prepare() {
ds = Factory.getH2DataSource();
}
@Test
public void test1() {
repo = new Repository(ds);
...test content...
}
...other tests...
}
Of course it could be the case to totally create a database mock (e.g. using Mockito) instead of relying on H2 for testing. It does not matter though, because in the @Before
function we would just write ds = mock(DataSource.class)
instead of taking an implementation from Factory
, but the injection of ds
into Repository
would still be done in the same way (the tests would not change… nice!).
Google Guice
Dependency Injection as a concept is not difficult to grasp. However, complex projects need to inject multitudes of configurations in multiple different places, and so DI easily becomes a burden.
Google Guice to the rescue! Guice is a DI framework oriented to simplicity and in-code configuration through the power of decorators. First, we need to add the dependency to the project pom.xml
:
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.1.0</version>
</dependency>
Secondly, with Guice the DI configuration must be done within a class extending the abstract class AbstractModule
provided by the framework itself. Let’s call this class AppConfig
:
public class AppConfig extends AbstractModule {
@Override
protected void configure() {
bind(DataSource.class).toInstance(Factory.getH2DataSource());
}
}
The bind(this).to(that)
DSL is very readable and intuitive: the this
must be an interface, the that
must be any implementation of that interface, that’s it!
More precisely, you have to master few different variants: if you get the implementation from a factory (like in our example), you have to use the .toInstance()
DSL, but if you get the implementation from a written class, the correct syntax is .to()
.
There is a little other step to do in the actual App
class code, and is to retrieve the configured implementation in AppConfig
within the App
class itself, using the Guice::createInjector
function:
public class App {
public static void main(String... args) {
DataSource ds = Guice.createInjector(new AppConfig()).getInstance(DataSource.class);
Repository repo = new Repository(ds);
...etc...
}
}
So the net number of lines has slightly increased, but the DI configuration is now isolated in its own class. If we want to switch from H2 to Postgres, we just go to AppConfig
and modify the line in configure()
as bind(DataSource.class).toInstance(Factory.getPostgresDataSource());
without even touching the Application itself or its dependencies.
Needless to say, the very same approach can be done in unit testing: we can create an independent TestConfig
class extending AbstractModule
, and in the RepositoryTest
discussed in the last section we would simply change the line in the @Before
function like this:
@Before
public void prepare() {
ds = Guice.createInjector(new TestConfig()).getInstance(DataSource.class);
}
and the tests would still work as they did previously, no line of code should be touched other than this.
Foreword
You can see that DI is a very important piece of the puzzle in writing enterprise-quality code. In must be said though that it is a pattern coming from how Java (and C#) are structured and have traditionally been programmed — especially considering the evolution of those languagues and what became a pattern (DI) and what became an anti-pattern (not using DI when having multiple implementations of interfaces).
Other languages don’t have interfaces and in those kind of languages DI management is simply impossible (as also mocking is impossible). Some of these, like Python, have evolved in time and even if they don’t provide interfaces, they provide Abstract Base Classes (ABCs) as injecting point, so DI and mocking are possible.
Hope you liked my tutorial!