S.O.L.I.D. The first 5 principles of Object Oriented Design

S.O.L.I.D. The first 5 principles of Object Oriented Design

SOLID is the most important five principles of OOP design which is found by Uncle Bob (Robert C. Martin). Michael Feathers introduced the acronym. When you use these principles, the code becomes a clean code. The clean code provides an overall control of the software project. It enhances low coupling, high cohesion and strong encapsulation which are definitely sought by good designers.

SOLID Initials

S – Single Responsibility
O – Open-Closed Relationship
L – Liskov Substitution Rule
I – Interface Segregation Principle
D – Dependency Inversion Rule

SOLID Details

Single Responsibility

This is the simplest rule. There should be never more than one reason for a class to change. Each class declared inside a software project should have a single responsibility. The class should do only one work.

Think about the below class:

public class Member {
    public static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);
    private String name;
    private String EMail;
    //getters and setters..
    
    public boolean validateEmail() {
        Matcher matcher = EMAIL_PATTERN.matcher(email);
        return matcher.find();
    }
}

A developer can change this class for more than one reason: To update something about Member fields or to update the email validation rule. According to the first rule of SOLID, We have to divide it into two modules like:

public class EmailValidatorService {
    public static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);

    public boolean validateEmail(String email) {
        Matcher matcher = EMAIL_PATTERN.matcher(email);
        return matcher.find();
    }
}
public class Member {
    private String name;
    private String EMail;
    //getters and setters..
}

In summary, we can say that Member class keeps the information of members. EmailValidatorService validates the given email String. They both have single responsibilities now.

Open-Closed

A module should be open for extension but closed for modifications. Let’s check-out the below code.

public class Shape {
    private int width;
    private int height;
    //getters and setters
}
public class AreaCalculator {
    public List<Integer> calculateAreas(List<Shape> shapes) {
        List<Integer> calculatedAreas = new ArrayList<>();
        for(Shape shape: shapes) {
            calculatedAreas.add(shape.getWidth() * shape.getHeight());
        }
        return calculatedAreas;
    }
}

Think that we have to add a new shape “cube” and it has three dimensions.

public class Shape {
    private int width;
    private int height;
    //getters and setters
}
public class Cube extends Shape {
    private int depth;
    //getters and setters
}
public class AreaCalculator {
    public List<Integer> calculateAreas(List<Shape> shapes) {
        List<Integer> calculatedAreas = new ArrayList<>();
        for(Shape shape: shapes) {
            if(shape instanceOf Cube) {
                calculatedAreas.add(shape.getWidth() * shape.getHeight() * ((Cube)shape).getDepth());
            } else {
                calculatedAreas.add(shape.getWidth() * shape.getHeight());
            }
        }
        return calculatedAreas;
    }
}

As a matter of fact, the code really seems bad now. There is a check with “instanceOf”. This is not suitable according to “keeping the coupling loose“. The module uses AreaCalculator knows about the semantic. This code is not open for extensions but modifications. Below code demonstrates a code suitable for the Open-Closed rule.

public class Shape {
    private int width;
    private int height;
    public int calculateArea() {
        return this.width * this.height;
    }
    //getters and setters
}
public class Cube extends Shape {
    private int depth;
    @Override
    public int calculateArea() {
        return this.width * this.height * this.depth;
    }
    //getters and setters
}
public class AreaCalculator {
    public List<Integer> calculateAreas(List<Shape> shapes) {
        List<Integer> calculatedAreas = new ArrayList<>();
        for(Shape shape: shapes) {
            calculatedAreas.add(shape.calculateArea());
        }
        return calculatedAreas;
    }
}

Now it is safe to extend this code for further shapes

Liskov Substitution Rule

“If it looks like a duck, quacks like a duck but needs batteries, you probably have the wrong abstraction”. In OOP development, inheritance got an “is-a” relation that, if an object “is-a” BaseObject then Object is inherited from the BaseObject.

The Liskov Substitution Principle
The Liskov Substitution Principle

Let’s say we inherited class Square from the class Rectangle.

public class Rectangle {
    private Integer width;
    private Integer height;
    public void setWidth(Integer width) {
        this.width = width;
    }
    public void setHeight(Integer height) {
        this.height = height;
    }
}
public class Square extends Rectangle {
    @Override
    public void setWidth(Integer width) {
        this.width = width;
        this.height = width;
    }
    @Override 
    public void setHeight(Integer height) {
        this.width = height;
        this.height = height;
    }

The two methods are doing the same thing now. If you change one then you have to change the other. There must be a single method that only sets one edge for the shape ‘square’.

Interface Segregation

This principle (ISP) says that no client should be forced to depend on methods that it does not use. Sample:

public interface Ship {
    String getName();
    void sail();
    void takeOff();
}
public class F4 implements Ship {
    @Override public String getName() { 
        return "F4"; 
    }
    @Override
    public void takeOff() {
        System.out.println("Taking off..");
    }
    @Override
    public void sail() {
        //Sail? I do not sail..
    }
}
public class Catamaran implements Ship {
    @Override public String getName() { 
        return "Catamaran"; 
    }
    @Override
    public void takeOff() {
        //Take off? I do not fly..
    }
    @Override
    public void sail() {
        System.out.println("Sailing..");
    }
}

Most of the times we must accept that we are lazy 🙂 In the sample above, the code is a lazy developer’s code. She/he doesn’t want to create separate interfaces for airships and sea ships. Creates one for all! Let’s fix this according to the Interface Segregation rule.

public interface Ship {
    String getName();
}
public interface AirShip extends Ship {
    void takeOff();
}
public interface SeaShip extends Ship {
    void sail();
}
public class F4 implementsAirShip {
    @Override 
    public String getName() { 
        return "F4"; 
    }
    @Override
    public void takeOff() {
        System.out.println("Taking off..");
    }
}
public class Catamaran implements SeaShip {
    @Override 
    public String getName() { 
        return "Catamaran";
    }
    @Override
    public void sail() {
        System.out.println("Sailing..");
    }
}

In the beginning, setting the proper structure seems like a very big issue, but the time spent here will gain maybe five or ten times in the future while refactoring for the changes.

Dependency Inversion Rule

When a high-level module uses a low-level module, it should not depend on the low-level module but the abstraction of it. Abstractions should not depend on details.  Details should depend upon abstractions. You can watch the video at the end of this post that Uncle Bob explains deeply about the SOLID principles and the relations of the modules.

Here is a sample.

public class Notification {
    private SMS sms;
    private EMail eMail;
    
    public Notification() {
        this.sms = new SMS();
        this.eMail = new EMail();
    }
    public void send() {
        this.sms.send();
        this.eMail.post();
    }
}

As we can read from this post about Keeping Coupling Loose, Notification class here is simple-object coupled to both SMS and EMail classes. To decrease the coupling level here, we must make the class Notification depending on an abstraction. To do this we will simply create an interface.

public interface Message {
    void sendMessage();
}
public class SMS implements Message {
    //class details..
    @Override
    public void sendMessage() {
        this.send();
    }
}
public class EMail implements Message {
    //class details..
    public void sendMessage() {
        this.post();
    }
}
public class Notification {
    private List<Message> messages;
    
    public Notification(List<Message> messages) {
        this.messages = messages;
    }
    public void send() {
        this.messages.forEach(Message::sendMessage);//--> Applying abstraction increases readability
    }
}

After applying an abstraction, both SMS, EMail and Notification classes now depend on the interface Message. This decreases coupling and increases the overall management. Below code is suitable for Dependency Inversion rule.

In conclusion, SOLID helps you to manage the completely invisible structure. You can read the book Clean Code (the writer is Robert C. Martin) to gain more power in increasing the overall control of your codes and to increase the flexibility that helps you to change any part easily and effectively. Please watch the video below to understand clearly SOLID principles explained by Uncle Bob.

Related Post