2. 面向对象设计原则(上):单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)
好,咱们今天聊聊面向对象设计的三个核心原则。这三个原则,说白了就是写代码的“交通规则”。你遵守了,代码就顺滑、好改、不容易出车祸。你不遵守,早晚得重构到怀疑人生。
我个人习惯把这仨原则当成“代码体检指标”。每次写完一个模块,我都会问自己三个问题:它是不是只管一件事?能不能在不改它的情况下加新功能?子类替换父类会不会出幺蛾子?如果答案都是肯定的,那这代码基本稳了。
核心知识体系
这三个原则分别解决不同层面的问题:SRP 管“类的职责边界”,OCP 管“系统的扩展能力”,LSP 管“继承关系的正确性”。它们环环相扣,缺一不可。
2.1 单一职责原则(SRP)
单一职责原则,英文叫 Single Responsibility Principle。它的核心思想很简单:一个类应该只有一个引起它变化的原因。换句话说,一个类只负责一件事。
我在项目中遇到过这样的场景:一个订单类,里面既有订单数据的存取逻辑,又有订单金额的计算逻辑,还混着发送邮件通知的逻辑。你想想看,哪天邮件服务换了供应商,我得改这个类;哪天计算规则变了,我还得改这个类。一个类被改了三次,每次改的原因都不一样——这就是典型的 SRP 被破坏了。
判断技巧:如果你写一个类的职责描述时,需要用“和”字连接两个以上的功能,那大概率已经违反了 SRP。比如“这个类负责订单存储和邮件发送”——嗯,这里要注意,该拆了。
咱们看个代码示例。假设有一个报表生成器,既要生成数据,又要格式化输出,还要保存到文件:
// 违反 SRP 的写法
public class ReportGenerator {
public String fetchData() { /* 从数据库取数据 */ }
public String formatReport(String data) { /* 格式化 */ }
public void saveToFile(String report) { /* 保存文件 */ }
}
这个类有三个职责。哪天数据库换了,要改;哪天报表格式变了,要改;哪天存储方式从文件改成云存储,还要改。改一次,整个类都得重新测试。
正确的做法是拆成三个类:
// 符合 SRP 的写法
public class DataFetcher {
public String fetchData() { /* 从数据库取数据 */ }
}
public class ReportFormatter {
public String formatReport(String data) { /* 格式化 */ }
}
public class ReportSaver {
public void saveToFile(String report) { /* 保存文件 */ }
}
拆开之后,每个类只有一个修改原因。改数据库不影响格式化,改格式不影响存储。这就是高内聚、低耦合的体现。
注意:SRP 不是让你把类拆得越细越好。拆得太碎会导致类数量爆炸,反而增加维护成本。我个人的经验是:如果一个类的方法超过 20 个,或者代码行数超过 300 行,就该考虑是否违反了 SRP。
2.2 开闭原则(OCP)
开闭原则,Open-Closed Principle。定义是:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
说白了就是:你想加新功能,别去改已有的代码,而是通过扩展来实现。为什么?因为已有的代码可能已经被测试过、被其他模块依赖着。你一改,可能就引入 bug 了。
我记得有一次,一个支付系统需要支持新的支付方式。原来的代码里,支付逻辑是用 if-else 写的:
// 违反 OCP 的写法
public class PaymentProcessor {
public void process(String type) {
if (type.equals("alipay")) {
// 支付宝支付逻辑
} else if (type.equals("wechat")) {
// 微信支付逻辑
}
// 每加一种支付方式,就得加一个 else if
}
}
这种写法,每加一种支付方式,就得修改 PaymentProcessor 类。改多了,这个类就变成了“屎山”。
符合 OCP 的做法是:定义一个支付接口,然后每种支付方式实现这个接口:
// 符合 OCP 的写法
public interface Payment {
void pay(double amount);
}
public class Alipay implements Payment {
public void pay(double amount) { /* 支付宝逻辑 */ }
}
public class WechatPay implements Payment {
public void pay(double amount) { /* 微信逻辑 */ }
}
public class PaymentProcessor {
public void process(Payment payment, double amount) {
payment.pay(amount);
}
}
现在要加新的支付方式,只需要新建一个类实现 Payment 接口,完全不用动 PaymentProcessor。这就是“对扩展开放,对修改关闭”。
核心要点:OCP 的关键在于抽象。通过接口或抽象类定义稳定的契约,具体实现可以无限扩展。抽象层越稳定,系统越灵活。
2.3 里氏替换原则(LSP)
里氏替换原则,Liskov Substitution Principle。定义比较学术:子类对象应该能够替换父类对象,并且程序的行为不会发生变化。
我曾经踩过一个坑。当时有个 Rectangle(矩形)类,有 setWidth 和 setHeight 方法。然后我写了个 Square(正方形)类继承它。正方形要求宽高相等,所以我重写了 setWidth,让它同时设置宽和高。结果呢?
// 违反 LSP 的示例
public class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
}
public class Square extends Rectangle {
public void setWidth(int w) {
width = w;
height = w; // 强制宽高相等
}
public void setHeight(int h) {
height = h;
width = h; // 强制宽高相等
}
}
// 客户端代码
public void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
// 期望:width=5, height=10
// 如果传入 Square:width=10, height=10 —— 行为变了!
}
你看,传入 Rectangle 时行为正常,传入 Square 时行为就变了。这就是违反了 LSP。子类没有保持父类的行为契约。
避坑指南:我曾经以为继承就是“is-a”关系,但后来发现“is-a”不够。正方形“is-a”矩形没错,但行为上正方形不能随意改变宽高而不影响高。所以,继承不仅要考虑语义,还要考虑行为契约。
正确的做法是:让 Rectangle 和 Square 都继承自一个更抽象的 Shape 类,或者干脆用组合代替继承。比如:
// 符合 LSP 的写法
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width, height;
public Rectangle(int w, int h) { width = w; height = h; }
public int getArea() { return width * height; }
}
public class Square extends Shape {
private int side;
public Square(int s) { side = s; }
public int getArea() { return side * side; }
}
这样,不管用 Rectangle 还是 Square,调用 getArea 的行为都是一致的——计算面积。没有 setter 的副作用,就不会出问题。
判断方法:如果你在写子类时,需要重写父类的方法并且改变其行为,或者需要抛出新的异常,那很可能违反了 LSP。问问自己:这个子类真的能完全替代父类吗?
2.4 三个原则的关系
这三个原则不是孤立的。SRP 让每个类职责清晰,为 OCP 打下基础——职责单一的类更容易被抽象和扩展。LSP 则保证了继承体系的正确性,让 OCP 的扩展不会出问题。
我个人的习惯是:先按 SRP 把职责拆清楚,然后用接口或抽象类定义稳定的契约(OCP),最后在实现继承时严格遵循 LSP。这三步走下来,代码质量基本不会差。
| 原则 | 核心问题 | 解决什么 | 常见违反表现 |
|---|---|---|---|
| SRP | 一个类该做几件事? | 降低变更影响范围 | 类中混合多种职责 |
| OCP | 如何加新功能不改旧代码? | 提高系统可扩展性 | 大量 if-else 或 switch |
| LSP | 子类能安全替换父类吗? | 保证继承的正确性 | 子类改变父类行为契约 |
好了,这三个原则就先聊到这儿。记住:SRP 管职责,OCP 管扩展,LSP 管继承。下次写代码的时候,不妨拿这三个原则过一遍,你会发现代码质量有明显提升。
公众号:蓝海资料掘金营,微信deep3321