top of page

Software Engineering Fundamentals - S.O.L.I.D.

Updated: Apr 13, 2023

SOLID Principles

SOLID is an acronym that stands for five principles of object-oriented software design, which were introduced by Robert C. Martin in the early 2000s.

The SOLID principles are:

  • Single Responsibility Principle (SRP): A class should have one and only one reason to change, meaning that a class should have only one responsibility.

  • Open-Closed Principle (OCP): A class should be open for extension but closed for modification, meaning that the behavior of a class can be extended without modifying the source code of the class.

  • Liskov Substitution Principle (LSP): Subtypes should be substitutable for their base types, meaning that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program.

  • Interface Segregation Principle (ISP): A class should not be forced to implement interfaces it doesn't use, meaning that a class should only be required to implement the methods that it needs.

  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules, but both should depend on abstractions, meaning that the design should favor abstraction over concrete implementation.

These principles help to guide the design of software systems by promoting loose coupling, flexibility, maintainability and reusability. By following the SOLID principles, developers can create software systems that are easier to understand, change and test, and can help to prevent the creation of tightly coupled, hard-to-change code. Single Responsibility Principle (SRP)

Here are some examples of how the SOLID principles can be applied in code:


class Order {
    public void calculateTotal() {...}
    public void saveOrder() {...}
    public void sendInvoice() {...}
}

In this example, the Order class is handling three different responsibilities: calculating the total, saving the order, and sending the invoice. This violates the SRP because a change in any of these responsibilities would require the class to be modified. A better design would be to split this class into separate classes, each with its own responsibility:


class Order {
    public void calculateTotal() {...}
    public void saveOrder() {...}
}

class Invoice {
    public void sendInvoice() {...}
}

Open-Closed Principle (OCP)

class Shape {
    public double area() {...}
}

class Rectangle extends Shape {
    public double width;
    public double height;
    public double area() { return width * height; }
}

In this example, the area method in the Shape class is open for modification because any change to the way the area is calculated would require modifying the Shape class. A better design would be to make the area method abstract and have each shape implement it.


abstract class Shape {
    public abstract double area();
}

class Rectangle extends Shape {
    public double width;
    public double height;
    public double area() { return width * height; }
}

Liskov Substitution Principle (LSP)

class Rectangle {
    public double width;
    public double height;
    public double area() { return width * height; }
}

class Square extends Rectangle {
    public double side;
    public void setWidth(double side) {
        this.side = side;
        this.width = side;
    }
    public void setHeight(double side) {
        this.side = side;
        this.height = side;
    }
}

In this example, a Square is a rectangle but it's not substitutable for it because the setWidth and setHeight methods of the square class break the class invariant of a rectangle. A better design would be to have a square class that implements a square interface and is not a subclass of rectangle class. Interface Segregation Principle (ISP)

Interface Segregation Principle allows you to divide interfaces into smaller, specialized interfaces to prevent clients from depending on unnecessary methods.

interface Worker {
    public void work();
    public void eat();
    public void sleep();
}

class Human implements Worker {
    public void work() {
        // code to work
    }
    public void eat() {
        // code to eat
    }
    public void sleep() {
        // code to sleep
    }
}

class Robot implements Worker {
    public void work() {
        // code to work
    }
    public void eat() {
        // This method is not applicable for Robot
    }
    public void sleep() {
        // This method is not applicable for Robot
    }
}

In this example, the Worker interface has three methods: work(), eat(), and sleep(). The Human class implements all three methods, but the Robot class does not require the eat() and sleep() methods, as robots do not eat or sleep. This is a violation of the ISP because the Robot class is forced to implement methods that are not applicable to it. Instead, we can create separate interfaces for different responsibilities


interface Workable {
    public void work();
}
interface Eatable {
    public void eat();
}
interface Sleepable {
    public void sleep();
}
class Human implements Workable,Eatable, Sleepable {
    public void work() {
        // code to work
    }
    public void eat() {
        // code to eat
    }
    public void sleep() {
        // code to sleep
    }
}
class Robot implements Workable {
    public void work() {
        // code to work
    }
}

By doing this, the Robot class only has to implement the methods that are applicable to it, and it is not forced to implement methods that are not relevant to it. This makes the code more flexible and easier to maintain.


Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) is a principle in object-oriented programming that states that:

  • 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.

This principle is closely related to the concept of inversion of control (IoC), which is a design pattern that helps to implement DIP.


Here is an example of how DIP can be applied:


interface MessageService {
    void sendMessage(String msg, String rec);
}

class EmailService implements MessageService {
    public void sendMessage(String msg, String rec){
        //logic to send email
        System.out.println("Email sent to "+rec+ " with Message="+msg);
    }
}

class MyApplication {
    private MessageService service;
   
    public MyApplication(MessageService svc){
        this.service=svc;
    }
   
    public void sendMessage(String msg, String rec){
        //some business logic here
        service.sendMessage(msg, rec);
    }
}

class EmailServiceInjector implements MessageServiceInjector {
    public MyApplication getConsumer(){
        return new MyApplication(new EmailService());
    }
}

class Main {
    public static void main(String[] args) {
        MessageServiceInjector injector = new EmailServiceInjector();
        MyApplication app = injector.getConsumer();
        app.sendMessage("Hello", "abc@example.com");
    }
}

In conclusion, SOLID is a set of five design principles for object-oriented programming that promote code maintainability and flexibility. The principles are Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Following SOLID principles can help developers write more organized, understandable, and easily maintainable code.

Have a look at my code examples for the SOLID principles on GitHub. Also, read through this coding best practices to help you with improving the quality of your code.

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating

Subscribe to get exclusive updates

Thanks for subscribing!

CONTACT ME
avatar-formal-round.png

Follow

  • Medium
  • Facebook
  • Twitter
  • LinkedIn
  • Instagram
  • Youtube
  • linktree
  • Buy Me A Coffee

© 2019 - 2024 By Biyi Akinpelu. The LORD Is My Banner

bottom of page