SOLID Design Principles

SOLID Design Principles

Introduction

  • SOLID is an acronym that represents five principles of object-oriented programming and design intended to create more maintainable, flexible, and understandable software.
  • These principles were introduced by Robert C. Martin and have become fundamental guidelines for designing robust and maintainable software systems.

S - Single Responsibility Principle (SRP)

  • SRP states that every software component should have only one and only one reason to change, meaning it should have only one responsibility.
  • As per this principle, we should aim for High Cohesion and Loose Coupling.
  • Cohesion is the degree to which various parts of a software component are related.
  • Coupling is defined as the level of inter dependency between various software components.
  • Following Single Responsibility Principle can lead to considerable software maintenance costs.

Example:

public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public void calculateSalary() {
      // Calculate employee's salary based on various factors
      // This method is responsible for both storing salary and calculating it
    }

    public void saveToDatabase() {
     // Save employee information to the database
     // This method is responsible for both calculating salary and saving to the database
    }
}        

In this example, the Employee class violates the SRP because it has two responsibilities or it has two reasons to get changed:

  • calculateSalary(): This method is responsible for calculating the employee's salary.
  • saveToDatabase(): This method is responsible for saving employee information to the database.

To follow the SRP, you should separate these unrelated responsibilities into different classes. Here's a refactored version of the code that adheres to SRP:

public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    // Getters and setters for name and salary

    // Other methods related to employee information

}

public class SalaryCalculator {
    public double calculateSalary(Employee employee) {
        // Calculate employee's salary based on various factors
        return /* calculated salary */;
    }
}

public class EmployeeDatabase {
    public void saveToDatabase(Employee employee) {
        // Save employee information to the database
    }
}        

In this refactored code, the responsibilities of calculating the salary and saving to the database are delegated to separate classes (SalaryCalculator and EmployeeDatabase), adhering to the Single Responsibility Principle. This makes the code more maintainable and easier to understand.

O - Open/Closed Principle (OCP)

  • OCP suggests that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
  • This means you can add new features or behaviors without altering existing code.

Example:

public class Circle {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Square {
    private double sideLength;

    public Square(double sideLength) {
        this.sideLength = sideLength;
    }

    public double getSideLength() {
        return sideLength;
    }

    public double calculateArea() {
        return sideLength * sideLength;
    }
}

public class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return circle.calculateArea();
        } else if (shape instanceof Square) {
            Square square = (Square) shape;
            return square.calculateArea();
        }
        return 0.0;
    }
}        

In this example, the AreaCalculator class violates the Open/Closed Principle. If you want to add support for a new shape (e.g., a triangle), you would need to modify the AreaCalculator class to add another conditional branch for the new shape. This violates the principle because it's not open for extension; instead, it's open for modification.

To adhere to the Open/Closed Principle, you should design the code to allow for new shapes to be added without modifying existing code. Here's a refactored version of the code that adheres to the OCP using polymorphism:

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

public class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Square extends Shape {
    private double sideLength;

    public Square(double sideLength) {
        this.sideLength = sideLength;
    }

    public double getSideLength() {
        return sideLength;
    }

    @Override
    public double calculateArea() {
        return sideLength * sideLength;
    }
}

public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}        

In this refactored code, we use polymorphism by creating an abstract Shape class and having Circle and Square extend it. Now, you can easily add new shapes by creating new subclasses of Shape without modifying the AreaCalculator class, adhering to the Open/Closed Principle.

L - Liskov Substitution Principle (LSP)

  • LSP emphasizes that objects of a derived class should be substitutable for objects of the base class without affecting the correctness of the program.
  • In other words, subtypes must be able to replace their base types without introducing errors or unexpected behavior.

Example:

class Bird {
    void fly() {
        // Common flying behavior for all birds
    }
}

class Ostrich extends Bird {
    @Override
    void fly() {
        // Ostriches cannot fly, but we provide an empty implementation here
    }
}

class Sparrow extends Bird {
    // Sparrows can fly, but we do not override the fly() method
}

public class Main {
    public static void main(String[] args) {
        Bird bird1 = new Ostrich();
        Bird bird2 = new Sparrow();

        bird1.fly(); // This will execute without errors, but it doesn't make sense for an ostrich.
        bird2.fly(); // This will execute, but it's expected to fly.

        // The behavior of the program is not consistent with the LSP.
    }
}        

In this example, the Ostrich class violates the LSP because it overrides the fly() method from the Bird superclass with an empty implementation. The Sparrow class also potentially violates the LSP because it doesn't override the fly() method, even though sparrows can fly.

To adhere to the Liskov Substitution Principle, you should ensure that subclass behavior is consistent with the superclass and that clients using objects of the superclass don't encounter unexpected behaviors when using objects of the subclass. In this case, you should design the class hierarchy differently to accommodate non-flying birds like ostriches.

Here's a refactored version that adheres to the LSP:

abstract class Bird {
    abstract void move(); // General movement behavior
}

class Ostrich extends Bird {
    @Override
    void move() {
        // Ostriches walk instead of flying
    }
}

class Sparrow extends Bird {
    @Override
    void move() {
        // Sparrows fly
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird1 = new Ostrich();
        Bird bird2 = new Sparrow();

        bird1.move(); // Ostriches move by walking
        bird2.move(); // Sparrows move by flying

        // The behavior of the program now aligns with the LSP.
    }
}        

In this refactored code, we introduce a new abstract method move() in the Bird class to handle general movement behavior. The Ostrich and Sparrow classes override this method to provide their specific movement behaviors. This design ensures that the LSP is not violated, and the behavior of each bird subclass is consistent with its nature.

I - Interface Segregation Principle (ISP)

  • ISP advises that clients should not be forced to depend on interfaces they do not use.
  • It encourages designing small and focused interfaces tailored to specific client needs, rather than creating large monolithic interfaces.

Example:

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

class Robot implements Worker {
    @Override
    public void work() {
        // Robot works
    }

    @Override
    public void eat() {
        // Robots don't eat, but we have to provide an empty implementation.
    }
}

class Human implements Worker {
    @Override
    public void work() {
        // Human works
    }

    @Override
    public void eat() {
        // Human eats
    }
}

public class Main {
    public static void main(String[] args) {
        Worker robot = new Robot();
        Worker human = new Human();

        robot.work();
        robot.eat(); // Robots don't eat, but this method is forced upon them.

        human.work();
        human.eat(); // Humans eat, so this method is relevant.

        // The Robot class is forced to implement a method it doesn't need.
    }
}        

In this example, both the Robot and Human classes implement the Worker interface, which includes two methods: work() and eat(). The Robot class violates the ISP because it's forced to implement the eat() method, even though robots don't eat.

To adhere to the Interface Segregation Principle, you should design interfaces that are specific to the needs of the classes that implement them. Here's a refactored version of the code that follows ISP:

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    @Override
    public void work() {
        // Robot works
    }
}

class Human implements Workable, Eatable {
    @Override
    public void work() {
        // Human works
    }

    @Override
    public void eat() {
        // Human eats
    }
}

public class Main {
    public static void main(String[] args) {
        Workable robot = new Robot();
        Workable human = new Human();
        Eatable eater = new Human();

        robot.work();

        human.work();
        eater.eat(); // Now, we can use the Eatable interface when necessary without forcing it on the Robot class.

        // Each class now implements only the methods relevant to its behavior.
    }
}        

In this refactored code, we have split the Worker interface into two separate interfaces: Workable and Eatable. Each class then implements the interface(s) that are relevant to its behavior, ensuring that the classes are not forced to implement unnecessary methods, adhering to the ISP.

D - Dependency Inversion Principle (DIP)

  • DIP suggests that high-level modules should not depend on low-level modules; both should depend on abstractions.
  • Additionally, abstractions should not depend on details; details should depend on abstractions.
  • This promotes loose coupling and facilitates easier maintenance and testing.

Example:

class LightBulb {
    public void turnOn() {
        // Code to turn on the light bulb
    }

    public void turnOff() {
        // Code to turn off the light bulb
    }
}

class Switch {
    private LightBulb bulb;

    public Switch(LightBulb bulb) {
        this.bulb = bulb;
    }

    public void operate() {
        // Operate the light bulb
        bulb.turnOn();
        bulb.turnOff();
    }
}        

In this example, the Switch class depends directly on the LightBulb class, which is a low-level module. This violates the DIP because high-level modules should not depend on low-level modules. If you wanted to switch to a different type of device (e.g., a fan) in the future, you would need to modify the Switch class, which creates a tightly coupled design.

To adhere to the Dependency Inversion Principle, you should introduce an abstraction (an interface or an abstract class) and make both the LightBulb and any future devices adhere to that abstraction. Here's a refactored version that follows DIP:

interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        // Code to turn on the light bulb
    }

    @Override
    public void turnOff() {
        // Code to turn off the light bulb
    }
}

class Fan implements Switchable {
    @Override
    public void turnOn() {
        // Code to turn on the fan
    }

    @Override
    public void turnOff() {
        // Code to turn off the fan
    }
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        // Operate the device
        device.turnOn();
        device.turnOff();
    }
}        

In this refactored code, we introduce the Switchable interface as an abstraction that both the LightBulb and Fan classes implement. Now, the Switch class depends on the abstraction, adhering to the Dependency Inversion Principle. This design allows you to easily add new switchable devices without modifying the Switch class, promoting flexibility and maintainability.


Thank you for reading my article. I hope you found it insightful. Your feedback and engagement are greatly appreciated!

要查看或添加评论,请登录

Ahmed Majeed的更多文章

  • Web Server VS Application Server

    Web Server VS Application Server

    Web Server Web Server is a specialized software or hardware system designed to serve web content over the internet or…

    1 条评论
  • Load Balancing Algorithms

    Load Balancing Algorithms

    What is Load Balancing? Load balancing involves distributing incoming network traffic or requests across multiple…

  • Forward Proxy VS Reverse Proxy

    Forward Proxy VS Reverse Proxy

    Forward Proxy Introduction A client proxy, also known as a forward proxy or simply a proxy, is a server that acts on…

    1 条评论
  • Emotional Intelligence and its importance in team work

    Emotional Intelligence and its importance in team work

    We may find it challenging to manage our emotions when deadlines are tight, or support others when conflicts arise…

社区洞察

其他会员也浏览了