Onwards to the last of the SOLID principles, namely the Dependency Inversion Principle (DIP) (not to be confused with Dependency Injection (DI)). What this principle is saying is that high level modules should not depend on concrete low level modules and that both high and low level modules should depend on abstractions.

I will attempt to explain this by using bikes again. We’ll start with a Bike interface:

interface Bike {
void pedal()
void backPedal()
}

Two classes implement this interface: a MountainBike and a ClassicBike.

class MountainBike implements Bike {
override void pedal() {
//complex code that computes the inner workings of what happens
//when pedalling on a mountain bike, which includes taking into
//account the gear in which the bike currently is.
}
override void backPedal() {
//complex code that computes what happens when we back pedal
//on a mountain bike, which is that you pedal in the wrong
//direction with no discernible effect on the bike
}
}
class ClassicBike implements Bike {
override void pedal() {
//the same as for the mountain bike with the distinction that
//there is a single gear on a classic bike
}
override void backPedal() {
//complex code that actually triggers the brake function on the
//bike
}
}

As you can see, normal pedalling moves the bike forward but it involves more complexity in the mountain bike case because it has multiple gears. Back pedalling, on the other hand, does nothing on the mountain bike but it triggers the brake on the classic bike.

The reason I mentioned “complex code” in the comments describing each method is to point out that we should move said code in a different module. We’ll do this to simplify the bike classes and respect the single responsibility principle (the bike classes should not be responsible for calculating what happens when you pedal or back pedal, they should handle more high level bike stuff).

In order to do this we’ll create some behaviour classes for each type of pedalling.

class MountainBikePedalBehaviour {
void pedal() {
//complex code
}
}
class MountainBikeBackPedalBehaviour {
void backPedal() {
//complex code
}
}
class ClassicBikePedalBehaviour {
void pedal() {
//complex code
}
}
class ClassicBikeBackPedalBehaviour {
void backPedal() {
//complex code
}
}

We’ll use these classes as follows:

class MountainBike implements Bike {
override void pedal() {
var pedalBehaviour = new MountainBikePedalBehaviour()
pedalBehaviour.pedal()
}
override void backPedal() {
var backPedalBehaviour = new MountainBikeBackPedalBehaviour()
backPedalBehaviour.backPedal()
}
}
class ClassicBike implements Bike {
override void pedal() {
var pedalBehaviour = new ClassicBikePedalBehaviour()
pedalBehaviour.pedal()
}
override void backPedal() {
var backPedalBehaviour = new ClassicBikeBackPedalBehaviour()
backPedalBehaviour.backPedal()
}
}

At this moment it’s clear that the high level module MountainBike depends on the concrete low level modules MountainBikePedalBehaviour and MountainBikeBackPedalBehaviour. The same goes for the ClassicBike with its low level modules. According to DIP, both the high and low level modules should depend on abstractions. To do this we need the following interfaces:

interface PedalBehaviour {
void pedal()
}
interface BackPedalBehaviour {
void backPedal()
}

The code for our behaviour classes will be the same as before with the exception of implementing the interfaces above.

class MountainBikePedalBehaviour implements PedalBehaviour {
override void pedal() {
//same as before
}
}

and so on for the rest of the behaviour classes.

Now we need a way to pass in a PedalBehaviour and a BackPedalBehaviour to our MountainBike and ClassicBike. We can do this in the constructor or in the pedal() and backPedal() methods. In this example we will use the constructor.

class MountainBike implements Bike {

PedalBehaviour pedalBehaviour;
BackPedalBehaviour backPedalBehaviour;
public MountainBike(PedalBehaviour pedalBehaviour,
BackPedalBehaviour backPedalBehaviour) {
this.pedalBehaviour = pedalBehaviour;
this.backPedalBehaviour = backPedalBehaviour;
}
override void pedal() {
pedalBehaviour.pedal();
}
override void backPedal() {
backPedalBehaviour.backPedal();
}
}

The same goes for the ClassicBike.

Our high level modules (MountainBike and ClassicBike) no longer depend on concrete lower level modules but on the abstractions PedalBehaviour and BackPedalBehaviour.

In this situation our main app module can look something like this:

class MainModule {
MountainBike mountainBike;
ClassicBike classicBike;
MountainBikePedalBehaviour mountainBikePedalBehaviour;
ClassicBikePedalBehaviour classicBikePedalBehaviour;
MountainBikeBackPedalBehaviour mountainBikeBackPedalBehaviour;
ClassicBikeBackPedalBehaviour classicBikeBackPedalBehaviour;
public MainModule() {
mountainBikePedalBehaviour = new MountainBikePedalBehaviour();
mountainBikeBackPedalBehaviour =
new MountainBikeBackPedalBehaviour();
mountainBike = new MountainBike(mountainBikePedalBehaviour,
mountainBikeBackPedalBehaviour);

classicBikePedalBehaviour = new ClassicBikePedalBehaviour();
classicBikeBackPedalBehaviour =
new ClassicBikeBackPedalBehaviour();
classicBike = new ClassicBike(classicBikePedalBehaviour,
classicBikeBackPedalBehaviour);
}
public void pedalBikes() {
mountainBike.pedal()
classicBike.pedal()
}
public void backPedalBikes() {
mountainBike.backPedal();
classicBike.backPedal();
}
}

We can see that our MainModule depends on concrete lower level modules and not on abstractions. We can change this by passing the dependencies in the constructor like this:

public MainModule(Bike mountainBike, Bike classicBike, PedalBehaviour mBikePB, BackPedalBehaviour mBikeBPB, PedalBehaviour cBikePB, BackPedalBehaviour cBikeBPB)...

Now the MainModule depends on abstractions and the lower level modules also depend on these abstractions. The relationships between all these modules do not depend on implementation details.

In order to delay the instantiation of a concrete class as long as possible until we reach the highest module in our app, we usually rely on Dependency Injection and DI Frameworks. You can find more about DI here. We can think of dependency injection as a tool that helps us adhere to DIP. We keep passing dependencies down the chain to avoid the instantiation of concrete classes.

So why go through all this trouble? One of the advantages of not depending on concretions is that we can mock a class thus making testing easier. Let’s see a simple example of this in action.

interface Network {
public String getServerResponse(URL serverURL);
}
class NetworkRequestHandler implements Network {
override public String getServerResponse(URL serverURL) {
//network code implementation
}
}

Let’s say that we also have a NetworkManager that has a public method that returns a server response by using a Network:

public String getResponse(Network networkRequestHandler, URL url) {
return networkRequestHandler.getServerResponse(url)
}

Because we have our code setup like this, we can test how the code handles a “404” response from the server. In order to do this we will create a mock version of the NetworkRequestHandler. We can do that because the NetworkManager depends on an abstraction, namely a Network object, not on a concrete NetworkRequestHandler.

class Mock404 implements Network {
override public String getServerResponse(URL serverURL) {
return "404"
}
}

By calling getResponse and passing it an instance of our Mock404 class, we can easily test the desired behaviour. There are also mocking libraries out there like Mockito. These libraries help you mock certain classes without the need of writing a separate class to do that.

Besides the ease of testing, our app is better adapted to change. Because the relationships between modules are based on abstractions we can change the implementations of our concrete modules without the need of altering code all over the place.

Least but not least it leads to more simplicity. If you paid attention to the bike examples you can see that MountainBike and ClassicBike are now identical. That means that we don’t need separate classes anymore. We can have a simple class GenericBike that implements Bike and instantiate mountain bike and a classic bike like:

GenericBike mountainBike = new GenericBike(mbPedalB, mbBackPedalB);
GenericBike classicBike = new GenericBike(cbPedalB, cbBackPedalB);

We reduced the number of concrete bike implementations by half, which means that our code is easier to manage.

Conclusion

All these principles might seem like overkill and you might resist them. I did that for a long time. With the passing of time I naturally started shifting my code to be testable and easier to maintain. I started thinking things like: “if only there was a way to separate this part from that part and put that one in a different class so that I can …”. Usually the answer was that there is a way and someone else already implemented that. Most of the times that way was inspired by the SOLID principles. Of course that tight deadlines and other real-life factors might prevent you from adhering to all of these principles. It is difficult to be 100% SOLID but it is much better to be 30% SOLID than not at all. Maybe you can isolate the most susceptible to change parts of your code and try to implement these principles over there. You don’t have to go overboard with these principles, think of them as guidelines that help you improve code quality. If you have to make a quick prototype or a proof of concept app, there’s no need to try to build the best architecture. SOLID is more of a long term strategy, very useful for software that must stand the test of time.

In this three part article, I tried to present some bits of SOLID that I found interesting. There are many opinions and many interpretations out there. To get a better understanding and gain knowledge from multiple perspectives, read other articles as well.

I hope that you found this article helpful.

If you haven’t already, you can read part 1 here and part 2 here. If you liked this post you can find more on our website.

--

--

Razvan Soare

Cofounder @ Bit Treat Software where we build mobile apps & games.