对 SOLID 原则的完整解释
Source: Dev.to
介绍
软件很少因为 “算法错误” 而直接失败。
更常见的情况是它逐渐退化:添加新功能变得困难,错误重新出现,哪怕是微小的改动也会让人觉得风险很大。
SOLID 原则为我们提供了五种实用的方法,使代码在更长的时间内保持可理解和可适应。
SOLID 不是 三种物质状态之一。
它是五条设计原则的首字母缩写,帮助我们编写高度可维护的代码。
这些原则是:
- 单一职责原则(SRP)
- 开放‑封闭原则(OCP)
- 里氏替换原则(LSP)
- 接口分离原则(ISP)
- 依赖倒置原则(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) { /* … */ }
}
问题
- 四个改变理由(
validate、saveToDatabase、sendEmail、log)。 - 难以复用——调用方在只需要验证时不想触发邮件和数据库写入。
- 测试噩梦——必须模拟日志记录器、邮件客户端和数据库客户端。
更佳设计
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) 对 Sparrow 和 Ostrich 都能正常工作,不会出现意外行为。
记忆辅助(中文翻译): 如果子类的行为和父类一样,它就可以在任何需要父类的地方使用(对应原 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 不再创建或了解 MasalaTeaMaker 或 GingerTeaMaker。相反,在运行时会注入具体的 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(依赖倒置)。
如果有任何问题,欢迎在下方留言。
感谢阅读 – 敬请期待!