Let’s learn SOLID principles with one big example. This is the most common interview question as well.
First, we need to break down the abbreviation.
S: Single Responsibility Principle
O: Open-Closed Principle
L: Liskov’s Substitution Principle
I: Interface Segregation Principle
D: Dependency Inversion Principle
Pretty cool, huh? Let’s explore each of them.
Single Responsibility Principle
A class must have a specific responsibility and nothing else. It should change for only one reason.
So the responsibility is actually encapsulated within the class; our class HAS-A Responsibility.
As you can see in this UML: class IPhone has properties that can define an IPhone. But hold on -- what is CalculateTotal_Sale() doing here?
Do you see the problem? It is not the responsibility of an IPhone class to calculate Total sale
Problem
What if tomorrow I have IPad class, I’ll have to re-code the same method for that class as well.
Solution
Rather we can move the responsibility of calculating Total sale to new class Sale. And IPhone can encapsulate Sale to calculate the total sale.
Here, now Sale is managing the responsibility of calculating TotalSale(). And our IPhone class simply reuse this responsibility rather than creating one by itself. Hence IPhone has the TotalSale().
Open-Closed principle
Open for Extension, Closed for Modification. Software entities (objects, modules, functions) should be open for extension but closed for modification. This simply means that an entity should be easily extendable without modifying them-self.
Problem
We know that different IPhone models have different specs, right? IPhone XR has different specifications than IPhone X.
Solution
We must abstract our problem so that our IPhone class can delegate its an implementation based on model type.
Now IPhone can ask for specific Model and set specifications based on the model it gets.
Here class IPhone is closed for modification but it is open for extension. Note that class IPhone now has an IModel encapsulated. So ModelName property is not the responsibility of IPhone class anymore. Yet again we are using our first design principle(Single responsibility). And our abstraction is generic enough to satisfy any model apple ever creates.
Our client, an IPhone, doesn't have to change just because Apple decides to have a new Model (closed for modification). And apple can now have “n” number of models (open for extension).
Liskov’s Substitution Principle
Parent classes should be easily substituted with their child classes.
In our UML above as we can see that, we have created IModel interface, now any class which inherits the IModel must define all of its methods in order to satisfy IS-A relationship else you’ll get a compile-time error. But your compiler is not going to be there when you inherit classes rather than interfaces.
IPhone HAS-A CPU class, IPhone could have Bionic chips but none of the IPhone is compatible with Snapdragon.
Problem
Have you ever seen IPhone with Snapdragon? Hell no!!! Right? So when we try to assemble Snapdragon’s CPU to our IPhone class it will throw an error as you can see in Snapdragon865 class it is throwing a not implemented exception. Because IPhone X & XR are only compatible with Bionic chips.
Solution
We should not have child classes which are not completely implementing their parent’s behaviour.
Instead of Snapdragon, Apple has A8, used in IPhone 6. Hence it is compatible with an Iphone.
Interface Segregation Principle
Remember the Single responsibility principle? It is the same principle but this principle is applied on interfaces instead.
Problem
We should not have a more generalized interface; i.e. we must avoid jumbling up everything in one single interface then forcing their concrete classes to define those methods which they might not require & they end up in throwing a NotImplementedException.
Instead, we must have many client-specific interfaces rather than one general interface.
We have 2 IPhones XR & X: Iphone XR has a single camera where IPhone X has a dual camera. So it would be wrong for an IPhone XR to define DualCamera, as it does not have a dual camera. Here IPhone XR is only defining Single camera, DualCamera is throwing an exception.
Solution
As discussed, we should not have a generalized interface. We can have top-level abstraction but implementation has to be broken down into small pieces as per their requirements.
Here class IPhone can set up camera-based on their model.
class IPhone has a camera which could be Single-Dual camera based on Model type. for XR it is a single-camera setup & for X it is the dual-camera setup.
Dependency Inversion Principle
Our application must be loosely coupled. Classes should depend on abstraction but not on concretion because tightly coupled applications get more tangled with each other as application grows.
Problem
We have tight coupling between IOSUpdate & IPhone. So if tomorrow Apple needs an IOS update for IPad, then we will have to make one more tight coupling relationship, And it gets difficult to maintain as the application grows more.
IOSUpdate is tightly coupled with IPhone.
Solution
An abstraction. Rather than directly communicating with the concrete class, there should be a loose coupled class.
IUpdate
Abstraction. IPhoneUpdate & IPadUpdate are concrete classes of an abstraction.
As you can see the above IUpdate is an interface which has 2 concrete classes, IPhoneUpdate & IPadUpdate. And when our client, which is iPhone, in this case, asks for an update we can simply inject IPhoneUpdate dependency in constructor or method.
For an IPad, It is injecting IPadUpdate dependency.
So in the future, when Apple needs to have a new product, say a MacBook, we can simply create one more MacBookUpdate class which will be a concrete representation of IUpdate interface. And client MacBook can resolve <IUpdate, MacBookUpdate> dependency.
Dependency inversion is a wide topic to learn in and of itself.
I have covered that in my other blog, feel free to visit if you wish to understand its workings,
Happy coding.
Connect with me,