Item 5: Prefer dependency injection to hardwiring resources - saurabhojha/Effective-java GitHub Wiki
Dependency:
In object oriented programming, an object 'A' is said to be dependent on another object 'B' if object 'A' contains 'B' as a data member. This means that the execution of 'A' is dependent on 'B' and thus 'A' has a dependency on 'B'.
Coupling:
In software engineering coupling is the measurement of the dependency of an entity/object/software module over another. A highly coupled design is undesirable. Loosely coupled software design is desirable since it facilitates in isolating a module and thereby helps in easier unit testing of individual modules.
Coupling example:
A good example to understand loose and tight coupling will be to analyse buying a laptop vs buying a pc.
When we purchase a laptop, the components such as processor, gpu, psu are all soldered on the motherboard. This makes it un-upgradable. We cannot swap out these components and replace them with newer or different ones. Thus we say, that laptop is tightly coupled to the processor, gpu, psu and even mother board.
On the other hand, when we purchase a personal computer, we have the option of upgradability. We can swap out processors, gpus, power supplies and heck, even the motherboard. This gives us great flexibility over what we choose to install in our pc. This is an example of loose coupling where even though a computer requires a processor, it has the freedom to choose from different versions of these.
Dependency Injection:
It is the process of liberating a class from the task of instantiating its own dependencies and delegating some other class to inject these dependencies on their behalf. Dependency injection ensures that loose coupling is achieved.
There are various forms of dependency injection:
1. Constructor injection: In constructor injection, the dependency is injected as a parameter while object creation. Constructor injection must be used when the dependency is required for a significant proportion of the object's lifecycle. In other words use constructor injection when the dependency is strictly required by the object. (Strict Dependencies)
2. Setter injection: In setter injection, the dependency is set using a setter for the dependency field. This injection is used in case when we do not require the dependency strictly or need it only in certain scenarios. (Optional dependencies).
3. Method injection: Imagine a scenario, where the dependency needs to change every time we use it or it has many forms and we are not sure which form will the dependency assume. (Multiple implementations of an interface). One way to overcome this would be to call the setter injection method every time the dependency changes its form. This design leads to temporal coupling. In essence temporal coupling states that for a code to behave as intended, the statements must be executed in a certain order. In case the execution deviates from this order, the behaviour of the code becomes unintended or undefined. Setter injection is not a viable choice in case of multithreaded applications as it can cause temporal coupling. In such cases, method injection has to be used. In method injection, we accept the concrete class implementation as the parameter and assign it to the interface dependency in the object.
Example
Consider the following example:
We have to design a cake class. For the sake of clarity let us assume that the cake consists of only three dependencies.
- Every cake is made of cake batter. This makes batter a necessary dependency.
- A cake contains icing. However this is optional since we can savour plain cakes as well. This makes our dependency optional.
- In case a cake contains topping, depending upon a user's request, we must be able to supply different toppings to the cake. Some may like fruit toppings, and some may love chocochips on their cake. Thus this dependency is varying in nature.
Hence our cake class would look something like this:
class Cake {
private CakeBatter cakeBatter;
private Icing icing;
private Topping topping;
}
1. Adding constructor injection to the cake class: This is trivial and since cakeBatter is a strict dependency we shall pass an instance of cakeBatter while creating a Cake object. Thus the code will look something like this:
class Cake {
private CakeBatter cakeBatter;
private Icing icing;
private Topping topping;
//Constructor Injection of cakeBatter in the Cake Object
Cake(CakeBatter cakeBatter) {
this.cakeBatter = cakeBatter;
}
}
2. Adding setter injection for the icing: This is also trivial and the code would look something like this:
class Cake {
private CakeBatter cakeBatter;
private Icing icing;
private Topping topping;
//Constructor Injection of cakeBatter in the Cake Object
Cake(CakeBatter cakeBatter) {
this.cakeBatter = cakeBatter;
}
//Setter Injection of the Icing in the Cake Object
void setIcing(Icing icing) {
this.icing = icing;
}
}
3. Adding method injection for the topping:
Suppose we have an interface topping containing the method addTopping. This interface is implemented by two classes FruitTopping and ChocolateTopping.
interface Topping {
void addTopping();
}
class FruitTopping implements Topping {
@Override
public void addTopping() {
System.out.println("Adding fruit toppings.");
}
}
class ChocolateTopping implements Topping {
@Override
public void addTopping() {
System.out.println("Adding choco chips topping");
}
}
To set this topping dependency our method in the Cake class would look something like this.
class Cake {
private CakeBatter cakeBatter;
private Icing icing;
private Topping topping;
//Constructor Injection of cakeBatter in the Cake Object
Cake(CakeBatter cakeBatter) {
this.cakeBatter = cakeBatter;
}
//Setter Injection of the Icing in the Cake Object
void setIcing(Icing icing) {
this.icing = icing;
}
//Method Injection of the Topping in the Cake Object
void addToppingToCake(Topping topping) {
this.topping = topping;
topping.addTopping();
}
}
To send a particular implementation of this topping, our code will be:
//Adding fruit topping in delegated class
void addFruitTopping(Cake cake) {
Topping topping = new FruitTopping();
cake.addToppingToCake(topping);
}
//Adding chocolate topping in delegated class
void addChocolateTopping(Cake cake) {
Topping topping = new ChocolateTopping();
cake.addToppingToCake(topping);
}
Click here and here to read more examples of dependency injection.
Even though the advantages of dependency injection are great, with a large number of dependencies, maintaining and writing clean code becomes difficult. To eliminate this issue, we have dependency injection frameworks, that do the heavy-lifting for us. One such dependency injection framework is Spring, which is used for rapid application development in java.
Inversion of control:
Till now, we have been writing codes where we were responsible for creating and managing the objects. Inversion of control states that instead of us doing these tasks, delegate it to some other class to handle these, and let the class in hand focus on it's main objective. This inversion of control principle along with dependency injection form the basis of dependency management frameworks. These frameworks have containers that create, manage and inject dependencies. This greatly reduces the boilerplate code that one has to write in addition to the main functionalities of the class. These frameworks thus improve the readability, maintainability and flexibility of the code. However, it may seem too magical at times, since a lot of implementation is hidden, and can result in code which is difficult to debug or understand.