Skip to content

SOLID Principle

SOLID principles are object-oriented design concepts relevant to software development. SOLID is an acronym for five other class-design principles: Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle.

  • Single Responsibility Principle

  • An object should have only one responsibility, and this responsibility should be entirely encapsulated within a single class. Each class should be responsible for a single part or functionality of the system.

  • Open-Closed Principle

  • Software entities should be open for extension but closed for modification.

  • This means that a software entity, such as a class, module, or function, should be open for extension but closed to modification. Here, openness to extension is from the perspective of the provider, while closure to modification applies to the caller or client.

  • Liskov Substitution Principle

  • In simple terms, a subclass can extend the functionality of the superclass, but it should not alter the existing functionality of the superclass:

    1. A subclass can implement abstract methods from the superclass but should not override concrete (non-abstract) methods.
    2. A subclass can add its own unique methods.
    3. When a subclass overloads a superclass method, the method's preconditions (i.e., input parameters) should be less strict than the superclass method.
    4. When a subclass implements a superclass method (either through overriding or overloading), the postconditions (i.e., output/return value) should be as strict as or stricter than those of the superclass.
  • Meaning that subclass should not contain constraints that superclass does not have.

  • Interface Segregation Principle

  • Clients should not be forced to depend on interfaces they do not use.

  • Dependency Inversion Principle

  • High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details, but details should depend on abstractions.

    A great example of its application is the Spring framework.

1 Simple Responsibility Pinciple ✅

The Single Responsibility Principle (SRP) is the simplest of object-oriented design principles, used to control the size and granularity of a class.

An object should have only one responsibility, and this responsibility should be entirely encapsulated within a single class.

In this example, we have a People class:

// A person class
public class People {

    /**
     * People can code
     */
    public void coding(){
        System.out.println("int main() {");
        System.out.println("   printf(\"Hello World!\");");
        System.out.println("}");
        System.out.println("Hmm, why isn't it running? I followed exactly what the instructor typed...");
    }

    /**
     * People can also work in a factory
     */
    public void work(){
        System.out.println("So happy to join the assembly line at Foxconn.");
        System.out.println("Wait, why are all my coworkers running away?");
    }

    /**
     * People can also deliver food
     */
    public void ride(){
        System.out.println("Finally passed the final interview with Meituan and joined the dream company.");
        System.out.println("The interview seemed pretty easy. Not sure why my friend had to solve a LeetCode 'Trapping Rain Water' problem, while I just had to say I could ride a bike.");
        System.out.println("(*Excitedly puts on the delivery uniform*)");
    }
}

Here, the People class is highly versatile, covering various skills, but in reality, people specialize in their own fields. Following the Single Responsibility Principle, each skill should be encapsulated within its relevant role—programming for a coder, assembly work for a factory worker, and food delivery for a rider. This ensures each class has only one reason to change and keeps the code manageable.

Refactored code based on SRP:

class Coder {
    /**
     * Coders know how to program
     */
    public void coding(){
        System.out.println("int main() {");
        System.out.println("   printf(\"Hello World!\");");
        System.out.println("}");
        System.out.println("Hmm, why isn't it running? I followed exactly what the instructor typed...");
    }
}

class Worker {
    /**
     * Workers know how to assemble
     */
    public void work(){
        System.out.println("So happy to join the assembly line at Foxconn.");
        System.out.println("Wait, why are all my coworkers running away?");
    }
}

class Rider {
    /**
     * Riders know how to deliver
     */
    public void ride(){
        System.out.println("Finally passed the final interview with Meituan and joined the dream company.");
        System.out.println("The interview seemed pretty easy. Not sure why my friend had to solve a LeetCode 'Trapping Rain Water' problem, while I just had to say I could ride a bike.");
        System.out.println("(*Excitedly puts on the delivery uniform*)");
    }
}

This restructuring makes it clear and organized. The same principle can be applied to separate concerns across different layers, such as Mapper, Service, and Controller, for different functionalities. It aligns with the design of microservices, where each service is responsible for a single function, thereby achieving high cohesion and low coupling.

https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.788.videopod.episodes&vd_source=73e7d2c4251a7c9000b22d21b70f5635&p=2

定义: 一个类应该只有一个引起它变化的原因,即它应该只有一个职责。 作用: 降低类的复杂度,提高代码的可维护性。

示例(错误示范):

class Invoice {
    void calculateTotal() { /* 计算订单总价 */ }
    void printInvoice() { /* 打印订单信息 */ }
}

问题: 这个 Invoice 类既负责计算总价,又负责打印发票,违反了单一职责原则。

正确示例:

class InvoiceCalculator {
    double calculateTotal(Invoice invoice) { /* 计算订单总价 */ }
}

class InvoicePrinter {
    void print(Invoice invoice) { /* 打印订单信息 */ }
}

改进点: 将计算和打印功能分开,避免职责混乱,提高代码的复用性。

2 Open-Closed Principle ✅

The Open-Closed Principle (OCP) is also an essential object-oriented design principle.

Software entities should be open for extension but closed for modification.

This means that a software entity, such as a class, module, or function, should be open for extension but closed to modification. Here, openness to extension is from the perspective of the provider, while closure to modification applies to the caller or client.

For example, let’s say we have different types of programmers: Java developers, C# developers, C++ developers, PHP developers, frontend developers, etc. They all share the responsibility of writing code, but the way they code depends on their specific language. We can abstract the coding behavior into a common interface or abstract class. This meets the first requirement of OCP: open to extension, as each type of programmer can implement the coding behavior in their own way. The second requirement, closed to modification, is also met, as each programmer’s coding style is defined independently without interference from others.

Here’s an example:

public abstract class Coder {

    public abstract void coding();

    class JavaCoder extends Coder {
        @Override
        public void coding() {
            System.out.println("Java is too competitive T_T, better start learning Go!");
        }
    }

    class PHPCoder extends Coder {
        @Override
        public void coding() {
            System.out.println("PHP is the best language in the world.");
        }
    }

    class CPlusPlusCoder extends Coder {
        @Override
        public void coding() {
            System.out.println("Funny, no matter how great Java is, it still depends on me for the low-level stuff.");
        }
    }
}

By providing an abstract Coder class that defines the coding behavior without implementing it, we allow each specific type of programmer to implement their unique way of coding. This creates a flexible structure that is easy to extend with new types of programmers, ensuring longevity and adaptability.

In fact, as we review everything we’ve learned so far, it seems like the Open-Closed Principle has been applied in many places along the way.

https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.788.videopod.episodes&vd_source=73e7d2c4251a7c9000b22d21b70f5635&p=3

定义: 软件实体(类、模块、函数)应该对扩展开放,对修改关闭。 作用: 通过扩展而不是修改已有代码来适应新的需求,避免代码改动引入新问题。

示例(错误示范):

class DiscountService {
    double applyDiscount(String type, double price) {
        if (type.equals("BlackFriday")) {
            return price * 0.8;
        } else if (type.equals("Christmas")) {
            return price * 0.9;
        }
        return price;
    }
}

问题: 每增加一种折扣类型,就必须修改 applyDiscount 方法,违反了开闭原则。

正确示例(使用多态扩展功能):

interface DiscountStrategy {
    double applyDiscount(double price);
}

class NoDiscount implements DiscountStrategy {
    public double applyDiscount(double price) { return price; }
}

class BlackFridayDiscount implements DiscountStrategy {
    public double applyDiscount(double price) { return price * 0.8; }
}

改进点: 通过新增类而不是修改原有代码来支持新功能,符合开闭原则。

3 Liskov Substitution Principle ✅

The Liskov Substitution Principle (LSP) is a specific guideline for handling subtypes. It was first introduced by Barbara Liskov in her 1987 speech titled "Data Abstraction and Hierarchy."

Objects of a subclass should be replaceable with objects of the superclass without altering the correctness of the program.

In simple terms, a subclass can extend the functionality of the superclass, but it should not alter the existing functionality of the superclass:

  1. A subclass can implement abstract methods from the superclass but should not override concrete (non-abstract) methods.
  2. A subclass can add its own unique methods.
  3. When a subclass overloads a superclass method, the method's preconditions (i.e., input parameters) should be less strict than the superclass method.
  4. When a subclass implements a superclass method (either through overriding or overloading), the postconditions (i.e., output/return value) should be as strict as or stricter than those of the superclass.

Let's look at the following example:

public abstract class Coder {

    public void coding() {
        System.out.println("I know how to code");
    }

    class JavaCoder extends Coder {

        /**
         * In addition to coding, this subclass can also play games
         */
        public void game() {
            System.out.println("The strongest player in Ionia has logged in");
        }
    }
}

Here, JavaCoder inherits from Coder without overriding any superclass methods. It also extends functionality by adding its own method game(), which complies with the Liskov Substitution Principle.

Now let’s examine a different example:

public abstract class Coder {

    public void coding() {
        System.out.println("I know how to code");
    }

    class JavaCoder extends Coder {
        public void game() {
            System.out.println("The strongest player in Ionia has logged in");
        }

        /**
         * Here, we've overridden the behavior of the superclass, so it no longer retains its original functionality.
         */
        public void coding() {
            System.out.println("After sixteen years of hard work, I'm still outdone by a bootcamp graduate...");
            System.out.println("Life is so tiring with the house, car, and marriage expenses.");
            System.out.println("Should life be this hard just to buy a house? Can't I live freely?");
            System.out.println("Giving up... alright, alright.");
            // End of 'emo' phase—let's get back to work. Life has beauty, despite its hardships, and there are still things to look forward to.
        }
    }
}

In this case, we’ve overridden the coding method in JavaCoder, which changes the original behavior of the superclass. This violates the Liskov Substitution Principle because the subclass no longer preserves the original behavior of the superclass.

If a programmer doesn’t know how to code, can they still be called a programmer?

In this situation, inheriting from Coder is no longer suitable. Instead, we can promote the behavior to a more generic class, People, and redefine the structure:

public abstract class People {

    public abstract void coding();   // Define the behavior here but don't implement it

    class Coder extends People {
        @Override
        public void coding() {
            System.out.println("I know how to code");
        }
    }

    class JavaCoder extends People {
        public void game() {
            System.out.println("The strongest player in Ionia has logged in");
        }

        public void coding() {
            System.out.println("Giving up... alright, alright.");
        }
    }
}

The Liskov Substitution Principle is an important way to achieve the Open-Closed Principle as well.

https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.788.player.switch&vd_source=73e7d2c4251a7c9000b22d21b70f5635&p=4

定义: 子类必须能够替换其基类,并且不影响程序的正确性。 作用: 保证继承关系的合理性,提高代码的可靠性。

示例(错误示范):

class Bird {
    void fly() { System.out.println("Flying"); }
}

class Penguin extends Bird {
    @Override
    void fly() { throw new UnsupportedOperationException("企鹅不会飞!"); }
}

问题: Penguin 继承了 Bird,但 Penguin.fly() 方法抛出异常,违反了 LSP。

正确示例(修改继承关系):

interface Bird { }

interface FlyingBird extends Bird {
    void fly();
}

class Sparrow implements FlyingBird {
    public void fly() { System.out.println("Flying"); }
}

class Penguin implements Bird { /* 企鹅不会飞,不需要实现 fly 方法 */ }

改进点:Bird 拆分为 FlyingBirdBird,避免不合理的继承。

4 Interface Segregation Principle ✅

The Interface Segregation Principle (ISP) focuses on refining interfaces.

"Clients should not be forced to depend on interfaces they do not use."

When defining interfaces, it’s essential to control their granularity. Here’s an example:

interface Device {
    String getCpu();
    String getType();
    String getMemory();
}

// A computer is a type of electronic device, so we implement this interface
class Computer implements Device {

    @Override
    public String getCpu() {
        return "i9-12900K";
    }

    @Override
    public String getType() {
        return "Computer";
    }

    @Override
    public String getMemory() {
        return "32G DDR5";
    }
}

// A fan is also an electronic device
class Fan implements Device {

    @Override
    public String getCpu() {
        return null;   // Does a simple fan really need a CPU?
    }

    @Override
    public String getType() {
        return "Fan";
    }

    @Override
    public String getMemory() {
        return null;   // A fan doesn't need memory either
    }
}

Although we defined a Device interface, its granularity is too coarse. While it works well for a computer, it’s not suitable for a fan, which doesn’t require CPU or memory methods. This is where we should create more specialized, fine-grained interfaces:

interface SmartDevice {   // Only smart devices have getCpu and getMemory
    String getCpu();
    String getType();
    String getMemory();
}

interface NormalDevice {   // Basic devices only have getType
    String getType();
}

// A computer is a smart device, so it implements this interface
class Computer implements SmartDevice {

    @Override
    public String getCpu() {
        return "i9-12900K";
    }

    @Override
    public String getType() {
        return "Computer";
    }

    @Override
    public String getMemory() {
        return "32G DDR5";
    }
}

// A fan is a normal device
class Fan implements NormalDevice {
    @Override
    public String getType() {
        return "Fan";
    }
}

By dividing the interfaces into finer, more specific interfaces, each type of device can implement only the interfaces relevant to it. However, we should avoid over-segmentation and keep interfaces practical and based on real requirements.

https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.788.player.switch&vd_source=73e7d2c4251a7c9000b22d21b70f5635&p=6

定义: 客户端不应该被迫依赖它不需要的接口。 作用: 避免“胖接口”(包含过多方法的接口),提高系统的灵活性和可维护性。

示例(错误示范):

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

class Robot implements Worker {
    public void work() { System.out.println("Working..."); }
    public void eat() { throw new UnsupportedOperationException("机器人不吃饭!"); }
}

问题: Robot 不需要 eat() 方法,但仍然必须实现它,违反了接口隔离原则。

正确示例(拆分接口):

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Human implements Workable, Eatable {
    public void work() { System.out.println("Working..."); }
    public void eat() { System.out.println("Eating..."); }
}

class Robot implements Workable {
    public void work() { System.out.println("Working..."); }
}

改进点: 将接口拆分,使 Robot 只依赖 Workable,避免不必要的方法。

5 Dependence Inversion Principle ✅

The Dependence Inversion Principle (DIP) is a fundamental principle in object-oriented design. A great example of its application is the Spring framework.

"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details, but details should depend on abstractions."

Do you remember why we’ve consistently used interfaces to define functionality, implementing them afterward? Let’s look back at what code might look like without the Spring framework:

public class Main {

    public static void main(String[] args) {
        UserController controller = new UserController();
        // use the controller as needed
    }

    static class UserMapper {
        // CRUD operations
    }

    static class UserService {
        UserMapper mapper = new UserMapper();
        // Business logic
    }

    static class UserController {
        UserService service = new UserService();
        // Business logic
    }
}

Now imagine one day the company changes its requirements, and the UserService needs to be replaced with a new implementation:

public class Main {

    public static void main(String[] args) {
        UserController controller = new UserController();
    }

    static class UserMapper {
        // CRUD operations
    }

    static class UserServiceNew { // This change in UserServiceNew impacts other high-level modules
        UserMapper mapper = new UserMapper();
        // Business logic
    }

    static class UserController { // Changing the lower-level module forces modifications here too
        UserService service = new UserService(); // Old service no longer usable
        UserServiceNew serviceNew = new UserServiceNew(); // Switching to the new service
        // Business logic
    }
}

As we can see, each module is directly dependent on another, creating a tightly coupled structure. Any change in a low-level module (like replacing UserService) directly impacts higher-level modules that depend on it. In a large project, these modifications would be overwhelming and difficult to maintain.

With the introduction of the Spring framework, our development approach changes:

public class Main {

    public static void main(String[] args) {
        UserController controller = new UserController();
    }

    interface UserMapper {
        // Only defines CRUD methods
    }

    static class UserMapperImpl implements UserMapper {
        // Implements CRUD methods
    }

    interface UserService {
        // Defines business logic methods
    }

    static class UserServiceImpl implements UserService {
        @Resource // Spring selects an appropriate implementation and injects it, avoiding hard coding
        UserMapper mapper;

        // Implements business logic
    }

    static class UserController {
        @Resource
        UserService service; // Uses the interface only, so no need to modify if the implementation changes

        // Business logic
    }
}

Here, by using interfaces, we loosen the original tight coupling between modules. The high-level UserController only needs to know about the methods defined in the UserService interface. The specific implementation details are handled by the UserServiceImpl class and are injected by Spring, removing the need for hard-coded dependencies.

This approach weakens dependencies, allows greater flexibility, and enhances maintainability, aligning well with the Dependence Inversion Principle.。

https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.788.player.switch&vd_source=73e7d2c4251a7c9000b22d21b70f5635&p=5

定义: 高层模块不应该依赖低层模块,二者都应该依赖抽象。 作用: 降低模块之间的耦合度,提高代码的可扩展性。

示例(错误示范):

class Keyboard { }
class Monitor { }

class Computer {
    private Keyboard keyboard;
    private Monitor monitor;

    Computer() {
        this.keyboard = new Keyboard();
        this.monitor = new Monitor();
    }
}

问题: Computer 直接依赖具体的 KeyboardMonitor,如果换成 MechanicalKeyboardOLEDMonitor,就必须修改 Computer 代码,违反依赖倒置原则。

正确示例(使用依赖注入):

interface Keyboard { }
class MechanicalKeyboard implements Keyboard { }
class MembraneKeyboard implements Keyboard { }

class Computer {
    private Keyboard keyboard;

    Computer(Keyboard keyboard) {
        this.keyboard = keyboard;
    }
}

改进点: Computer 依赖 Keyboard 接口,而不是具体的实现类,提高了灵活性。