DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Dependency Injection in Spring
  • Understanding the Dependency Injection Lifecycle: Singleton, Scoped, and Transient With Detailed Examples
  • Singleton: 6 Ways To Write and Use in Java Programming
  • The ABCs of Unity's Coroutines: From Basics to Implementation

Trending

  • How to Perform Custom Error Handling With ANTLR
  • Data Quality: A Novel Perspective for 2025
  • Unlocking AI Coding Assistants: Generate Unit Tests
  • Agile’s Quarter-Century Crisis
  1. DZone
  2. Coding
  3. Languages
  4. About Dependency Injection

About Dependency Injection

Still wrapping your head around DI? Let's explore the philosophy and functionality behind it, then see how Guice, or any other such framework, can help.

By 
Ioan Tinca user avatar
Ioan Tinca
·
Dec. 29, 17 · Tutorial
Likes (16)
Comment
Save
Tweet
Share
26.5K Views

Join the DZone community and get the full member experience.

Join For Free

When I first heard about Dependency Injection at my first workplace, it sounded very fancy to me, and, somehow, a bit scary. And, in combination with Spring, it looked like black magic. But after reading a bit about it, I think this expression "dependency injection is a 25-dollar term for a 5-cent concept" describes it perfectly. I don’t want to say that it is a cheap pattern: It is not! It is extremely helpful and a must-have in software development. But it is simple, and its simplicity makes this pattern simply brilliant.

So What Is It?

Let’s see a simple example to understand better what this means. Let’s build a simple payment service for an online shop. We have a cart with a list of products, and we create a method that iterates all the products and uses a cash payment service to pay for them.

public class CashPaymentService {
    public void pay(Product product) {
        System.out.println(“Payed with cash“);
    }
}

public class Cart {
    private List < Product > productList = new ArrayList < > ();
    private CashPaymentService paymentService = new CashPaymentService();

    public addProductToCart(Product product) {
        productList.add(product);
    }

    public void buy() {
        productList.stream().forEach(paymentService::pay);
    }
}


This works.

But why should the Cart be responsible for creating the CashPaymentService? The Cart is tightly coupled to the CashPaymentService object, and there are several problems with this. What if we want to use another payment service? Maybe we want to add a service for paying with a credit card. Then, in this case, we would have to change the implementation of the Cart object to use another payment service — and this is not Cart’s responsibility. Or maybe we want to have a mock payment service for testing the Cart object. Then we should change the Cart object again to use a mocked payment service in a test environment.

The problem here is the combination of concerns in the Cart object. The Cart should do just what it is meant to do. It holds a list of products and offers a method for buying them. The payment service is an external dependency here. This service is used by the Cart, but it is not Cart’s responsibility to create it. The Cart should just receive this dependency. There are multiple ways to do this, and the next code snippet shows the simplest method. We send the dependency through a constructor. This is Dependency Injection in its simplest form.

public class Cart {
    private List < Product > productList = new ArrayList < > ();
    private CashPaymentService paymentService;

    public Cart(CashPaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public addProductToCart(Product product) {
        productList.add(product);
    }

    public void buy() {
        productList.stream().forEach(paymentService::pay);
    }
}


The Cart object needs a payment service. It is not responsible to create it. It is just an external dependency that it must receive, so it receives it when the object is instantiated through its constructor. You also can use setter methods — doesn’t matter. The point of Dependency Injection is just to inject the dependency and not to make a class responsible for creating or looking up to its dependencies. This way, with the DI pattern, we achieve separation of concerns.

Improvements

This does not resolve the problem that the Cart object is tightly coupled to the CashPaymentService. Even if the CashPaymentService is injected, the Cart is strongly tied to the CashPaymentService implementation. The solution here is simple, and we know it from the strategy pattern. The key of the strategy pattern that we'll use here is to program to an interface, not an implementation. So we need to create a PaymentService interface that has only one method: pay(). This is the only thing that the Cart must know about the PaymentService.

public interface PaymentService {
    void pay(Product product);
}

public class Cart {
    private List < Product > productList = new ArrayList < > ();
    private PaymentService paymentService;

    public Cart(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public addProductToCart(Product product) {
        productList.add(product);
    }

    public void buy() {
        productList.stream().forEach(paymentService::pay);
    }
}


Now the Cart is not tied to the CashPaymentService implementation, so we are free to inject whatever PaymentService implementation we need.

public class CashPaymentService implements PaymentService {
    @Override
    public void pay(Product product) {
        System.out.println(“Payed with cash“);
    }
}

public class CardPaymentService implements PaymentService {
    @Override
    public void pay(Product product) {
        System.out.println(“Payed with credit card“);
    }
}

public class TestPaymentService implements PaymentService {
    @Override
    public void pay(Product product) {
        System.out.println(“Test payment“);
    }
}


We have multiple ways of injecting the desired dependency when we instantiate the Cart object. Let’s see an example using the Factory Pattern.

public class PaymentServiceFactory {
    public PaymentService getPaymentService(String paymentType) {
            if (paymentType == null) {
                return null;
            }
            if (paymentType.equalsIgnoreCase(“CASH”) {
                    return new CashPaymnetService();
                }
                if (paymentType.equalsIgnoreCase(“CARD”) {
                        return new CardPaymnetService();
                    }
                    if (paymentType.equalsIgnoreCase(“TEST”) {
                            return new TestPaymnetService();
                        }
                        return null;
                    }
                }


Now, we can use this factory class to build whatever payment service we want.

public class Demo {
    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService(“CARD”);
        Cart cart = new Cart(paymentService);
    }
}


What did we accomplish here? Using Dependency Injection, the Cart object receives its dependencies. Using the strategy pattern, we decouple the Cart from a specific payment service, and we can use whatever payment service we desire. Using the factory pattern, we can specify, with a string parameter, which payment service we want, and the factory class is responsible for creating it. The code now is loosely coupled and clearly looks better than before.

So Why All the Frameworks?

Using the factory pattern, the dependencies are hidden in the code. If you want to add a new implementation, you have to modify the factory class. Moreover, using the factory pattern, we have to call the “new” keyword. It’s our responsibility to instantiate the objects. For now, that does not sound so bad, but what if we have a large project with hundreds of classes, and all of them have all kinds of dependencies to each other? Well, then our factory class will evolve into a monstrosity where we will have to manage a very big and ugly dependency tree.

Dependency Injection frameworks, like Spring or Guice, are here to do this for us, and they do it well. Using frameworks like these, you don’t have to manage the dependency tree. They encourage you to program by interface and not by implementation, and it is their responsibility to create the necessary objects and to inject them where you need while maintaining the application’s dependency tree. Let’s see how the payment service looks using the Guice framework.

Like I said before, these frameworks encourage you to program by interface and not by implementation, just like the strategy pattern. So let’s start with our PaymentService interface and then build the CashPaymentService implementation.

interface PaymentService {
    public void pay(Product product);
}

public class CashPaymentService implements PaymentService {
    @Override
    public void pay(Product product) {
        System.out.println(“Payed with cash“);
    }
}


The Cart object is the same, except is just using the @Inject annotation before the constructor. The annotation is used by Guice to know where to inject the dependencies.

public class Cart {
    private List < Product > productList = new ArrayList < > ();
    private PaymentService paymentService;

    @Inject
    public Cart(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public addProductToCart(Product product) {
        productList.add(product);
    }

    public void buy() {
        productList.stream().forEach(paymentService::pay);
    }
}


We now have the interface and the implementation, so let’s see how we could bind them together. Guice requires a module for configuring the bindings. A module is just a class that extends the AbstractModule class. This class is the central point of the Guice framework. Here we keep all the dependency configurations, all the bindings, and their scopes. We are not forbidden from using a single module class — we can as how many of them as we want for different purposes. But let’s start with a single one and see what it does for us.

public class PaymentModule extends AbstractModule {
    @Override
    protected void configure configure() {
        bind(PaymentService.class).to(CashPaymentService.class);
        bind(Cart.class);
    }
}


In this module, we specify that the PaymentService interface is bound to the CashPaymentService implementation. What this means is that when Guice sees a class that has a dependency to the PaymentService class, it will inject an instance of the CashPaymentService implementation. Let’s see how we can use it.

public class Demo {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new PaymentModule());

        // Now, whenever we want an instance we tell the injector to create it for us.
        Cart cart = injector.getInstance(Cart.class);
    }
}


The injector will now create a Cart object, and it will inject the necessary dependencies — in this case, a new CashPaymentService.

The module classes in Guice are very powerful. With them, we can manage all the instances in a big application. We can create binding annotations and use different scopes, like @Singleton, @SessionScoped, or @RequestScoped. Here is some great documentation for Guice, where you can see all the things that Guice can do for us.

Summary

Using Dependency Injection, your classes are not tightly coupled to their dependencies. You specify what dependencies you need in your class and are concerned just for the class's own business logic. Asking for interfaces (and not just for concrete classes) gives you the liberty to choose between multiple implementations.

In a very large application, it could become impossible to manage all the dependencies, so why not let someone trustworthy to do this? I showed, after a little introduction, what Guice can do, and Guice is not the only one. You can also use Spring or Play. They look different, but all accomplish the same thing. They manage your dependencies and inject them.

Dependency injection Object (computer science) Implementation Interface (computing) Factory (object-oriented programming)

Opinions expressed by DZone contributors are their own.

Related

  • Dependency Injection in Spring
  • Understanding the Dependency Injection Lifecycle: Singleton, Scoped, and Transient With Detailed Examples
  • Singleton: 6 Ways To Write and Use in Java Programming
  • The ABCs of Unity's Coroutines: From Basics to Implementation

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!