In this article, we will find something out about Dependency Inversion Principle. Let’s get started.


Table of contents


What is Dependency

In fact, in object-oriented programming all this can be summarized by classes depending on other classes. Whenever class A uses another class B, then it is said that A depends on B. A can not work without B, and A can not be reused without also reusing B. In such a situtation, the class A is called a dependent, and the class B is called a dependency.

These classes are coupled either strongly or loosely, but let’s see a concrete example.

Here we have a class, BookService, whose job is to create books. A book is represented by a book class that contains the title of the book and a number. This number is actually an ISBN number generated by an IsbnGenerator class, which has a method called generateNumber().

In the above diagram, BookService depends on an IsbnGenerator to create a book. Without an ISBN, the book could not be created. This dependency between classes is typical in object-oriented design. Classes have separate concerns.

  1. Strongly coupled dependencies

    Two classes that use each other are called coupled. Decoupling between classes can be loose or tight. Tight coupling leads to strong dependencies between classes. In an above example, IsbnGenerator is a class that has a unique method, generateNumber(), that returns an ISBN as a string. The simplicity will develop a very simple algorithm that generates a random number, starting by 13.

     public class IsbnGenerator {
         public String generateNumber() {
             return "13-84356-" + Math.abs(new Random().nextInt());
         }
     }
    

    On the other hand, the BookService class is in charge of creating a book object. The createBook() method takes a title as a parameter and returns a Book object.

     public class BookService {
         private IsbnGenerator isbn = new IsbnGenerator();
    
         public Book createBook(String title) {
             return new Book(title, isbn.generateNumber());
         }
     }
    

    To complete, the Book object needs the title, as well as an ISBN number, and for that it delegates the work to the IsbnGenerator class. As we can see, there is a strong dependency between those two classes. The BookService class depends on the IsbnGenerator class, but what’s wrong with that?

    This type of depdency on the IsbnGenerator class means that BookService is only capable of creating books with ISBN numbers. It cannot use any other number generator, if needed. We can say that BookService is tightly coupled to the IsbnGenerator class and thereby the number generator algorithm. It shows the strong coupling between classes can be bad because it decreases reuse. Remember that in OOP code, reuse is the idea that a class written at one time can be used by another class written at a later time.

    Strong coupling reduces reusability and, therefore, development speed, code quality, code readability, and so forth.

  2. Loosely coupled Dependencies

    A less tightly couplied solution would help in changing the NumberGenerator implementation at runtime. A way of doing it is through interfaces. Instead of depdending on the IsbnGenerator class, the BookService could depend on a NumberGenerator interface. This interface has one method called generateNumber() and is implemented by IsbnGenerator. If we need to generate ISSN numbers, we just create a new class called IssnGenerator that implements a different NumberGenerator algorithm. The BookService ends up depending on either an IsbnGenerator or an IssnGenerator according to some conditions or environment.

    In terms of code, it’s quite easy. Everything starts with a Number Generator interface that defines a generateNumber() method. This interface is implemented by the IsbnGenerator, which defines its own NumberGenerator algorithm, here a random number with a prefix starting with 13. To have a different implementation, we create a new class that implements a same NumberGenerator interface and redefines a different NumberGenerator algorithm, this time a number starting with 8.

     public interface NumberGenerator {
         String generateNumber();
     }
    
     public class IsbnGenerator implements NumberGenerator {
         public String generateNumber() {
             return "13-84356-" + Math.abs(new Random().nextInt());
         }
     }
    
     public class IssnGenerator implements NumberGenerator {
         public String generateNumber() {
             return "8-" + Math.abs(new Random().nextInt());
         }
     }
    

    Now that the classes are not directly coupled, how would we connect a BookService to either an ISBN or ISSN implementation? One solution is to pass the implementation to the constructor and leave an external class to choose which implementation is wants to use. So let’s refactor our BookService.

     public class BookService {
         private NumberGenerator generator;
    
         public BookService(NumberGenerator generator) {
             this.generator = generator;
         }
    
         public Book createBook(String title) {
             return new Book(title, generator.generateNumber());
         }
     }
    
     BookService service = new BookService(new IsbnGenerator());
    

    The BookService depends on an interface, not implementation. The implementation is passed as parameter of the constructor. So if we need a BookService that generates an ISBN number, we just pass the IsbnGenerator implementation to the constructor. If we need to generate an ISSN number, we just change the implementation to be IssnGenerator. This is what’s called Inversion of Control. The control of choosing the dependency is inverted because it’s giving to an external class, not the class itself. But we ends up connecting the dependencies ourselves using the constructor to choose implementation. This is called Constructor injection. Our techniques can be used, but all-in-all is just constructing dependency programmatically by hand, which is not flexible. Instead of constructing depedencies by hand, we can leave an injector to do it by using some frameworks such as CDI of Java EE, Dependency Injection of Spring framework, or something else.


Dependency Inversion Principle

The dependency Inversion Principle states that:

1. High level modules should not depend on low level modules; both should depend on abstractions.

2. Abstractions should not depend on details. Details should depend upon abstraction.

With the definitions of DIP, we have some questions such as What is high-level module or a low-level module?

  1. High level modules

    High level modules are the part of our application that bring real value. They are the modules written to solve real problems and use cases.

    They are more abstract and map to the business domain. Most of us call this business logic. Each time we hear the words business logic, we are referring to those high-level modules that provide the features of our application. High level modules tell us what the software should do, not how it should do, but what the software should do.

  2. Low level modules

    Low level modules are implementation details that are required to execute the business policies. Because high-level modules tend to be more abstract in nature, at some point in time, we will need some concrete features that help us to get our business implementation ready. They are the plumbing for the internals of a system. And they tell us how the software should do various tasks. So, high level modules tell us what the software should do, and low level modules tell us how the software should do various takss.

    For example, logging, data access, network communication, and IO.

  3. Abstraction

    Something that is not concrete.

    Something that we can not “new” up. In Java applications, we tend to model abstractions using interfaces and abstract classes.

Let’s take a look at how this principle actually works. Traditionally, when we depend on details, our components tend to look like this. We have high level components, which directly depend upon low level components. Of course, this violates the dependency inversion principle because both should depend on abstractions.

Component A, which is a high level component, no longer depends directly on component B. It depends upon an abstraction. And component B, which is low level, also depends upon that abstraction.

For example,

// low level class
// It's a concrete class that use SQL to return products from the database.
class SqlProductRepo {
    public Product getById(String productId) {
        // grab product from SQL database
    }
}

// High level class
class PaymentProcessor {
    public void pay(String productId) {
        SqlProductRepo repo = new SqlProductRepo();
        Product product = repo.getById(productId);
        this.processPayment(product);
    }
}

We can easily find that PaymentProcessor has a direct dependency with the SqlProductRepo. Because in pay() method, we actually instantiate the repo of SqlProductRepo. We are newing up a new instance of the SqlProductRepo class. This clearly violates the dependency inversion principle. We will refactor to make this code better.

interface ProductRepo {
    Product getById(String productId);
}

// low level class depends on abstraction
class SqlProductRepo implements ProductRepo {
    @Override
    public Product getById(String productId) {
        // concrete details for fetching a product
    }
}

class PaymentProcessor {
    public void pay(String productId) {
        ProductRepo repo = ProductRepoFactory.create();
        Product product = repo.getById(productId);
        this.processPayment(product);
    }
}

class ProductRepoFactory {
    public static ProductRepo create(String type) {
        if (type.equals("mongo")) {
            return new MongoProductRepo();
        }

        return new SqlProductRepo();
    }
}

Now, our pay() method does not directly depend on a concrete implementation of the ProductRepo. We depend on the abstraction. We depen upon the ProductRepo interface. The factory will give us a concrete instance. It can be a SqlProductRepo, a MongoProductRepo, or an ExcelProductRepo. It doesn’t really matter, and this high level component doesn’t care what instance is served at runtime as long as it respects the contract. The factory is pretty simple. It has a static method that returns an instance of a ProductRepo abstraction.


Introduction to Dependency Injection

Dependency Injection is very used in conjunction with the Dependency Inversion Principle. However, they are not the same thing. Let’s look at how we left the PaymentProcessor class.

We have the pay() method, and the ProductRepo abstraction is now produced by the ProductRepoFactory. Although we have eliminated the coupling with the concrete SqlProductRepo class, we still have a small coupling with the ProductRepoFactory. We have more flexibility after applying the dependency inversion principle, but we can do a better design than this. Let’s come up with a better solution. This is where our dependency injection comes in.

  1. Dependency Injection

    Dependency injection is a technique that allows the creation of dependent objects outside of a class and provides those objects to a class.

    We have various methods of doing this. One of them is by using public setters to set those dependencies. However, this is not a good approach because it might leave objects in an uninitialized state. A better approach is to declare all the dependencies in the component’s constructor like we are doing like the following.

     class PaymentProcessor {
         public PaymentProcessor(ProductRepo repo) {
             this.repo = repo;
         }
    
         public void pay(String productId) {
             Product product = this.repo.getById(productId);
             this.processPayment(product);
         }
     }
    
     ProductRepo repo = ProductRepoFactory.create();
     PaymentProcessor paymentProc = new PaymentProcessor(repo);
     paymentProc.pay("123");
    
  2. Types of Dependency Injection

    Dependency Injection can be performed by using:

    • Constructor injection

      For example, with using CDI container.

        public class DataUtil {
      
            @Produces
            private RealData data = new RealData();
        }
      
        public class Notification {
      
            private RealData realData;
      
            @Inject
            public Notification(RealData realData) {
                this.realData = realData;
            }
      
            // ...
        }
      
    • Field injection

      For example, with using CDI container.

        public class Notification {
      
            @Inject
            private RealData data;
      
        }
      
    • Method injection

      For example, with using CDI container.

        public class Notification {
      
            private RealData data;
      
            @Inject
            public void setData(RealData data) {
                this.data =data;
            }
      
        }
      


Inversion of Control

Inversion of Control can help us create large system by taking away the reponsibility of creating objects.

Inversion of control is a design principle in which the control of object creation, configuration, and lifecycle is passed to a container or framework.

The control of creating and managing objects is inversed from the programmer to this container. We do not have to new up objects anymore. Something else creates them for us, and that something else is usually called an IoC container or DI container. The control of object creation is inverted. It’s not the programmer but the container that controls those objects. It makes sense to use it for some objects in an application like services, data access, or controllers.

However, for entities, data transfer objects, or value objects, it doesn’t make sense to use an IoC container. We can simply new up those objects, and it’s perfectly OK from architectural point of view.

There are many benefits in using an IoC container for our system.

  • First of all, it makes it easy to switch between different implementations of a particular class at runtime.
  • Then, it increases the programs modularity.
  • Last but not least, it manages the lifecycle of objects and their configuration.

For example, at the core of the Spring framework is the Spring IoC container. Spring beans are objects used by our application and that are managed by the Spring IoC container. They are created with the configuration that we supply to the container.

There are many ways to configure an IoC container in Spring. XML is one example.Creating configuration classes is another. Or simply by annotating classes with special annotations like @Service, @Component, @Repository, …

@Configuration
public class DependencyConfig {
    @Bean
    public A a() {
        return new A();
    }

    @Bean
    public B b() {
        return new B();
    }

    @Bean
    public C c(A a, B b) {
        return new C(a, b);
    }
}


Dependency Injection with CDI of Java EE

  1. Setup library to use CDI in our Java project

    CDI is a standard dependency injection framework included in Java EE 6 and later.

    It allows us to manage the lifecycle of stateful components via domain-specific lifecycle contexts and inject components (services) into client objects in a type-safe way.

    • Use javaee-api with version 8.0

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
      

      To understand about Java EE 8, we can read deeper in this link.

      To migrate a Java EE 8 project to Jakarta EE 8, replace the following dependency:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
      

      …with Jakarta EE 8 API

        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>8.0.0</version>
            <scope>provided</scope>
        </dependency>
      
    • Use CDI 2.0

        <dependency>
            <groupId>javax.enterprise</groupId>
            <artifactId>cdi-api</artifactId>
            <version>2.0</version>
            <scope>provided</scope>
        </dependency>
      
  2. Implementation with CDI

    • Use qualifier in CDI

      Context independency injection is a standard solution that manages dependency between classes. Injection is made using strongly type annotations, as well as XML configuration if needed. CDI removes boilerplate code by using a very simple API, so we do not have to use construction of dependencies by hand, and CDI brings many other features to dependency injection. To see how this works, let’s take back our example.

      Nothing has changed in the above code. What changes is the way BookServices manages its dependencies. Basically it use @Inject annotation from CDI to inject the implementation of the NumberGenerator. This leaves the constructor useless, and we can just get rid of it. That means that the way of instantiating BookService has also changed. Instead of calling its constructor, we also need to inject it with CDI. Then to switch implementations, we use annotations and this way get a ThirteenDigits NumberGenerator, an EightDigits NumberGenerator or any other one.

        // Use qualifier to specify which beans will be chosen
        @Qualifier
        @Retention(RUNTIME)
        @Target({ FIELD, TYPE, METHOD, PARAMETER })
        public @interface ThirteenDigits {
        }
      
        public class BookService {
      
            @Inject
            @ThirteenDigits
            private NumberGenerator generator;
      
            public Book createBook(String title) {
                return new Book(title, generator.generateNumber());
            }
        }
      
        @Inject
        BookService bookService;
      

      CDI is a managed environment where the container uses a type-safe approach to inject the right dependency.

    • Use @Named annotation

      Beside the way that uses qualifier to differentiate many beans with same type, CDI allows us to perform service injection with the @Named annotation. This method provides a more semantic way of injecting services, by binding a meaningful name to an implementation:

        @Named("GiffFileEditor")
        public class GiffFileEditor implements ImageFileEditor {
            // ...
        }
      
        @Named("JpgFileEditor")
        public class JpgFileEditor implements ImageFileEditor {
            // ...
        }
      
        @Named("PngFileEditor")
        public class PngFileEditor implements ImageFileEditor {
            // ...
        }
      

      At the moment, we will inject one of above beans in ImageFileProcessor class with constructor injection, or field injection, or method injection.

        public class ImageFileProcessor {
      
            // field injection
            // @Inject
            // private @Named("PngFileEditor") ImageFileEditor editor;
      
            @Inject
            public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor editor) {
                // ...
            }
      
            // @Inject
            // public void setEditor(@Named("PngFileEditor") ImageFileEditor editor) {
            //    this.editor = editor;
            // }
      
        }
      


Advantages

  • Loose coupling
  • Easier testing
  • Better layering
  • Interface-based design
  • Dynamic proxies (segue to AOP)


Benefits of SOLID code

This is the last section of SOLID principles. So, we will conclude benefits when using SOLID in our code.

  • Easy to understand and reason about.

  • Changes are faster and have a minimal risk level.

  • Highly maintainable over long periods of time.

  • Cost effective.


Wrapping up


Thanks for your reading.


Refer:

SOLID Software Design Principles in Java

https://keyholesoftware.com/2014/02/17/dependency-injection-options-for-java/

http://olivergierke.de/2013/11/why-field-injection-is-evil/

https://www.baeldung.com/java-ee-cdi