In this exercise we will look at how to apply the Dependency Inversion Principle (DIP) in code.
The Dependency Inversion Principle (DIP) states:
A. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). B. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
classDiagram
Sensor <|-- FireSensor
Sensor <|-- SmokeSensor
FireSensor <.. ControlUnit
SmokeSensor <.. ControlUnit
Sensor <.. ControlUnit
ControlUnit <|-- App
class Sensor
<<interface>> Sensor
class FireSensor
class SmokeSensor
class ControlUnit{
-List~Sensor~ : senors
+ControlUnit()
+pollSensors()
}
class App
✏️ Take a look at the class diagram above showing the relationships between the different parts of the Alarm System application. (Dotted arrows denotes a dependency relation, full arrows denotes an inheritance relation (see UML Class Diagrams Tutorial)).
✏️ ControlUnit class currently knows which sensors (FireSensor and SmokeSensor) are registered in the system because they are new-ed up in the class (new means glue!). This adds a thight coupling between the ControlUnit and the Sensor classes.
✏️ This thight coupling also violatetes the Open Closed Principle (OCP), because the ControlUnit class is not open for extension (eg. adding new types of sensors is not possible without changing the class).
💡 We want to apply the Dependency Inversion Principle (DIP) in order to make the ControlUnit oblivious to which type of sensor it administers, thereby decoupling it from the sensors.
📖 When we introduce this kind of loose coupling, the ControlUnit class is not prone to change as the Sensor class changes. E.g. we can add new sensor types without changing the ControlUnit class. As long as the different types of sensors adhere to the interface of how a sensor should behave. The ControlUnit class no longer has a direct dependency to the different sensor types, instead it only knows the behaviour of a sensor.
📖 A common way to implement DIP is via constructor injection. See the following class diagram:
classDiagram
Sensor <|-- FireSensor
Sensor <|-- SmokeSensor
Sensor <.. ControlUnit
FireSensor <.. App
SmokeSensor <.. App
ControlUnit <.. App
class Sensor
<<interface>> Sensor
class FireSensor
class SmokeSensor
class ControlUnit{
-List~Sensor~ : senors
+ControlUnit(List~Sensor~ : sensors)
+pollSensors()
}
class App
📖 Notice that ControlUnit no longer has any relationship to FireSensor and SmokeSensor, only the Sensor interface. Instead, the App class now controls what sensors the ControlUnit polls via a new constructor.
✏️ Add a list of sensors as new constructor parameter to ControlUnit.
✏️ Replace the new ArrayList assignment of the sensors instance variable in the constructor with the value of the new constructor parameter you added.
✏️ Remove the new Sensor statements from the constructor.
✏️ In the App class, create a list of sensor implementations (FireSensor and SmokeSensor) and pass them to the ControlUnit class constructor as a parameter.
✏️ Run the application again to make sure it still works as before.
📖 If you are completely stuck, here is an example:
Details
public class ControlUnit {
private final List<Sensor> sensors;
// Add List<Sensor> sensors parameter here:
public ControlUnit(List<Sensor> sensors) {
this.sensors = sensors;
// Remove the following:
// this.sensors = new ArrayList<>();
// sensors.add(new FireSensor());
// sensors.add(new SmokeSensor());
}
// ...
}public class App {
public static void main(String[] args) {
// Add list of sensors:
List<Sensor> sensors = new ArrayList<>();
sensors.add(new FireSensor());
sensors.add(new SmokeSensor());
// Pass list of sensors to ControlUnit constructor as parameter:
ControlUnit controlUnit = new ControlUnit(sensors);
// ...
}
}- The
ControlUnitclass violates the Dependency Inversion Principle, because the "high-level module"ControlUnitdepends on the "low-level modules"FireSensorandSmokeSensor. - By introducting constructor injection the module
ControlUnitnow depends on an abstraction (theSensorinterface) instead of contrete implementations (FireSensorandSmokeSensor), thereby adhering to the Dependency Inversion Principle. - By making the
ControlUnitnot depend directly on sensors theControlUnitnow is also adhering to the Open Closed Principle, because we can add new Sensors to poll without changing theControlUnitclass.