SOLID - Liskov Substitution, Interface Segregation, and Dependency Inversion
Publish date: 2024-06-27
Tags:
<a href="https://programmercave.com/tags/System-Design/">System-Design</a>, <a href="https://programmercave.com/tags/Software-Engineering/">Software-Engineering</a>
Liskov Substitution Principle
- states that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program
- Let us take a look at our final version of the
Birdclass from SOLID - Single Responsibility, Open/Closed Principle - We started with a
Birdclass which had SRP and OCP violations. We now have aBirdabstract class which can be extended by theEagle,PenguinandParrotsubclasses.

- All the subclasses of
Birdhave to implement this method. A penguin cannot fly, yet we have added afly()method to thePenguinclass. - In the above methods, we are trying to force a contract on a class which does not follow it.
List<Bird> birds = List.of(new Eagle(), new Penguin(), new Parrot());
for (Bird bird : birds) {
bird.fly();
}
- This is a violation of the Liskov Substitution Principle.
- if we have a
Birdobject, we should be able to replace it with an instance of its subclasses without altering the correctness of the program. - In our case, we cannot replace a
Birdobject with aPenguinobject because thePenguinobject requires special handling.
Fixing Liskov Substitution violation
Creating new abstract classes
- A way to solve the issue with the
Penguinclass is to create a new set of abstract classes,FlyableBirdandNonFlyableBird.

- This is an example of multi-level inheritance.
- The issue with the above approach is that we are tying behaviour to the class hierarchy. If we want to add a new type of behaviour, we will have to add a new abstract class.
- For instance if we can have birds that can swim and birds that cannot swim, we will have to create a new abstract class
SwimableBirdandNonSwimableBirdand add them to the class hierarchy. - But now how do you extends from two abstract classes? You can’t. Then we would have to create classes with composite behaviours such as
SwimableFlyableBirdandSwimableNonFlyableBird.

- This is why we should not tie behaviour to the class hierarchy.
Creating new interfaces
- We can create an
Flyableinterface and anSwimmableinterface. - The
Penguinclass will implement theSwimmableinterface and theEagleandParrotclasses will implement theFlyableinterface.

- To identify violations, we can check if we can replace a class with its subclasses having to handle special cases and expect the same behaviour.
- Prefer using interfaces over abstract classes to implement behaviour since abstract classes tend to tie behaviour to the class hierarchy.
Interface Segregation Principle
- states that many client-specific interfaces are better than one general-purpose interface.
- Clients should not be forced to implement a function they do no need.
- Declaring methods in an interface that the client doesn’t need pollutes the interface and leads to a “bulky” or “fat” interface
- A fat interface is an interface that has too many methods.
- If we have a fat interface, we will have to implement all the methods in the interface even if we don’t use them.
- Let us take the example of our
Birdclass. To not tie the behaviour to the class hierarchy, we created an interfaceFlyableand implemented it in theEagleandParrotclasses.
public interface Flyable {
void fly();
void makeSound();
}
- Along with the
fly()method, we also have themakeSound()method in theFlyableinterface. This is because theEagleandParrotclasses both make sounds when they fly. - But what if we have a class that implements the
Flyableinterface? The class does not make a sound when it flies. - This is a violation of the interface segregation principle. We should not have the
makeSound()method in theFlyableinterface.
Dependency Inversion Principle
- refers to the decoupling of software modules. This way, instead of high-level modules depending on low-level modules, both will depend on abstractions.
- High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features.
- To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other.
- Our
Birdclass looks pretty neat now. We have separated the behaviour into different lean interfaces which are implemented by the classes that need them. When we add new sub-classes we identify an issue. For birds that have the same behaviour, we have to implement the same behaviour multiple times.
public class Eagle implements Flyable {
@Override
public void fly() {
System.out.println("Eagle is gliding");
}
}
public class Sparrow implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is gliding");
}
}
- The above can be solved by adding a default method to the
Flyableinterface. This way, we can avoid code duplication. But which method should be the default implementation? What if in future we add more birds that have the same behaviour? We will have to change the default implementation or either duplicate the code. - Instead of default implementations, let us abstract the common behaviours to a separate helper classes. We will create a
GlidingBehaviourclass and aFlappingBehaviourclass. TheEagleandSparrowclasses will implement theFlyableinterface and use theGlidingBehaviourclass. TheParrotclass will implement theFlyableinterface and use theFlappingBehaviourclass.
public class Eagle implements Flyable {
private GlidingBehaviour glidingBehaviour;
public Eagle() {
this.glidingBehaviour = new GlidingBehaviour();
}
@Override
public void fly() {
glidingBehaviour.fly();
}
}
- Now we have a problem. The
Eagleclass is tightly coupled to theGlidingBehaviourclass. If we want to change the behaviour of theEagleclass, we will have to open the Eagle class to change the behaviour. This is a violation of the dependency inversion principle. We should not depend on concrete classes. We should depend on abstractions. - Naturally, we rely on interfaces as the abstraction. We create a new interface
FlyingBehaviourand implement it in theGlidingBehaviourandFlappingBehaviourclasses. TheEagleclass will now depend on theFlyingBehaviourinterface.
interface FlyingBehaviour{
void fly()
}
class GlidingBehaviour implements FlyingBehaviour{
@Override
public void fly() {
System.out.println("Eagle is gliding");
}
}
...
class Eagle implements Flyable {
private FlyingBehaviour flyingBehaviour;
public Eagle() {
this.flyingBehaviour = new GlidingBehaviour();
}
@Override
public void fly() {
flyingBehaviour.fly();
}
}
These are my notes from the Low-Level Design (LLD) course I took at Scaler.
Check Python, Java and Go code on Github Repo
Tags:
<a href="https://programmercave.com/tags/System-Design/">System-Design</a>, <a href="https://programmercave.com/tags/Software-Engineering/">Software-Engineering</a>