Introduction to SOLID Design Principles for Java Developers
Check out this introduction to SOLID design principles, for Java developers.
Join the DZone community and get the full member experience.
Join For FreeS.O.L.I.D. principles and design patterns are not easy to understand or get used to it when you are a newcomer to software engineering. We all had problems and it was hard to grasp the ideas of SOLID+DP, and even more difficult to implement them correctly. Indeed, the whole concept of "why SOLID?" and how to implement design patterns it’s something that requires time and a lot of practice.
You may also like: The SOLID Principles in Real Life
One thing I can honestly say about SOLID design patterns, along with some other fields like TDD, is that by nature, they are very hard to teach. It’s very difficult to teach and transfer all this knowledge and information to young minds in the correct way.
SOLID Made Easy
In this post, I will aim to teach every letter of SOLID in as simplest terms as possible, with straightforward 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 that you should try and separate the concerns into different classes. A class should be focusing on a single problem, piece of logic, or a single domain. When the domain, specification, or logic changes, it should only affect one class.
Before Implementing SRP
Below, we have a violation of SRP. The class VehicleServiceResource
has implemented two different things and ended up with 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.
The 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 the VehicleService
class.
@EndPoint("vehicles")
public class VehicleResource {
@Service
private VehicleService service;
@GET
public List getVehicles() {
return this.service.getVehicles();
}
...
}
We created a new class with a name VehicleService
. This class implements all the vehicle-related logic.
@Service
public class VehicleService {
...
public List getVehciles() {}
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."
The term “Open for extension” means that we can expand and include 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):
In order for this post to serve as an introduction to SOLID, and not get confusing, I will try to keep LSP as simple as possible and exclude a lot of the gritty details since LSP is a whole other discussion and debate for another day.
LSP states that the 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 going to check out the 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);
}
}
When talking about LSP, we have the method setDimensions
in the Rectangle class that 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 through the 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.”
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 ended 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. We have 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 {
}
}
Here's another 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();
}
}
And that's it! There you have it! We hope you enjoyed this simple overview of SOLID design principles in Java code. Please share your thoughts and feedback in the comments section.
Further Reading
Published at DZone with permission of Avraam Piperidis, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments