对 SOLID 原则的完整解释

发布: (2025年12月31日 GMT+8 20:03)
10 min read
原文: Dev.to

Source: Dev.to

介绍

软件很少因为 “算法错误” 而直接失败。
更常见的情况是它逐渐退化:添加新功能变得困难,错误重新出现,哪怕是微小的改动也会让人觉得风险很大。

SOLID 原则为我们提供了五种实用的方法,使代码在更长的时间内保持可理解和可适应。

SOLID 不是 三种物质状态之一。
它是五条设计原则的首字母缩写,帮助我们编写高度可维护的代码。

这些原则是:

  1. 单一职责原则(SRP)
  2. 开放‑封闭原则(OCP)
  3. 里氏替换原则(LSP)
  4. 接口分离原则(ISP)
  5. 依赖倒置原则(DIP)

它们共同鼓励松耦合、明确职责和稳定抽象——这些对于长期维护的代码至关重要。

1. 单一职责原则(SRP)

一个类应该只有唯一且唯一的改变理由

不良示例

class UserService {
    public void validate(User user) { /* … */ }
    public void saveToDatabase(User user) { /* … */ }
    public void sendEmail(User user) { /* … */ }
    public void log(User user) { /* … */ }
}

问题

  • 四个改变理由(validatesaveToDatabasesendEmaillog)。
  • 难以复用——调用方在只需要验证时不想触发邮件和数据库写入。
  • 测试噩梦——必须模拟日志记录器、邮件客户端和数据库客户端。

更佳设计

class UserValidator {
    public void validate(User user) { /* … */ }
}

class UserRepository {
    public void save(User user) { /* … */ }
}

class EmailNotifier {
    public void send(User user) { /* … */ }
}

class Logger {
    public void log(User user) { /* … */ }
}

每个类现在只有一个改变理由,可以独立使用(例如,仅进行验证而不发送邮件)。

箴言: Ek class – ek kaam(一个类——一个工作)。

2. 开闭原则 (OCP)

软件实体应该对扩展开放,但对修改关闭

违反 OCP

class AreaCalculator {
    public double calculateArea(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;
        }
        // Adding a Square would require modifying this method → OCP violation
    }
}

符合 OCP 的设计

abstract class Shape {
    abstract double area();
}

class Rectangle extends Shape {
    double width, height;
    @Override double area() { return width * height; }
}

class Circle extends Shape {
    double radius;
    @Override double area() { return Math.PI * radius * radius; }
}

// New shapes (e.g., Square) just extend Shape – no change to AreaCalculator
class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.area();
    }
}

现在我们可以添加一个 Square(或任何新形状)而无需触碰 AreaCalculator

箴言: Extend karo, edit nahi(扩展,不要编辑)。

3. 里氏替换原则 (LSP)

超类的对象应该可以被其子类的对象所替代,而不会改变程序的期望属性

有问题的示例

interface Bird {
    void fly();
    void walk();
}

class Sparrow implements Bird {
    public void fly() { /* flying logic */ }
    public void walk() { /* walking logic */ }
}

class Ostrich implements Bird {
    public void fly() { throw new UnsupportedOperationException(); }
    public void walk() { /* walking logic */ }
}

Ostrich 破坏了 fly() 的契约——期望任何 Bird 都能飞的代码将会失败。

更好的抽象

interface Walkable {
    void walk();
}

interface Flyable {
    void fly();
}

abstract class Bird implements Walkable { /* common walk logic */ }

class Sparrow extends Bird implements Flyable {
    public void fly() { /* flying logic */ }
}

class Ostrich extends Bird {
    // No fly() method – respects LSP
}

现在 makeWalk(Bird b)SparrowOstrich 都能正常工作,不会出现意外行为。

记忆辅助(中文翻译): 如果子类的行为和父类一样,它就可以在任何需要父类的地方使用(对应原 Hindi: “Agar bachche ka behavior parents ke jaise hai, to wo party mein ja sakta hai”)。

4. 接口分离原则 (ISP)

客户端不应被迫依赖它们不使用的接口。

违反 ISP

interface IWorker {
    void work();
    void eat();   // Robots don’t eat!
}

Robot 类必须实现一个毫无意义的 eat() 方法,从而产生冗余代码并导致紧耦合。

符合 ISP 的设计

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Human implements Workable, Eatable {
    public void work() { /* … */ }
    public void eat()  { /* … */ }
}

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

现在每个类只实现它们实际需要的接口。

箴言: 分而治之 —— 将庞大的接口拆分为专注的子接口。

5. 依赖倒置原则 (DIP)

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

紧耦合示例

class MasalaTeaMaker {
    public String makeTea() { return "Masala Tea ready!"; }
}

class TeaShop {
    private MasalaTeaMaker maker = new MasalaTeaMaker(); // Hard‑coded dependency!

    public String orderTea(String type) {
        if ("masala".equals(type)) {
            return maker.makeTea();
        }
        return "Only Masala available"; // Rigid!
    }
}

TeaShop 直接依赖于 MasalaTeaMaker。若要添加新的茶类,就必须修改 TeaShop

符合 DIP 的设计

interface TeaMaker {
    String makeTea();
}

class MasalaTeaMaker implements TeaMaker {
    public String makeTea() { return "Masala Tea ready!"; }
}

class GreenTeaMaker implements TeaMaker {
    public String makeTea() { return "Green Tea ready!"; }
}

class TeaShop {
    private final Map<String, TeaMaker> makers = new HashMap<>();

    public TeaShop(Map<String, TeaMaker> makers) {
        this.makers.putAll(makers);
    }

    public String orderTea(String type) {
        TeaMaker maker = makers.get(type);
        return (maker != null) ? maker.makeTea() : "Tea not available";
    }
}
  • TeaShop 依赖于 抽象 TeaMaker
  • 可以在不修改 TeaShop 的情况下添加新的茶类,只需在映射中注册即可。

摘要

原则核心思想常用箴言
SRP每个类只有一个修改的理由一个类 – 一个工作
OCP在不修改已有代码的情况下扩展行为扩展吧,别编辑
LSP子类型必须能够替代其基类型子类行为如同父类
ISP将臃肿的接口拆分为针对特定客户端的接口分而治之
DIP依赖抽象而非具体实现高层 ↔ 低层 通过接口

应用这五大原则有助于我们构建松耦合、高内聚、易维护的软件,使其能够随时间优雅地演进。

更新的 TeaShop 示例:使用抽象

下面是原始代码片段的清理版。
现在的代码遵循 依赖倒置原则(SOLID 中的 “D”),通过依赖抽象 (TeaMaker) 而不是具体类来实现。

1️⃣ 领域模型(Java)

// 1. 抽象 -------------------------------------------------
public interface TeaMaker {
    String makeTea();
}

// 2. 具体实现 --------------------------------------
public class MasalaTeaMaker implements TeaMaker {
    @Override
    public String makeTea() {
        return "Masala Tea ready!";
    }
}

public class GingerTeaMaker implements TeaMaker {
    @Override
    public String makeTea() {
        return "Ginger Tea ready!";
    }
}

// 3. 高层模块(依赖抽象) -------------
public class TeaShop {

    // 构造函数注入(也可以使用 setter 注入)
    private final TeaMaker teaMaker;

    public TeaShop(TeaMaker teaMaker) {
        this.teaMaker = teaMaker;
    }

    public String orderTea(String type) {
        // 商店不再了解具体的制茶器。
        // 它只会调用注入的 TeaMaker 来准备饮品。
        return teaMaker.makeTea();
    }
}

2️⃣ 工作原理

组件责任依赖
TeaMaker为所有制茶类定义契约。
MasalaTeaMaker / GingerTeaMaker实现 TeaMaker 契约的具体类。实现 TeaMaker
TeaShop高层类,点茶只依赖 TeaMaker(抽象)。

TeaShop 不再创建或了解 MasalaTeaMakerGingerTeaMaker。相反,在运行时会注入具体的 TeaMaker 实例。

3️⃣ 依赖注入方式

注入类型示例
构造函数注入(如上所示)new TeaShop(new MasalaTeaMaker())
Setter 注入java\npublic void setTeaMaker(TeaMaker teaMaker) { this.teaMaker = teaMaker; }\n
字段注入(框架特定)@Inject private TeaMaker teaMaker;

选择最适合你项目架构的方式。

4️⃣ 快速记忆口诀

“抽象上依赖,类上不依赖。”

把它当作 “依赖 接口,而不是具体类”。这句口诀帮助你遵循 SOLID 的 依赖倒置原则

5️⃣ 结束语

通过上述重构,我们已经满足了 SOLID 原则,尤其是 D(依赖倒置)。
如果有任何问题,欢迎在下方留言。

感谢阅读 – 敬请期待!

Back to Blog

相关文章

阅读更多 »