SOLID- Dependency Inversion Principal

In this post, we are focusing on the “D” – Dependency Inversion Principle. Uncle Bob tells us: “Depend on abstractions, not on concretions.” This is sometimes referred to as Inversion of Control. In 2004, Martin Fowler referred to it as Dependency Injection

What’s Dependency Injection?

Let’s take a look at a definition of Inversion of Control. Uncle Bob Martin says there are three aspects of a bad design:

  1. Rigidity – hard to change because of the impact on every other part of the system.
  2. Fragility – changes break other parts of the system.
  3. Immobility – reuse elsewhere is hard because of entanglement between components of the system

Inversion of Control addresses these three issues using Uncle Bob’s simple approach: Depend on abstractions, not on concretions. In the world of Java, that means relying on interfaces and not implementation

The World Before Dependency Injection

Let’s take a look at a counter example:

// DON'T DO THIS

public class BadExample {


    public static void main(String[] args) {

        new ComputerProcessor()

            .addComputer(new Doubler())

            .addComputer(new Squarer())

            .computeAll(8);

    }


    static class ComputerProcessor {


        private List computers = new ArrayList();


        public ComputerProcessor addComputer(Object o) {

            computers.add(o);

            return ComputerProcessor.this;

        }


        public void computeAll(long value) {

            for (Object o : computers) {

                long computedValue = -1;


                if (o instanceof Doubler) {

                    computedValue = ((Doubler) o).computeDouble(value);

                } else if (o instanceof Squarer) {

                    computedValue = ((Squarer) o).computeSquare(value);

                }

                String name = o.getClass().getSimpleName();

                System.out.println("Computer: " + name + ", value: " + value + " computed value: " + computedValue);

            }

        }

    }


    static class Doubler {


        public long computeDouble(long value) {

            return value*2;

        }

    }


    static class Squarer {


        public long computeSquare(long value) {

            return value*value;

        }

    }

}

The ComputerProcessor is very fragile in this example. If we add a new class called Cuber, and we want to be able to represent it in ComputerProcessor, we need another if statement and more casting in order to call its computeCube method.

Here’s the same example using Inversion of Control:

public class IoCExample {


    public static void main(String[] args) {

        new ComputerProcessor()

            .addComputer(new Doubler())

            .addComputer(new Squarer())

            .computeAll(8);

    }


    static class ComputerProcessor {


        private List<Computer> computers = new ArrayList<>();


        public ComputerProcessor addComputer(Computer c) {

            computers.add(c);

            return ComputerProcessor.this;

        }


        public void computeAll(long value) {

            for (Computer c : computers) {

                String name = c.getClass().getSimpleName();

                System.out.println("Computer: " + name + ", value: " + value + " computed value: " + c.compute(value));

            }

        }

    }


    interface Computer {


        long compute(long value);

    }


    static class Doubler implements Computer {


        public long compute(long value) {

            return value*2;

        }

    }


    static class Squarer implements Computer {


        public long compute(long value) {

            return value*value;

        }

    }

}

The enabling technology here is the Computer interface. Now, ComputerProcessor only deals with objects that conform to the Computer interface. If we want to add a new Cuber class, all it has to do is implement the Computer interface. Nothing need change in ComputerProcessor to deal with Cuber objects.

Dependency Injection takes this concept and systematizes it in a way that makes it even easier to interact with your interfaces. Throughout this post, I will be using a Spring Boot example to demonstrate Dependency Injection. Since 2003, Dependency Injection has been a core feature of the Spring framework. The source code can be found here


Dependency Injection, Spring Style

Let’s jump into a simple example and then break it down:

@RestController
public class HomeController {
 
    @Autowired
    GreetingService greetingService;
 
    @RequestMapping("/")
    public String home() {
        return greetingService.greet();
    }
}

Spring introduced the @Autowired annotation for dependency injection. Any of the Spring components can be autowired. These include, components, configurations, services and beans. We’ll look at a few of these in detail below.

It’s a common pattern for controllers to be responsible for managing requests and responses while services perform business logic. Let’s look at our GreetingService:


public interface GreetingService {
 
    String greet();
}

Pretty straightforward. Here’s an implementation class:

@Service public class EnglishGreetingService implements GreetingService { @Override public String greet() { return "Hello World!"; } }

The @Service annotation makes it autowireable. Spring injects the dependency into our controller. If I set a breakpoint, I can see the implementation class backing greetingService:




Together, this setup adheres to the Dependency Inversion Principle. If the internals of the implementation class change, we don’t need to touch HomeController.

Now, let’s say I want to support another GreetingService implementation. Say, FrenchGreetingService:

@Service
public class FrenchGreetingService  implements GreetingService {
 
    @Override
    public String greet() {
        return "Bonjour Monde!";
    }
}

If I try to fire up my Spring Boot app now, I will get an error:
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.stormpath.example.service.GreetingService] is defined: expected single matching bean but found 2: englishGreetingService,frenchGreetingService

Spring Boot has no way of knowing which one of my implementation classes I want to inject into the HomeController. However, it does have a number of powerful annotations to deal with this. Let’s say that I want to inject the language specific version of the greeting service based on a property setting. We can use a Configuration to tell Spring Boot which Service we want.


@Configuration
public class GreetingServiceConfig {

    @Bean
    @ConditionalOnProperty(name = "language.name", havingValue = "english", matchIfMissing = true)
    public GreetingService englishGreetingService() {
        return new EnglishGreetingService();
    }

    @Bean
    @ConditionalOnProperty(name = "language.name", havingValue = "french")
    public GreetingService frenchGreetingService() {
        return new FrenchGreetingService();
    }
}

On line 1, we are telling Spring Boot that this is a @Configuration. That causes this class to be instantiated and the beans defined within to (potentially) be exposed for use throughout the Spring ecosystem. I’ve also removed the @Service annotation from the EnglishGreetingService and FrenchGreetingService classes as it’s now this @Configuration‘s responsibility to instantiate and expose the services as beans.

The @Bean tells Spring Boot to expose a GreetingService (this is how it is able to be autowired into our HomeController). But, we are defining both beans with the same return type. That doesn’t seem right. The @ConditionalOnProperty annotation ensures that only one of these beans will be injected. It’s looking for the language.name property. Notice that there’s a matchIfMissing parameter in the case of the EnglishGreetingService. By default, that property is false. By setting it to true here, we are indicating to Spring Boot that if it doesn’t find a language.name property, that EnglishGreetingService is the default.

Let’s look at this in action. One thing to note is that Spring Boot automatically converts environment variables that are in all-caps with underscores to lowercase, dotted properties. Also, the examples below use HTTPie (https://github.com/jkbrzt/httpie), an alternative to curl. Curl will work as well. Try executing the following after doing mvn clean install from your command line:

English

java -jar target/*.jar &
http localhost:8080

Also English

LANGUAGE_NAME=english java -jar target/*.jar &
http localhost:8080

French

LANGUAGE_NAME=french java -jar target/*.jar &
http localhost:8080


Important Note: Spring has very deep support for internationalization that the above example is not taking advantage of. The example is meant for demonstration purposes only.

Well, we jumped right into the deep end of Dependency Injection (DI) with Spring. Let’s take a step back and look at the different types of DI supported by Spring


Comments

Popular posts from this blog

Microservice Pattern: SAGA

Microservice Pattern: Database per service Context

SQL vs NoSQL | Difference between SQL & NoSQL | SQL Vs NoSQL Tutorial | SQL, NoSQL system design