In the first part, we mostly talked about the first two SOLID principles namley the Single Responsibility Principle and the Open-Closed Principle. In this part we’ll tackle the next two principles in the order in which they appear in the acronym. Let’s get to it.

L

The SOLID principle with the most cryptic name is the Liskov Substitution Principle (LSP). It is named after Barbara Liskov who first presented this principle in 1987. What this principle says is that if object A is a subclass of object B, or if object A implements interface B (so basically if A is also a B), then we should be able to use A like a B with no special treatment.

To clear things up, let’s see an example with more bikes. We have the base class Bike:

class Bike {
void pedal() {
//pedal code
}

void steer() {
//steering code
}
void handBrakeFront() {
//hand braking front code
}
void handBrakeBack() {
//hand braking back code
}
}

A MountainBike class extends Bike like this:

class MountainBike extends Bike {
void changeGear() {
//change gear code
}
}

MountainBike respects LSP because it can be treated like a Bike. If we have an array of type Bike and we fill it with both Bike and MountainBike items, we can call steer() and pedal() and all the Bike methods with no issues. So we can treat all MountainBike items like they are Bike items with no special treatment.

Now imagine we add a class named ClassicBike that looks something like this.

class ClassicBike extends Bike {
void footBrake() {
//foot braking code
}
}

This class represents one of those classic bikes on which you brake by trying to pedal backwards. There are no hand brakes on this bike. In this situation, if we have ClassicBike elements mixed in our Bike array, we can still call steer and pedal with no issues. Problems arise when we try to call handBrakeFront or handBrakeBack. Calling these methods might result in a crash or do nothing, depending on their implementation. We can work around this and check to see if the current element is a ClassicBike like this:

foreach(var bike in bikes) {
bike.pedal()
bike.steer()

if(bike is ClassicBike) {
bike.footBrake()
} else {
bike.handBrakeFront()
bike.handBrakeBack()
}
}

As you can see, we cannot treat a ClassicBike like a Bike without special treatment. This clearly breaks the LSP. There are many ways to fix this and we’ll see one way to do that when we talk about the I in SOLID. An interesting consequence of adhering to LSP is that it’s harder to write code that doesn’t also adhere to the Open-Closed Principle.

There is no spoon

One of the problems with object oriented programming is that we forget that we’re working with data and not playing with real world objects. Some things from real life cannot be modelled straight forwardly in code. We must remember that abstractions are not magical and that the underlying data is just data (not a real bike).

Not paying attention to LSP can get you in all sorts of trouble. Let’s say that Donald, a programmer from a previous article, writes a String subclass called SuperSmartString. It does all kinds of stuff and overrides some of the String methods. He writes it in such a way that clearly breaks LSP. Afterwards he uses instances of SuperSmartString all around in his code and treats them as Strings. After a while he notices that “strange” and “mystical” bugs start popping up in all kinds of places. When issues like these are occurring, the programmer starts blaming the programming language, the compiler, the platform, the operation system, the city mayor and God. These kind of “magical” bugs can be avoided by adhering to LSP. If not for code quality, this principle should be respected simply for the programmer’s sanity. If you work on a mildly complex project, you only need a few “SuperSmartStrings” and a few “ClassicBikes” to make your work unbearable.

I

We’re almost there. Two more principles to go. The I stands for the Interface Segregation Principle (ISP). This one is pretty easy to understand. It states that we should keep our interfaces small and implement more smaller interfaces than one large interface. I’ll use bikes again but this time instead of a Bike base class I’ll use a Bike interface like this:

interface Bike {
void pedal()
void steer()
void handBrakeFront()
void handBrakeBack()
}

The MountainBike class implements the Bike interface and must provide implementations for all the methods:

class MountainBike implements Bike {
override void pedal() {
//pedal implementation
}
override void steer() {
//steer implementation
}
override void handBrakeFront() {
//front hand brake implementation
}
override void handBrakeBack() {
//back hand brake implementation
}
void changeGear() {
//change gear code
}
}

So far so good. For the problematic ClassicBike with its foot brake, we get this awkward implementation:

class ClassicBike implements Bike {
override pedal() {
//pedal implementation
}
override steer() {
//steer implementation
}
override handBrakeFront() {
//no code or throw an exception
}
override handBrakeBack() {
//no code or throw an exception
}
void brake() {
//foot brake code
}
}

In this example we have to override both hand brake methods even though we don’t need them. Also as previously stated, we are breaking the Liskov Substitution Principle.

A better approach would be to refactor the interface like this:

interface Bike() {
void pedal()
void steer()
}
interface HandBrakeBike {
void handBrakeFront()
void handBrakeBack()
}
interface FootBrakeBike {
void footBrake()
}

The MountainBike class will implement Bike and HandBrakeBike like this:

class MountainBike implements Bike, HandBrakeBike {
//same code as before
}

The ClassicBike will implement Bike and FootBrakeBike like this:

class ClassicBike implements Bike, FootBrakeBike {
override pedal() {
//pedal implementation
}
override steer() {
//steer implementation
}

override footBrake() {
//code that handles foot braking
}
}

Among the advantages of ISP is that we can mix and match interfaces on various objects which increases the flexibility and modularity of our code. We can also have a MultipleGearsBike interface and add the changeGear() method there. Now, we can build a bike that has a foot brake and gears.

As a bonus, our classes now also adhere to the LSP and both ClassicBike and MountainBike can be treated as a Bike with absolutely no problems. And as mentioned before, adhering to LSP helps adhering to OCP.

If you haven’t already, you can read part 1 here. In part 3 we talk about the last SOLID principle. If you liked this post you can find more on our website.

--

--

Razvan Soare

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