Dependency inversion is principle, inversion of control (IoC) is a design pattern, dependency injection is implementation technology.

Dependency inversion principle

In object-oriented design, the dependency inversion principle is a specific methodology for loosely coupled software modules.

  • High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

Inversion of control

In software engineering, inversion of control (IoC) is a design pattern in which custom-written portions of a computer program receive the flow of control from a generic framework.

Dependency injection

Why do we need dependency injection?

In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs. The pattern ensures that an object or function which wants to use a given service should not have to know how to construct those services.

Dependency injection is often used to keep code in-line with the dependency inversion principle.

Without dependency injection

In the following Java example, the Client class contains a Service member variable initialized in the constructor. The client directly constructs and controls which service it uses, creating a hard-coded dependency.

1
2
3
4
5
6
7
8
public class Client {
    private Service service;

    Client() {
        // The dependency is hard-coded.
        this.service = new ExampleService();
    }
}

Advantages and disadvantages

Advantages

  • A basic benefit of dependency injection is decreased coupling between classes and their dependencies.
  • By removing a client’s knowledge of how its dependencies are implemented, programs become more reusable, testable and maintainable.
  • This also results in increased flexibility: a client may act on anything that supports the intrinsic interface the client expects.
  • Many of dependency injection’s benefits are particularly relevant to unit-testing.

Disadvantages

  • Makes code difficult to trace because it separates behavior from construction.
  • Is typically implemented with reflection or dynamic programming, hindering IDE automation.

Types of manual dependency injection

Constructor injection

The most common form of dependency injection is for a class to request its dependencies through its constructor. This ensures the client is always in a valid state, since it cannot be instantiated without its necessary dependencies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Client {
    private Service service;

    // The dependency is injected through a constructor.
    Client(Service service) {
        if (service == null) {
            throw new IllegalArgumentException("service must not be null");
        }
        this.service = service;
    }
}

Setter injection

By accepting dependencies through a setter method, rather than a constructor, clients can allow injectors to manipulate their dependencies at any time. This offers flexibility, but makes it difficult to ensure that all dependencies are injected and valid before the client is used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Client {
    private Service service;

    // The dependency is injected through a setter method.
    public void setService(Service service) {
        if (service == null) {
            throw new IllegalArgumentException("service must not be null");
        }
        this.service = service;
    }
}

Interface injection

In this way, the dependencies become injectors. The key is that the injecting method is provided through an interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface ServiceSetter {
    void setService(Service service);
}

public class Client implements ServiceSetter {
    private Service service;

    @Override
    public void setService(Service service) {
        if (service == null) {
            throw new IllegalArgumentException("service must not be null");
        }
        this.service = service;
    }
}

public class ServiceInjector {
	private final Set<ServiceSetter> clients = new HashSet<>();

	public void inject(ServiceSetter client) {
		this.clients.add(client);
		client.setService(new ExampleService());
	}

	public void switch() {
		for (Client client : this.clients) {
			client.setService(new AnotherExampleService());
		}
	}
}

public class ExampleService implements Service {}

public class AnotherExampleService implements Service {}

Dependency injection framework

Manual dependency injection is often tedious and error-prone for larger projects, promoting the use of frameworks which automate the process.