SOLID 原则:编写可维护的面向对象代码

FreeGuideOnline 最新 2026-06-18

面向对象 SOLID 原则:编写可维护的面向对象代码

什么是 SOLID 原则?

SOLID 是面向对象编程和设计中五个基本原则的缩写,由 Robert C. Martin(Uncle Bob)提出并推广。这五个原则共同构成了一套编写可维护、可扩展、低耦合、高内聚代码的指南。当你的项目越变越大时,遵循这些原则可以有效减少“牵一发而动全身”的风险,让代码更容易理解和修改。

五个字母分别代表:

  • Single Responsibility Principle(单一职责原则)
  • Open/Closed Principle(开闭原则)
  • Liskov Substitution Principle(里氏替换原则)
  • Interface Segregation Principle(接口隔离原则)
  • Dependency Inversion Principle(依赖倒置原则)

单一职责原则(SRP)

定义

一个类应该只有一个引起它变化的原因。
换句话说,一个类只负责完成一个明确的功能,如果它因为多个不相关的理由需要被修改,那就说明这个类的职责不够单一。

为什么重要?

  • 提高类的内聚性,降低耦合。
  • 修改一个功能时不会意外影响其他不相关功能。
  • 类更小、更易读、更易测试。

违反 SRP 的例子

class Report {
    private String content;

    public void generateReport() {
        // 生成报告内容
    }

    public void saveToFile(String filename) {
        // 将报告保存到文件系统
    }

    public void sendByEmail(String email) {
        // 通过邮件发送报告
    }
}

在这个类中,Report 负责了三件不同的事情:生成内容持久化存储发送通知。未来如果文件存储方式改变(比如从本地文件变为云存储),或者邮件发送逻辑需要调整,都会迫使修改这个原本只关注“报告内容生成”的类。

遵循 SRP 的重构

class Report {
    private String content;

    public void generateReport() {
        // 生成报告内容
    }
}

class ReportPersistence {
    public void saveToFile(Report report, String filename) {
        // 保存报告到文件
    }
}

class ReportMailer {
    public void sendByEmail(Report report, String email) {
        // 发送报告邮件
    }
}

现在每个类只负责一件事,任何一方的变化都被隔离在各自的类中。

开闭原则(OCP)

定义

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
当需求变化时,应该通过添加新代码(扩展)来适应变化,而不是去修改已经存在并测试通过的代码。

实现方式

通常通过抽象和继承/接口来实现:定义一个稳定的抽象,让不同的行为实现这个抽象,从而在不改动原有代码的基础上扩展功能。

违反 OCP 的例子

class AreaCalculator {
    public double area(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.width * r.height;
        } else if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.radius * c.radius;
        }
        return 0;
    }
}

每当需要支持一种新图形(例如三角形)时,都必须修改 AreaCalculator,增加 else if 分支。这违背了“对修改关闭”。

遵循 OCP 的重构

interface Shape {
    double area();
}

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

class Circle implements Shape {
    double radius;
    public double area() { return Math.PI * radius * radius; }
}

class AreaCalculator {
    public double area(Shape shape) {
        return shape.area(); // 只需调用抽象方法
    }
}

现在增加三角形只需要新建一个实现 Shape 的类,而 AreaCalculator 完全不需要改变。

里氏替换原则(LSP)

定义

所有引用基类的地方必须能够透明地使用其子类的对象,而不会产生任何错误或异常。
简单来说:子类必须能真正替代父类,继承关系应该是“是一个(is-a)”的语义强化,而不是削弱。

违反 LSP 的典型情况

  • 子类重写父类方法并抛出父类未声明的异常。
  • 子类重写方法时修改了父类方法原本的含义或行为约束(如前置条件更强、后置条件更弱)。
  • 经典的“矩形—正方形”问题。
class Rectangle {
    private int width, height;
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w); // 强制让宽高相等
    }
    @Override
    public void setHeight(int h) {
        super.setWidth(h);
        super.setHeight(h);
    }
}

现在如果有一个方法接收 Rectangle 参数:

void changeSize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.getArea() == 50; // 对矩形成立
}

当传入 Square 对象时,setWidth(5) 会让宽和高都变成 5,setHeight(10) 又会让它们都变成 10,最终面积是 100,断言失败。正方形不是完全意义上的矩形,因此不应该用继承来实现。

遵循 LSP 的重构

解决方案之一是取消继承关系,让正方形不再继承矩形,或者两者都从更抽象的接口继承。如果不能确保完美的“is-a”关系,宁可使用组合而非继承。

接口隔离原则(ISP)

定义

客户端不应该被强迫依赖于它不使用的方法。
一个臃肿的接口往往会被不同客户端只使用其中一部分功能,修改这个接口会影响到所有客户端,即便它们不关心那部分改变。

典型坏味道

一个“万能”的接口:

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

如果有一个 Robot 类实现了 Worker,它并不需要 eat()sleep(),却不得不提供空实现或抛出异常,这就破坏了 ISP。

遵循 ISP 的重构

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

class HumanWorker implements Workable, Eatable, Sleepable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

class Robot implements Workable {
    public void work() { /* ... */ }
}

现在客户端可以根据自己的需要只依赖 Workable 接口,不再被无用的方法污染。

依赖倒置原则(DIP)

定义

高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。

  • 高层模块:实现业务逻辑的策略或用例。
  • 低层模块:具体的实现细节,如数据库操作、第三方库调用、文件读写等。

传统编程中高层模块直接调用低层模块,导致高层模块难以替换底层实现。DIP 要求我们反转依赖方向,让高层模块定义接口(抽象),低层模块去实现这些接口。

违反 DIP 的例子

class LightBulb {
    public void turnOn() { /* ... */ }
    public void turnOff() { /* ... */ }
}

class Switch {
    private LightBulb bulb;
    
    public Switch(LightBulb bulb) {
        this.bulb = bulb;
    }
    
    public void operate() {
        // ... toggle
    }
}

Switch 直接依赖于具体的 LightBulb,如果将来想控制 Fan 或其他设备,必须修改 Switch

遵循 DIP 的重构(依赖注入)

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

class LightBulb implements Switchable {
    public void turnOn() { /* ... */ }
    public void turnOff() { /* ... */ }
}

class Fan implements Switchable {
    public void turnOn() { /* ... */ }
    public void turnOff() { /* ... */ }
}

class Switch {
    private Switchable device;
    
    public Switch(Switchable device) {
        this.device = device;
    }
    
    public void operate() {
        // 调用 device.turnOn() 或 turnOff()
    }
}

现在 Switch 只依赖抽象 Switchable,任何实现了该接口的设备都可以被控制,改动低层细节不会影响高层开关逻辑。

总结

SOLID 原则不是僵化的教条,而是帮助你应对软件复杂度的指导方针。

  • SRP 让你的类更小、更专注,减少修改的理由。
  • OCP 让代码通过扩展引入新特性,而非冒险修改稳定代码。
  • LSP 保证继承体系的可用性,避免子类破坏程序的正确性。
  • ISP 避免臃肿接口,让客户端只依赖自己需要的方法。
  • DIP 将代码从具体实现中解耦,使得系统能更容易适应变化。

将这些原则组合使用,能够设计出健壮、灵活且易于维护的面向对象系统。日常开发中不必追求100%“纯净”,但时常自问:“这个设计是否能让未来变更更轻松?”会帮助你逐步内化这些思想。