Decoupling Using Interfaces and Dependency Injection in Java
Author
Elle J
Date
Jan. 20, 2020
Time to Read
11 min
Prerequisites
- ★An OOP language (basic knowledge)
Whenever we modify some piece of our code, that change may cause other parts to break unless we modify them as well. This is a sign that these various parts are dependent on one another which could be very costly as it affects several modules of the application, complicates reusability and testing, and makes overall maintenance harder.
Coupling is the level of dependency between classes. When classes have a high degree of dependency on each other they are said to be tightly coupled, whereas a low degree of dependency forms loosely coupled classes. Changes made in tightly coupled classes are, as mentioned, more costly, thus we should always strive to reduce the coupling (decouple) so as to minimize the impact of changes.
We will go through some of the ways we can achieve this in Java, particularly by making use of interfaces and dependency injection.
Inheritance and Coupling
Inheritance is an extremely useful tool in programming languages, however, it can also lead to tightly coupled classes. Classes become tightly coupled whenever deep inheritance hierarchies are implemented. It may be relevant to some of these classes to add a new field at some point, so when added to one of the classes at the top of the hierarchy it consequently becomes polluted if it is useless for some of the other classes. As inheritance usually is the strongest relationship in programming languages, it is also the least flexible.
Another well-known issue with inheritance is when multiple inheritance is used. However, Java, unlike other object oriented programming languages, does not implement this feature as it may lead to the so-called Diamond Problem. As seen in the figure above, the Diamond Problem arises when a super/base class (Class A) is inherited by two other classes (Class B and Class C) whereby a fourth class (Class D) inherits from both previous two classes. If both Class B and Class C have methods with the same signature (but differ in their implementations) or fields with the same name, Class D would not know which one it should inherit from. Although, as we will see shortly, interfaces can help us take advantage of the positive aspects of multiple inheritance while circumventing the potential problems.
What Are Interfaces?
Just as a class is a type, an interface is also a type, although it is implicitly abstract (thus, we do not instantiate interfaces). Unlike a class which may contain methods and their implementation details, an interface only holds method declarations, that is to say, it only determines what behavior each class that implements it should have.
We can think of an interface as a contract, or agreement. In this contract it may be stated that anyone that signs this contract (any class that implements this interface) needs to behave as stated herein (need to implement the methods declared). Thus, the interface determines what should be done (through method declarations), whereas the class determines how it should be done (through method definitions). For instance, one interface may determine that a method for sorting or one for searching must be present. The class then implements the algorithm for that particular method.
Programming against interfaces, as it is called when working with interfaces rather than the actual implementations, allows us to decouple classes and build loosely coupled applications. So instead of using inheritance and possibly stumbling upon a variety of issues, we put an interface in between the classes. Consequently, whenever we need to make changes to a class we do not have to worry about breaking other parts of the application since they are not dependent on each other any more. This is also why unit testing is facilitated when using interfaces.
The code below shows how an interface is implemented in Java as well as how a class can make use of it. (Naming convention for interfaces vary but is sometimes an adjective, has the letter "I" as a prefix, or just describes it as is.)
public interface Playable {
// the method declarations
// (methods are implicitly public, no need for the "public" access modifier)
void play();
}
public class Game implements Playable {
// the interface methods need to be public and have the
// exact same signature as declared in the interface
// (apply the "override" annotation as a best practice)
@Override
public void play() {
/* implementation details */
}
}
The Game
class is only implementing one interface in the example above, however it is entirely possible to implement several interfaces as well. An interface (InterfaceA
) itself can also extend another interface (InterfaceB
). In these cases, all classes that implement InterfaceA
need to define all methods declared in InterfaceA
and InterfaceB
(see below).
public interface InterfaceB {
void methodB();
}
public interface InterfaceA extends InterfaceB {
void methodA();
}
public class ClassExample implements InterfaceA {
@Override
public void methodA() {
/* implementation details */
}
@Override
public void methodB() {
/* implementation details */
}
}
Keep in mind that interfaces are, as you can see, not for sharing logic. If logic needs to be shared between classes, create an abstract class. Abstract classes cannot be instantiated, but rather holds common implementation details. These classes may also implement interfaces. Java has in later editions added possibilities to have logic inside of interfaces; however, this is considered bad practice by many developers as it violates the traditional meaning of an interface.
The Interface Segregation Principle
Classes that instead use interfaces no longer get coupled to each other, but important to note is that they are indeed coupled to the interface. When the contract was signed (when the interface was implemented by a class), whatever was stated at that particular time is what was implemented. So if the contract was to change (adding or removing a declaration) it would affect all classes that implement it. For this particular reason we always want to make sure we use lightweight interfaces. This goes hand in hand with the Interface Segregation Principle which states that big interfaces ought to be divided into smaller ones.
Thus, in any interface, check whether or not the methods have different capabilities. An interface with two methods both having different capabilities should be in two separate interfaces in order to minimize the coupling points.
Dependency Injection
By now we are well aware of the problems of coupling, but also how we can make use of interfaces to reduce this problem. Abstraction is another good way of minimizing the coupling points in an application. Abstraction is when we hide unnecessary details and thereby reduce complexity. A pretty straightforward example is when making methods private instead of public. If we have the possibility to keep the implementation details in a private method we isolate the changes to that particular class. Nonetheless, there is still a possibility that there are internal dependencies in the implementations themselves.
If you find yourself instantiating a dependency of a class from within that class, you are violating the Dependency Injection Principle. According to this principle, classes should only use their dependencies, never instantiate them. This is due to coupling. Whenever we instantiate a class we are dependent on its implementation details. If, for instance, the parameters of its constructor would change, it would cause breaking changes to the classes that instantiate that class. In order to abide by the principle we use the concept of separation of concerns by letting another class create the dependency and then pass it to the class that needs it. So remember, the class that is using its dependency should not be responsible for creating it.
The concept of passing a dependency into a class in order to decouple them is called dependency injection and there are various ways of doing this:
- Constructor Injection
- When a dependency gets passed via the constructor.
- Setter Injection
- When a dependency gets passed via a setter.
- Method Injection
- When a dependency gets passed via another method.
Consider the example below which is in violation of the Dependency Injection Principle. We have a SpanishEnglishDictionary
class that is being used as a field in another class called Translator
. The violation occurs in the Translator
’s constructor where the dictionary is being instantiated.
public class SpanishEnglishDictionary {
/* fields go here */
public String translate(String word) {
/* implementation details */
}
/* other methods go here */
}
public class Translator {
private SpanishEnglishDictionary dictionary;
/* other fields go here */
// constructor
public Translator() {
// violating the Dependency Injection Principle by
// instantiating the dependency within this class
this.dictionary = new SpanishEnglishDictionary(); // ← coupling point
}
public String translate(String word) {
return dictionary.translate(word);
}
/* other methods go here */
}
Let’s see how we can decouple the Translator
and SpanishEnglishDictionary
classes in three different ways using the aforementioned dependency injections. So, let's first create a Dictionary
interface that the SpanishEnglishDictionary
will implement.
public interface Dictionary {
String translate(String word);
}
public class SpanishEnglishDictionary implements Dictionary {
@Override
public String translate(String word) {
/* implementation details */
}
}
(1) Constructor Injection
public class Translator {
// the field is now using the interface type (Dictionary)
// instead of the class type (SpanishEnglishDictionary)
private Dictionary dictionary;
// the constructor is being injected with the already-created dictionary
// which has the interface type rather than the class type
public Translator(Dictionary dictionary) {
// no longer violating the Dependency Injection Principle
this.dictionary = dictionary;
}
public String translate(String word) {
return dictionary.translate(word);
}
}
// we leave the responsibility of creating the SpanishEnglishDictionary
// instance to another class
public class Main {
public static void main(String[] args) {
var dictionary = new SpanishEnglishDictionary(); // ← instantiation
var translator = new Translator(dictionary); // ← injection
}
}
(2) Setter Injection
public class Translator {
// the field is using the interface type
private Dictionary dictionary;
// a setter is being injected with the dictionary instance
public void setDictionary(Dictionary dictionary) {
this.dictionary = dictionary;
}
}
public class Main {
public static void main(String[] args) {
var translator = new Translator();
// instantiate and inject the dependency via the setter
translator.setDictionary(new SpanishEnglishDictionary()); // ←
}
}
(3) Method Injection
public class Translator {
// a method is being injected with the dictionary
public String translate(String word, Dictionary dictionary) {
return dictionary.translate(word);
}
}
public class Main {
public static void main(String[] args) {
var translator = new Translator();
// instantiate and inject the dependency via a method
translator.translate("hola", new SpanishEnglishDictionary()); // ←
}
}
In all of the examples above we make the Translator
class dependent on an interface rather than another class. Thus if we decide to change which kind of dictionary to use we can do so without affecting the Translator
class at all as long as that dictionary class is implementing the Dictionary
interface. Likewise, if the SpanishEnglishDictionary
was to add a required parameter in the constructor, the Translator
class would be completely unaffected as it is not concerned with the instantiation. Moreover, it would only make SpanishEnglishDictionary
recompile and not Translator
.
Out of the various injection methods, the constructor injection is most commonly used since the dependencies of a class can be seen more effortlessly.
A Few Last Words
Understanding the problems that come with coupling and how we may decouple our classes is definitely necessary when building robust applications. We always want to minimize the impact of changes so that implementations can be modified or entirely swapped for a better one whenever needed. Loosely coupled classes using interfaces also allow us to easily extend the application, facilitate reusability, and create unit tests to test the classes in isolation.
For smaller personal projects, this may not turn out to be that big an issue, however, if you strive to keep certain efficiency principles in mind when designing projects it will most certainly make you a better developer.
Keep Coding and Stay Curious! :)
Comments powered by Talkyard.
Comments powered byTalkyard.