S.O.L.I.D. principles and design patterns are not easy to understand and get used to it when you are a newcomer to software engineering. We all had problems and was hard to grasp the ideas of SOLID+DP and even more difficult to implement them correctly. Indeed the whole concept of the why(SOLID) and how(design patters) it’s something that you become professional step by step, requires time and a lot of practice.
One thing I can honestly say about SOLID, design patterns along with some other fields like TDD is that by nature are very hard to teach. It’s very difficult for the education system to teach and transfer all this knowledge and information to young minds the right way. SOLID and design patterns are hard to teach and make sense.
It’s very difficult for the education system to teach and transfer all this knowledge and information to young minds the right way
Simple SOLID
I will try to teach as simple as possible every letter of SOLID with straight forward and easy to grasp examples.
The S of SOLID
The S stands for SRP (Single Responsibility Principle)
The basic idea is to apply a separation of concerns. which means, you should try and separate the concerns into different classes. A class should be focusing on a single problem, logic, or a single domain. When the domain, specification or logic changes it should affect only one class.
Before implementing SRP
Below we have a violation of SRP. The class VehicleServiceResource has implemented two different things and ended up having two roles. As we can see, the class has two annotations marking its usage.
One is the role of exposing and serving HTTP endpoint vehicles to clients.
Second is the role of a vehicle service, which is fetching the vehicles from storage getVehicles()
and calculates the total value calculateTotalValue()
@EndPoint("vehicles") @Service public class VehicleServiceResource { … @GET public List getVehicles(){ } public double calculateTotalValue(){} … }
The simple goal to achieve SRP is to separate the VehicleServiceResource into two different classes. One for the endpoint and the other for the service.
After the implementation of SRP
What we did was to take the VehicleServiceResource class and split it into two different classes.
VehicleResource
class has one and one job only. To expose and serve HTTP resource vehicles to clients. All business logic related methods lead to VehicleService class.
@EndPoint("vehicles") public class VehicleResource { @Service private VehicleService service; @GET public ListgetVehicles() { return this.service.getVehicles(); } ... }
We created a new class with a nameVehicleService
. This class implements all the vehicle-related logic.
@Service public class VehicleService { ... public ListgetVehciles() {} public double calculateTotalValue(){} ... }
The O of SOLID
The O stands for OCP (Open-Closed Principle)
The Open-Closed Principle states that “software entities such as modules, classes, functions, etc. should be open for extension, but closed for modification“.
A simple definition.
The term “Open for extension” means that we can expand, include some extra cases/functionalities in our code without altering or affecting our existing implementation.
The term “Closed for modification” means that after we add the extra functionality, we should not modify the existing implementation.
A simple violation of the OCP
public class VehicleValueCalculator { // lets assume a simple method to calculate the total value of a vehicle // with extra cost depending the type. public double calculateVehicle(Vehicle v){ double value = 0; if(v instanceof Car){ value = v.getValue() + 2.0; } else if(v instanceof MotorBike) { value = v.getValue() + 0.4; } return value; } }
The OCP violation raises when we want to include a new type of vehicle a Truck. Refactoring and code modification on calculateVehicle method is needed.
Solution
public interface IVehicle { double calculateVehicle(); }
public class Car implements IVehicle { @Override public double calculateVehicle() { return this.getValue() + 2.0; } } public class MotorBike implements IVehicle { @Override public double calculateVehicle() { return this.getValue() + 0.4; } }
Our new Truck Vehicle
public class Truck implements IVehicle { @Override public double calculateVehicle() { return this.getValue() + 3.4; } }
This way by having a method that accepts an IVehicle, there is no need for refactoring/code modification in the future every time we add a new type of vehicle.
Example code
public class Main { public static void main(String[] args){ IVehicle car = new Car(); IVhecile motorBike = new MotorBike(); //new addition IVhecile truck = new Truck(); double carValue = getVehicleValue(car); double motorBikeValue = getVehicleValue(motorBike); double truckValue = getVehicleValue(truck); } public double getVehicleValue(IVehicle v) { return v.calculateVehicle(); } }
The L of SOLID
The L stands for LSP (Liskov Substitution Principle)
For the shake of this article being an understandable introduction to SOLID and not get confusing, I will try to keep LSP as simple as possible and exclude a lot of things, since LSP is a whole concept for discussion and debate.
The LSP states that Software should not alter the desirable results when we replace a parent type with any of the subtypes.
LSP is more of a problem definition than being a design pattern and what we can do to prevent undesirable effects.
To make this more clear we are gonna see a simple example below.
/** * The Base Rectangle class * This class defines the structure and properties of all types of rectangles */ public class Rectangle { private int width; private int height; public Rectangle(){} public Rectangle(int w,int h) { this.width = w; this.height = h; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getArea() { return this.height * this.width; } /** * LSP violation is case of a Square reference. */ public final static void setDimensions(Rectangle r,int w,int h) { r.setWidth(w); r.setHeight(h); //assert r.getArea() == w * h } }
/** * A Special kind of Rectangle */ public class Square extends Rectangle { @Override public void setHeight(int h){ super.setHeight(h); super.setWidth(h); } @Override public void setWidth(int w) { super.setWidth(w); super.setHeight(w); } }
The point to understand the LSP is that we have the method setDimensions
in Rectangle class which accepts a type of Rectangle object and sets the width and height. This is a violation because the behavior changed and we have inconsistent data when we pass a square reference.
There are many solutions. Some of them are to apply the Open Closed Principle and a Design by Contract pattern.
There are many other solutions to the LSP violations as well, but I am not going to explain it here because it’s out of the scope of this article.
The I of SOLID
The I stands for ISP (Interface Segregation Principle)
The Interface Segregation Principle was defined by Robert C. Martin while consulting for Xerox. He defined it as:
”Clients should not be forced to depend upon interfaces that they do not use.”
The ISP states that we should split our interfaces into smaller and more specific ones.
Below is an example of an interface representing two different roles. One is the role of handling connections like opening and closing and the other is sending and receiving data.
public interface Connection { void open(); void close(); byte[] receive(); void send(byte[] data); }
After we applied ISP we end up with two different interfaces with each one representing one exact role.
public interface Channel { byte[] receive(); void send(byte[] data); }
public interface Connection { void open(); void close(); }
The D of SOLID
The D stands for DIP (Dependency inversion principle)
The DIP states that we should depend on abstractions (interfaces and abstract classes) instead of concrete implementations (classes)
Next is a violation of DIP. An Emailer class depending on a direct SpellChecker class.
public class Emailer{ private SpellChecker spellChecker; public Emailer(SpellChecker sc) { this.spellChecker = sc; } public void checkEmail() { this.spellChecker.check(); } }
And the Spellchecker class
public class SpellChecker { public void check() throws SpellFormatException { } }
It may work at the moment but after a while, we have two different implementations of spellcheckers we want to include. We have the default spell checker and a new greek spell checker.
With the current implementation, refactoring is needed because the Emailer class uses only the SpellChecker class.
A simple solution is to create the interface for the different spell checkers to implement.
// The interface to be implemented by any new spell checker. public interface ISpellChecker { void check() throws SpellFormatException; }
Now the Emailer class accepts only an ISpellChecker reference on the constructor. Below we changed the Emailer class to not care/depend on the implementation (concrete class) but rely on the interface (ISpellChecker)
public class Emailer{ private ISpellChecker spellChecker; public Emailer(ISpellChecker sc) { this.spellChecker = sc; } public void checkEmail() { this.spellChecker.check(); } }
And we have many implementations for the ISpellChecker
public class SpellChecker implements ISpellChecker { @Override public void check() throws SpellFormatException { } } public class GreekSpellChecker implements ISpellChecker { @Override public void check() throws SpellFormatException { } }
An example by code. We are passing the ISpellChecker type to the Emailer constructor no matter what the implementation is.
public static class Main{ public static void main(String[] a) { ISpellChecker defaultChecker = new SpellChecker(); ISpellChecker greekChecker = new GreekSpellChecker(); new Emailer(defaultChecker).checkEmail(); new Emailer(greekChecker).checkEmail(); } }