Dependency inversion principle

The dependency inversion principle states the following:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (that is, interfaces)
  • Abstractions should not depend on details. Details (such as concrete implementations) should depend on abstractions

The first point may remind you of the LoD. It is largely talking about the same concept: the separation of high-level from low-level.

Our abstractions should be separated (decoupled) in such a way that we can easily change low-level implementation details at a later date without having to refactor all of our code. The dependency inversion principle, in its second point, suggests that we do this via intermediary abstractions through which the high-level modules can interface with the low-level details. These intermediary abstractions are sometimes known as adapters, as they adapt a low-level abstraction for consumption by a high-level abstraction.

Why is it called dependency inversion? A high-level module may initially depend on a low-level module. In languages that provide OOP concepts such as abstract classes and interfaces (a type of schematic for classes), such as Java, it can be said that a low-level module may end up depending upon the interface, as it is the interface that provides the scaffolding on which the low-level module is implemented. The high-level module also depends on this interface, so that it may utilize the low-level module. We can see here how the dependencies are inverted so that both high- and low-level modules depend on the same interface.

Considering our Calendar application once again, let's say that we wanted to implement a way to retrieve events happening within a specific radius of a fixed location. We may choose to implement a method like so:

class Calendar {

getEventsAtLocation(targetLocation, kilometerRadius) {

const geocoder = new GeoCoder();
const distanceCalc = new DistanceCalculator();

return this.events.filter(event => {

const eventLocation = event.location.address
? geocoder.geocode(event.location.address)
: event.location.coords;

return distanceCalc.haversineFormulaDistance(
eventLocation,
targetLocation
) <= kilometerRadius / 1000;

});

}

// ...

}

The getEventsAtLocation method here is responsible for retrieving events that are within a certain radius (measured in kilometers) from a given location. As you can see, it uses both a GeoCoder class and a DistanceCalculator class to achieve its purpose.

The Calendar class is a high-level abstraction, concerned with the broad concepts of a calendar and its events. The getEventsAtLocation method, however, contains a lot of location-related details that are more of a low-level concern. The Calendar class here is  concerns itself with which formula to utilize on the DistanceCalculator and the units of measurement used in the calculation. You can see how, for example, the kilometerRadius argument must be divided by 1000 to get the distance in meters, which is then compared to the distance returned from the haversineFormulaDistance method.

All of these details should not be the business of a high-level abstraction, such as Calendar. The dependency inversion principle asks us to consider how we can abstract away these concerns to an intermediary abstraction that acts as a bridge between high-level and low-level. One way in which we may accomplish this is via a new class, EventLocationCalculator:

const distanceCalculator = new DistanceCalculator();
const geocoder = new GeoCoder();
const METRES_IN_KM = 1000;

class EventLocationCalculator {
constructor(event) {
this.event = event;
}

getCoords() {
return this.event.location.address
? geocoder.geocode(this.event.location.address)
: this.event.location.coords
}

calculateDistanceInKilometers(targetLocation) {
return distanceCalculator.haversineFormulaDistance(
this.getCoords(),
targetLocation
) / METRES_IN_KM;
}
}

This class could then be utilized by the Event class in its own isEventWithinRadiusOf method. An example of this is shown in the following code:

class Event {

constructor() {
// ...
this.locationCalculator = new EventLocationCalculator();
}

isEventWithinRadiusOf(targetLocation, kilometerRadius) {
return locationCalculator.calculateDistanceInKilometers(
targetLocation
) <= kilometerRadius;
}

// ...

}

Therefore, all the Calendar class needs to concern itself with is the fact that Event instances have isEventWithinRadiusOf methods. It needs no information and makes no prescriptions as to the specific implementation that determines distances; the details of that are left to our lower-level EventLocationCalculator class:

class Calendar {

getEventsAtLocation(targetLocation, kilometerRadius) {
return this.events.filter(event => {
return event.isEventWithinRadiusOf(
targetLocation,
kilometerRadius
);
});
}

// ...

}

The dependency inversion principle is similar to other principles that are related to the delineation of abstractions, such as the interface segregation principle, but is specifically concerned with dependencies and how these dependencies are directed. As we design and build abstractions, we are, implicitly, setting up a dependency graph. For example, if we were to map out the dependencies for the implementation that we arrived at, then it would look something like this:

It's incredibly useful to draw dependency graphs such as these. They are a useful way to explore the true complexity of your code, and can often highlight areas of possible improvement. Most importantly, they let us observe where, if anywhere, our low-level implementations (details) impact our high-level abstractions. Only when we see such situations can we remedy them. So, as you advance through this book and beyond, always have in your mind's eye a graph of the dependencies; it'll help to steer you toward more decoupled, and thus more reliable, code.

The dependency inversion principle is the very last of the SOLID acronym, and SOLID, as we've seen, is chiefly concerned with how we go about building abstractions. The next principle we'll cover binds together a lot of the content we've covered so far, as it is the principle of abstraction itself. If we remember nothing else from this chapter, then we should, at least, remember the abstraction principle.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.111.125