DEV Community

Cover image for Dependency Injection in Java is easy - Part 1 - A Design Pattern
Tomer Figenblat
Tomer Figenblat

Posted on • Updated on

Dependency Injection in Java is easy - Part 1 - A Design Pattern

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;
}
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

An interface for contracting services responsible for pulling Mail from suppliers, the MailService.

public interface MailService {
  List<Mail> getMail();
}
Enter fullscreen mode Exit fullscreen mode

And last, an interface for contracting an engine responsible for collecting Mail from multiple services, the MailEngine.

public interface MailEngine {
  List<Mail> getAllMail();
}
Enter fullscreen mode Exit fullscreen mode

Implementations

Mail

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode
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());
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice TightMailEngine tightly depends on both GmailService and MicrosoftService. We can fix two things in this code with Dependency Injection.

  1. Testing this code is hard, invoking the getAllMail method will invoke the real Gmail and Microsoft's services.

  2. 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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
nezam05 profile image
Nezam uddin

Excellent start. waiting for more from this series.