Part 1 - A Design Pattern
This is part of a multiple-part tutorial. This first part will explain the concept of Dependency Injection, a design pattern used for achieving Inversion of Control by writing loosely coupled code.
You can check out the code for this tutorial part in Github.
What is Inversion of Control
Let's visit the wiki definition, "Inversion of Control inverts the flow of control as compared to traditional control flow".
A traditional control flow is: 🅰️ ➡️ 🅱️ ➡️ 🅾️
🅰️ is the customer invoking 🅱️, the provider that uses 🅾️, the service for performing some kind of action.
- 🅱️ controls 🅾️
- 🅱️ depends on 🅾️
- 🅱️ and 🅾️ are tightly coupled
Inverting the flow of control means decoupleing 🅱️ from 🅾️ by giving the control of it to 🅰️. Changing the flow to: 🅰️ (🅾️) ➡️ 🅱️ ➡️ ❌
Now, 🅰️ is the customer, describing 🅾️ the service and invoking 🅱️ the provider for performing some action with the given service.
- 🅱️ doesn't control 🅾️
- 🅱️ doesn't depend on 🅾️
- 🅱️ and 🅾️ are loosely coupled
This is achieved using Dependency Injection.
What is Dependency Injection
Let's visit the wiki definition, "Dependency Injection is a technique in which an object receives other objects that it depends on."
So, Dependency Injection is a Design Pattern. Sounds less scary. Let's code.
Mail Collector App
Let's build an app pulling emails from both Gmail and Microsoft.
Contracts
An Enum called MailSource
for categorizing the email source:
public enum MailSource {
GMAIL,
MICROSOFT;
}
An abstract class Mail
for contracting mail objects.
public abstract class Mail {
public abstract String from();
public abstract String subject();
public abstract MailSource source();
@Override
public String toString() {
return String.format("Got mail by %s, from %s, with the subject %s", source(), from(), subject());
}
}
An interface for contracting services responsible for pulling Mail from suppliers, the MailService
.
public interface MailService {
List<Mail> getMail();
}
And last, an interface for contracting an engine responsible for collecting Mail from multiple services, the MailEngine
.
public interface MailEngine {
List<Mail> getAllMail();
}
Implementations
The concrete Mail
implementations were designed with a builder pattern for convenience and immutability.
The Gmail Mail
implementation, GmailImpl
:
public final class GmailImpl extends Mail {
private final String setFrom;
private final String setSubject;
private GmailImpl(final String from, final String subject) {
setFrom = from;
setSubject = subject;
}
@Override
public String from() {
return setFrom;
}
@Override
public String subject() {
return setSubject;
}
@Override
public MailSource source() {
return MailSource.GMAIL;
}
public static GmailImpl.Builder builder() {
return new GmailImpl.Builder();
}
public static final class Builder {
private String prepFrom;
private String prepSubject;
public Builder from(final String setFrom) {
prepFrom = setFrom;
return this;
}
public Builder subject(final String setSubject) {
prepSubject = setSubject;
return this;
}
public GmailImpl build() {
requireNonNull(emptyToNull(prepFrom), "from cannot be empty or null");
requireNonNull(emptyToNull(prepSubject), "subject cannot be empty or null");
return new GmailImpl(prepFrom, prepSubject);
}
}
}
The Micsorosft Mail
implementation, MicrosoftImpl
:
public final class MicrosoftImpl extends Mail {
private final String setFrom;
private final String setSubject;
private MicrosoftImpl(final String from, final String subject) {
setFrom = from;
setSubject = subject;
}
@Override
public String from() {
return setFrom;
}
@Override
public String subject() {
return setSubject;
}
@Override
public MailSource source() {
return MailSource.MICROSOFT;
}
public static MicrosoftImpl.Builder builder() {
return new MicrosoftImpl.Builder();
}
public static final class Builder {
private String prepFrom;
private String prepSubject;
public Builder from(final String setFrom) {
prepFrom = setFrom;
return this;
}
public Builder subject(final String setSubject) {
prepSubject = setSubject;
return this;
}
public MicrosoftImpl build() {
requireNonNull(emptyToNull(prepFrom), "from cannot be empty or null");
requireNonNull(emptyToNull(prepSubject), "subject cannot be empty or null");
return new MicrosoftImpl(prepFrom, prepSubject);
}
}
}
Mail Services
The Gmail MailService
implementation:
public final class GmailService implements MailService {
@Override
public List<Mail> getMail() {
//This is where the actual Gmail API access goes.
//We'll fake a couple of emails instead.
var firstFakeMail =
GmailImpl.builder()
.from("a.cool.friend@gmail.com")
.subject("wanna get together and write some code?")
.build();
var secondFakeMail =
GmailImpl.builder()
.from("an.annoying.salesman@some.company.com")
.subject("wanna buy some stuff?")
.build();
return List.of(firstFakeMail, secondFakeMail);
}
}
The Microsoft MailService
implementation:
public final class MicrosoftService implements MailService {
@Override
public List<Mail> getMail() {
//This is where the actual Microsoft API access goes.
//We'll fake a couple of emails instead.
var firstFakeMail =
MicrosoftImpl.builder()
.from("my.boss@work.info")
.subject("stop writing tutorials and get back to work!")
.build();
var secondFakeMail =
MicrosoftImpl.builder()
.from("next.door.neighbor@kibutz.org")
.subject("do you have philips screwdriver?")
.build();
return List.of(firstFakeMail, secondFakeMail);
}
}
Mail Engine
First, let's build the concrete MailEngine
in a tightly coupled manner:
public final class TightMailEngine implements MailEngine {
private final MailService gmailService;
private final MailService microsoftService;
public TightMailEngine() {
gmailService = new GmailService();
microsoftService = new MicrosoftService();
}
@Override
public List<Mail> getAllMail() {
return concat(
gmailService.getMail().stream(),
microsoftService.getMail().stream())
.collect(toList());
}
}
Notice TightMailEngine
tightly depends on both GmailService
and MicrosoftService
. We can fix two things in this code with Dependency Injection.
Testing this code is hard, invoking the getAllMail method will invoke the real Gmail and Microsoft's services.
Adding another service will require modifications to
TightMailEngine
.
We can address testing by rewriting the engine loosely coupled as our first step into the Dependency Injection design pattern.
public final class LooseMailEngine implements MailEngine {
private final MailService gmailService;
private final MailService microsoftService;
public LooseMailEngine(final MailService setGmailService, final MailService setMicrosoftService) {
gmailService = setGmailService;
microsoftService = setMicrosoftService;
}
@Override
public List<Mail> getAllMail() {
return concat(
gmailService.getMail().stream(),
microsoftService.getMail().stream())
.collect(toList());
}
}
The LooseMailEngine
doesn't know, nor does it depend on, either GmailService
or MicrosoftService
. Instead, it expects them to be injected as constructor arguments. This is Inversion of control. The engine no longer has control of GmailService
or MicrosoftService
.
From a testing perspective, we can now easily inject mocks for both GmailService
and MicrosoftService
and prevent invocation of the real services.
Now, that we have leveraged dependency injection, we rewrite LooseMailEngine
again and make it more robust:
public final class RobustMailEngine implements MailEngine {
private final Set<MailService> mailServices;
public RobustMailEngine(final Set<MailService> setMailSerices) {
mailServices = setMailSerices;
}
@Override
public List<Mail> getAllMail() {
return mailServices.stream().map(MailService::getMail).flatMap(List::stream).collect(toList());
}
}
Now, whoever invokes our engine has complete control of the service dependencies.
😎
The Main App
This is the app itself, the MailCollectorApp
:
public final class MailCollectorApp {
private MailEngine engine;
public MailCollectorApp(final MailEngine setEngine) {
engine = setEngine;
}
public String getMail() {
var ret = "No mail found.";
if (!engine.getAllMail().isEmpty()) {
ret = Joiner.on(System.lineSeparator()).join(engine.getAllMail());
}
return ret;
}
public static void main(final String... args) {
var engine = new RobustMailEngine(List.of(new GmailService(), new MicrosoftService()));
var app = new MailCollectorApp(engine);
System.out.println(app.getMail());
}
}
Executing the main method will print:
Got mail by GMAIL, from a.cool.friend@gmail.com, with the subject wanna get together and write some code?
Got mail by GMAIL, from an.annoying.salesman@some.company.com, with the subject wanna buy some stuff?
Got mail by MICROSOFT, from my.boss@work.info, with the subject stop writing tutorials and get back to work!
Got mail by MICROSOFT, from next.door.neighbor@kibutz.org, with the subject do you have a star screwdriver?
Now, Let's test the code.
Unit Tests
We should always prefer to test smaller units of our code while writing unit tests. However, for brevity, we're testing the engine and the app combined while mocking the services:
public final class MailCollectorAppTest {
private MailService gmailServiceMock;
private MailService microsoftServiceMock;
private MailService thirdServiceMock;
private RobustMailEngine robustEngine;
private MailCollectorApp sut;
private Faker faker;
@BeforeEach
public void initialize() {
faker = new Faker();
gmailServiceMock = mock(MailService.class);
microsoftServiceMock = mock(MailService.class);
thirdServiceMock = mock(MailService.class);
robustEngine =
new RobustMailEngine(Set.of(gmailServiceMock, microsoftServiceMock, thirdServiceMock));
sut = new MailCollectorApp(robustEngine);
}
@Test
@DisplayName(
"make the services mocks return no mail and validate the return string as 'No mail found'")
public void getMail_noMailExists_returnsNoMailFound() {
willReturn(emptyList()).given(gmailServiceMock).getMail();
willReturn(emptyList()).given(microsoftServiceMock).getMail();
willReturn(emptyList()).given(thirdServiceMock).getMail();
then(sut.getMail()).isEqualTo("No mail found.");
}
@Test
@DisplayName(
"make the services return legitimate mail and validate the return string as expected")
public void getMail_foundMail_returnsExpectedString() {
var mail1 =
GmailImpl.builder()
.from(faker.internet().emailAddress())
.subject(faker.lorem().sentence())
.build();
var mail2 =
MicrosoftImpl.builder()
.from(faker.internet().emailAddress())
.subject(faker.lorem().sentence())
.build();
var mail3 =
MicrosoftImpl.builder()
.from(faker.internet().emailAddress())
.subject(faker.lorem().sentence())
.build();
willReturn(List.of(mail1)).given(gmailServiceMock).getMail();
willReturn(List.of(mail2, mail3)).given(microsoftServiceMock).getMail();
willReturn(emptyList()).given(thirdServiceMock).getMail();
then(sut.getMail().split(System.lineSeparator()))
.containsOnly(mail1.toString(), mail2.toString(), mail3.toString());
}
}
The next parts of this tutorial series are Part 2 - Google Guice and Part 3 - Spring Context. We'll introduce Dependency Injection Frameworks to the above code.
Top comments (1)
Excellent start. waiting for more from this series.