SOLID principles in a nutshell
In this article we will take up the five SOLID principles, we will start with an explanation of each principle, by giving a real-world example to get as closer as possible to the topic, then finish with an implementation of the use case in eclipse with java code. SOLID principles help us to make our software more readable, understandable, flexible and maintainable.
SOLID is just an acronym of the five principles, every letter stands for a specific principle.
S: Single responsibility principle.
O: Open/closed principle.
L: Liskov substitution principle.
I: Interface segregation principle.
D: dependency inversion principle.
S- Single responsibility principle (SRP):
“Every software component (class, method, module) should have one and only one responsibility.”
Swiss army knife Analogy:
Cohesion :
Cohesion (degree of relation) is the the degree to which the various parts of a software component are related.
Example:
In the example below, the methods of square class are not coherent, the first two methods are in charge of calculation, meanwhile the third and the fourth methods deal with the graphic rendering of the square. So Square class is violating the SRP.
Solution:
We split the first class into two classes, the first class if for the measurement of squares, the other one is for rendering images of squares. Higher cohesion helps attain better adherence to the single responsibility principle.
Coupling:
Coupling is defined as the level of inter dependency between various software components.
As shown in the picture, the save method will convert the student class into a serialized form and persist it into a database. Let’s say in the future we want to use another database, most of this code need to change, this means that the student class is tightly coupled with the database layer, and tight coupling violates SRP.
Solution:
To fix this, we will take the database related code, and move it to a new repository class, then we will refer to this repository class from inside Student class. By doing so, we have removed the tight coupling and made it loose. Loose coupling helps attain better adherence to the single responsibility principle.
Reasons to change:
Every software component should have one and only one reason to change.
Example:
Ine the last example, we had 3 reasons to change:
1- A change in the student id format.
2- A change in the student name format.
3- A change in the database, as advised by the technical team.
O-Open/closed principle:
“Software component should be closed for modification, but open for extension. This means that new software getting added to the software component, should not have to modify existing code.”
Real-world analogy:
The Nintendo WII is a very popular gaming console, it’s basically composed from a console (which contain the CPU) and a remote controller.
WII also manufactures a number of accessories that can go with the device. For instance, WII zapper a good accessory for playing FPS (first person shooter games), there’s also the steering wheel that needs the same setting.
To set this up, you just place the basic remote into a cradle inside the zapper and you are all set.
In order to add the the WII zapper or the steering wheel, we didn’t touch the CPU or the basic controller, so our system is closed for modification and open for extension.
Coding example:
An insurance company deals primarily with health insurance. The following picture shows how the premium discounts are calculated.
We have an InsurancePremiumDiscountCalculator class that has a calculatePremiumDiscountPercent.
Come tomorrow, this insurance company acquires another insurance company which is primarly into vehicule insurance. To do this, we have to change the parameter of calculatePremiumDiscountPercent into the object of the new class of vehicule, and this violate the OCP.
Solution:
We use abstractions and especially interfaces to avoid violating the OCP.
Key takeaways:
· Ease of adding new features.
· Leads to minimal cost of developing and testing software.
· Open closed principle often requires decoupling, which, in turn, automatically follows the single responsibility principle.
L-Liskov substitution principle:
“Objects should be replaceable with their subtypes without affecting the correctness of the program.”
As we know, inheritance is also known as Is-A relationship, here’s some examples:
Among this 3 examples, the one in the middle has a hidden problem.
Ostrich is a bird, but an Ostrich cannot fly.
The statement “Ostrich Is-A bird” might still be correct. But if we apply the Liskov substitution principle here, which says: objects should be replaceable with their subtypes without affecting the correctness of the program, this test fails, because we cannot use the Ostrich object in all the places where we use the bird object, if we do so and someone call the fly() method on the bird object, our program will fail.
Change the Is-A way of thinking:
“If it looks like a duck and quacks like a duck but need batteries, you probably have the wrong abstraction!”
Example:
Let’s say we have a generic Car class, and we have a racing car. Racing car extends car.
The Car class has one method getCabinWidth() that return the cabin width of the car.
The racing car class overrides getCabinWidth() funcyion and leaves it unimplemented, because racing cars have a set of specifications some of which might not match that of a generic car.
In generic car we call the width as cabin width, but in a racing car, there’s no cabin.
Let’s create a car called CarUtils which instantiates 3 objects with reference type Car, 2 of them are generic Car instances and the third is a racing car instance.
We insert all 3 car references in an arraylist and name it my Cars. Next we iterate through the car list and we print out the cabinwidthof each car. The first 2 iterations will work, the third won’t (because of unimplemented method).
Solution:
We create a generic class called vehicule, which can represent any mode of transportation, then we make both car and racing car extend it.
I-Interface segregation principle:
“No client should be forced to depend on methods it does not use.”
Real-world analogy:
Xerox WorkCentre is a multi-function all-in-one, that has printer, sacanner, copier, and fax all built into one.
This is a bad design for HP PrinterScanner and Canon Printer, because they contain methods that they don’t use.
Solution:
We split the IMultiFunction into 3 coherent interfaces, and every time we want to add a device we will only implement the interfaces needed.
So now, we get rid of methods that we don’t use because we’re not forced to use them.
Techniques to identify ISP:
· Fat interfaces.
· Interfaces with low cohesion.
· Empty method implementations.
D-Dependency inversion principle:
“High-level modules should not depend on low level modules. Both should depend on abstractions.”
“Abstractions should not depend on details, details should depend on abstractions.”
Real-wordl analofy:
An e-commerce web application.
These are some examples that violates DIP. Let’s take the first example and analyze it further.
We have a SQLProductRepository class which contains a method called getAllProductNames(), assume this method contains the code to run a SELECT statement from a product database table in the backend, that returns a list of product name strings.
We have also the ProductCatalog class that instantiates a SQLProductRepository object and call the function of that class.
So the ProductCatalog directly depends on SQLProductRepository class, and this is the violation of the principle as seen in code.
Solution:
To fix this we will create an interface and call it ProductRepository. We’ll make the SQLProductRepository implement this interface, and to get the SQLProductRepository object to the ProductCatalog we’ll use a Factory class, as shown in the figure below:
So now, the detail, which is the SQLProductRepository class, is dependent on the abstraction, which is the ProductRepository interface.