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