S.
O.
L.
I.
D PrinciplesShubham GuptaBlockedUnblockFollowFollowingJun 13Every wannabe a Developer should know to write more readable, extensible and maintainable programsObject oriented programming has brought new paradigm in software development.
OOP has enabled developers to design classes which combine the data and its functionality in single unit to deal with sole purpose of its existence.
But, this Object-oriented programming doesn’t prevent confusing, inflexible or unmaintainable programs.
Then comes the Uncle Bob aka Robert C.
Martin, with his 5 guidelines/principles.
These 5 principles helps developers to create more readable, flexible and maintainable program.
SOLID Principles is a coding standard that all developers should have a clear concept for developing software in a proper way to avoid a bad design.
SOLID is a mnemonic acronym for five design principles which individually stands for :S : Single Responsibility PrincipleO : Open Close PrincipleL : Liskov Substitution PrincipleI : Interface Segregation PrincipleD : Dependency Inversion PrincipleBefore we start looking at each of the principles individually, let me give you a brief introduction about me and my work.
I work as a Developer on an IIOT platform (Field/Edge to Cloud/DC) enabling simplification and acceleration of Real Time data.
I’ll be explaining SOLID principles using telemetry data formats which is used in IOT fields.
The data format is nothing but a combination of a Key and a Value.
For example :Key : TemperatureValue : 35Combination of both will create a KvEntry (Key-Value Entry).
Based on different data type of Value there are multiple implementation for KvEntry.
For Example, DoubleKvEntry, StringKvEntry, BooleanKvEntry, LongKvEntry.
Those who didn’t get the context of KvEntry data, you guys don’t have to worry, I’ll give more example with each principle to make is easy to understand.
Let’s take each principle one by one :S : Single Responsibility PrincipleAs the name says :“A Class should have one and only one job”A class should be responsible for only one thing.
If a class has more than one responsibility than its a violation of the first principle.
One job/thing doesn’t mean the class should have only one method.
Instead it says all method should relate directly to the responsibility of the class and work towards the same goal.
For example,class KvEntry { constructor(String key , Object value); getKey(); saveKvEntry(KvEntry kvEntry);}Does this KvEntry class violates the Single Responsibility Principle.
If yes, then How?Here we can draw out that KvEntry class have two responsibility : KvEntry properties management and KvEntry database management.
The constructor, getKey methods do property management whereas saveKvEntry manages KvEntry storage on database.
In future it will affects the application adversely.
As in, if application changes the way it manages database management, then the classes that uses KvEntry class need to touched and recompiled to compensate for the changes.
We can see the rigidity in the application.
To make this conform to Single Responsibility Principle, we create another class that will handle the database management for KvEntry.
class KvEntry { constructor(String key , Object value); getKey();}class KvEntryDb { getKvEntry(); saveKvEntry(KvEntry kvEntry);}With these changes our application will become highly cohesive.
Another example, I will just provide the class that violates the Single Responsibility Principle.
Try by yourself and see how it violates the principle and suggest changes.
class Animal { constructor(String name) getAnimalName(); saveAnimal(Animal animal);}O : Open Close PrincipleIn simple words its says :“Software entities should be open for extension, but closed for modification”Software entities are like classes, modules, functions, etc.
In much simpler words, it means that a class should be easily extendable without modifying the existing class or function itself.
Let’s continue with our KvEntry classclass KvEntry { constructor(String key, String value); getKey(); getValue();}We want to iterate through the list of KvEntry and print the data type for its values.
//… Construct a list of KvEntryArrayList<KvEntry> kvEntries = new ArrayList<KvEntry>();kvEntries.
add(new KvEntry("temp",35.
24));kvEntries.
add(new KvEntry("switchStatus",true));void printDataType(List<KvEntry> kvEntries) { foreach(KvEntry kvEntry :kvEntries){ if (kvEntry.
getValue() instanceof Double) { log.
info("Double"); } else if (kvEntry.
getValue() instanceof Boolean) { log.
info("Boolean"); } }}printDataType(kvEntries);As we can see, for every new implementation of KvEntry, we need to add a new logic to printDataType method.
For such a small application it’s pretty easy to handle these conditions.
But as our application grows and become complex, we will see that the if statements are getting repeated again and again in printDataType method each time a new KvEntry is added.
Now, the question is how to conform Open Close Principle?class KvEntry() { getDataType(); //…}class DoubleKvEntry extends KvEntry { getDataType() { return "Double"; }}class BooleanKvEntry extends KvEntry { getDataType() { return "Boolean"; }}class StringKvEntry extends KvEntry { getDataType() { return "String"; }}//…void printDataType(List<KvEntry> kvEntries) { foreach(KvEntry kvEntry : kvEntries) { log.
info(kvEntry.
getDataType()); }}printDataType(kvEntries);KvEntry now has a virtual method getDataType.
We have each KvEntry extend the KvEntry class and implement the virtual getDataType method.
Here each implementation of KvEntry have is own implementation of data type.
Now, if we add a new data type of KvEntry, the printDataType method doesn’t have to change.
All we need to do is to add a new KvEntry in KvEntries arraylist.
Another example to think on, extending our previous Animal class.
class Animal { constructor(String name) getAnimalName();}//… Construct a list of AnimalsArrayList<Animal> animals = new ArrayList<Animal>();animals.
add(new Animal("lion"));animals.
add(new Animal("dog"));void animalSound(List<Animal> animals) { foreach(Animal animal :animals){ if (animal.
getAnimalName().
equals("lion")) { log.
info("roar"); } else if (animal.
getAnimalName().
equals("dog")) { log.
info("bark"); } }}animalSound(animals);L : Liskov Substitution PrincipleIts wikipedia definition says :“Let q(x) be a property provable about objects of x of type T.
Then q(y) should be provable for objects y of type S where S is a subtype of T.
”Pretty complex right, let me put it in simple words :“Every subclass/derived class should be substitutable for their base/parent class.
”The principle says that a sub-class can take the place of its super-class without errors.
In other words, a subclass should override the parent class methods in a way that it doesn’t break functionality from a client’s point of view.
If the code finds itself checking the type of class then, it must have violated this principle.
Let’s take an example :void printValues(List(KvEntry) kvEntries) { foreach(KvEntry kvEntry : kvEntries) { if (kvEntry.
getValue() instanceof Double) { log.
info("Double Value : " +(Double)kvEntry.
getValue()); } else if (kvEntry.
getValue() instanceof Boolean) { log.
info("Boolean Value : " +(Boolean)kvEntry.
getValue); } else if (kvEntry.
getValue() instanceof String) { log.
info("String Value : " + (String)kvEntry.
getValue); } }}printValues(kvEntries);The above implementation of printValues violates the Liskov Substitution Principle ( and also the Open Close Principle).
It must know of every KvEntry type and call the associated getValue function.
With every new implementation of KvEntry, the printValues method must be modified to accept the new KvEntry.
//…class LongKvEntry extends KvEntry {}ArrayList<KvEntry> kvEntries = new ArrayList<KvEntry>();…kvEntries.
add(newLongKvEntry("distance",12345));…void printValues(List(KvEntry) kvEntries) { foreach(KvEntry kvEntry :kvEntries) { if (kvEntry.
getValue() instanceof Double) { log.
info("Double Value : " + (Double) kvEntry.
getValue()); } else if (kvEntry.
getValue() instanceof Boolean) { log.
info("Boolean Value : " + (Boolean) kvEntry.
getValue); } else if (kvEntry.
getValue() instanceof String) { log.
info("String Value : " + (String) kvEntry.
getValue); } else if (kvEntry.
getValue() instanceof Long) { log.
info("Long Value : " + (Long) kvEntry.
getValue); } }}printValues(kvEntries);So to follow the Liskov Substitution Principle there are two rules:If the super-class (KvEntry) has a method that accepts a super-class type (KvEntry) parameter, then is sub-class (LongKvEntry) should accept an argument of super-class type (KvEntry) or sub-class type (LongKvEntry).
If the super-class returns a super-class type (KvEntry).
Its sub-class should return a super-class type (KvEntry type) or sub-class type(LongKvEntry).
Now, let’s reimplement the printValues method :void printValues(List(KvEntry) kvEntries) { foreach(KvEntry kvEntry : kvEntries) { log.
info("Value : " + kvEntry.
getValue()); }}printValues(kvEntries);The printValues method cares less about the type of KvEntry passed, it just calls the getValue method.
All it knows is that the parameter must be of an KvEntry type, either the KvEntry class or its sub-class.
The KvEntry class now have to implement/define a getValue method:class KvEntry { //… getValue();}And its sub-classes have to implement the getValue method://…class LongKvEntry extends KvEntry { //… getValue() { //… }}When it’s passed to the printValues function, it returns the long value it has.
We can see that, the printValues doesn’t need to know the type of KvEntry to return its value, it just calls the getValue method of the KvEntry type because by contract a sub-class of KvEntry class must implement the getValue function.
Here is an another problem for you to try implementing Liskov Substitution Principle ://…class Pigeon extends Animal {}ArrayList<Animal> animals = new ArrayList<Animal>();animals.
add(new Pigeon("pigeon"));void animalLegCount(List<Animal> animals) { foreach(Animal animal : animals) { if (animal instance of Lion) { log.
info(animal.
lionLegCount()); } else if (animal instance of Dog) { log.
info(animal.
dogLegCount()); } else if (animal instanceof Pigeon) { log.
info(animal.
pigeonLegCount()); } }}animalLegCount(animals);I : Interface Segregation PrincipleWhat wikipedia says :“many client-specific interfaces are better than one general-purpose interface.
”In more simple words :“A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.
”This principle mainly deals with the disadvantages of implementing big interfaces.
Let’s take a break from KvEntry example and try an old and typical example of Shape :interface IShape { drawCircle(); drawSquare();}This interface draws circle and square.
class Circle, Square implementing IShape interface must implement both methods drawCircle, drawSquare.
class Circle implements IShape { drawCircle(){ //… } drawSquare(){ //… }}class Square implements IShape { drawCircle(){ //… } drawSquare(){ //… }}Doesn’t the above implementation looks weird and somewhat funny.
class Circle implements method drawSquare it has no use of, likewise class Square implementing drawCircle.
Now, in a new requirement we need to support a new Shape Triangle.
interface IShape { drawCircle(); drawSquare(); drawTriangle();}All classes need to implement the new method otherwise error will be thrown.
We see that it is impossible to implement a shape that can draw a circle but not a square or a triangle.
We can just implement the methods to throw an error that shows the operation cannot be performed.
Interface segregation principle frowns against the design of this IShape interface.
Clients (here Circle, and Square) should not be forced to depend on methods that they do not need or use.
Also, Interface segregation principle states that interfaces should perform only one job (just like the Single Responsibility Principle) any extra grouping of behavior should be abstracted away to another interface.
Here, our IShape interface performs actions that should be handled independently by other interfaces.
To make our IShape interface conform to the Interface Segregation principle, we segregate the actions to different interfaces:interface IShape { draw();}interface ICircle { drawCircle();}interface ISquare { drawSquare();}interface ITriangle { drawTriangle();}class Circle implements ICircle { drawCircle() { //… }}class Square implements ISquare { drawSquare() { //… }}class Triangle implements ITriangle { drawTriangle() { //… }}class CustomShape implements IShape { draw(){ //… }}The ICircle interface handles only the drawing of circles, IShape handles drawing of any shape, ISquare handles the drawing of only squares and ITriangle handles the drawing of only triangles.
ORClasses (Circle, Square, Triangle, etc) can just inherit from the IShape interface and implement their own draw behavior.
class Circle implements IShape { draw(){ //… }}class Triangle implements IShape { draw(){ //… }}class Square implements IShape { draw(){ //… }}We can then use the I-interfaces to create Shape specifics like Semi Circle, Right-Angled Triangle, Equilateral Triangle, Blunt-Edged Rectangle, etc.
D : Dependency Inversion PrincipleBy Definition it says :“Entities must depend on abstractions, not on concretions.
It states that the high level module must not depend on the low level module, but they should depend on abstractions.
”There comes a point in software development where our app will be largely composed of modules.
When this happens, we have to clear things up by using dependency injection.
High-level components depending on low-level components to function.
By applying the Dependency Inversion the modules can be easily changed by other modules just changing the dependency module and High-level module will not be affected by any changes to the Low-level module.
Let’s take a look at an example :class MySqlConnection { connect() { log.
info("MySql DB Connection "); }}class QueryExecutor { private MySqlConnection dbConnection; constructor(MySqlConnection dbConnection) { this.
dbConnection = dbConnection; }}There’s a common misunderstanding that dependency inversion is simply another way to say dependency injection.
However, the two are not the same.
In the above code in spite of Injecting MySQLConnection class in QueryExecutor class but it depends on MySQLConnection.
High-level module QueryExecutor should not depend on low-level module MySQLConnection.
If we want to change the connection from MySQLConnection to PostgresDBConnection, we have to change hard-coded constructor injection in QueryExecutor class.
QueryExecutor class should depend upon on Abstractions not on concretions.
But How can we do it ?interface Connection { connect();}class MySqlConnection implements Connection{ connect() { log.
info("MySql DB Connection "); }}class PostgresDBConnection implements Connection{ connect() { log.
info("Postgres DB Connection "); }}class QueryExecutor { private Connection dbConnection; constructor(Connection dbConnection) { this.
dbConnection = dbConnection; }}In the above code, we want to change the connection from MySQLConnection to PostgresDBConnection, we don’t need to change constructor injection in QueryExecutor class.
Because here QueryExecutor class depends upon on Abstractions, not on concretions.
ConclusionSo, we have covered all the 5 SOLID principles.
In start it will look like some rocket science, but trust me with steady practice and a little patience, these principles will be a part your programs.
.