跳转到内容

通用设计原则

本文整理一组通用设计原则:它们不限于面向对象,常与 SOLID、设计模式一起,共同支撑可复用、可扩展的代码。内容主要围绕三点:封装变化的内容面向接口开发组合优于继承


在谈具体原则前,先看我们通常希望设计具备哪些特征:

  • 代码复用
    • 最底层:类、类库、容器
    • 最高层:框架
    • 中间层:设计原则与设计模式(承上启下,指导如何拆分类、如何抽象)
  • 扩展性
    • 主观因素:性能与结构优化
    • 外部因素:技术栈演进、需求变更

通用设计原则正是为了提升复用扩展能力,减少“改一处动全身”和“难以测试、难以替换”的问题。


思路:把会变化的部分隔离出来,让变化局部化,避免散落各处。

  • 将「可能随需求或配置而变」的逻辑抽成独立方法,通过参数或策略控制行为,而不是在流程里写死分支。
  • 例如:算法步骤中的“比较规则”“计算方式”抽成方法或可替换的实现,便于单测和扩展。
  • 将「变化维度」拆成独立类,通过组合或多态切换不同实现,而不是在一个类里用大量 if/switch。
  • 例如:把“如何持久化”“如何发送通知”从业务类中抽离,让业务类依赖抽象,具体存储或通知方式由不同类实现。

实践要点:先识别“什么在变”,再决定是方法级封装即可,还是需要上升到类/接口的抽象。


核心:依赖接口或抽象类,而不是依赖具体实现类,从而降低耦合、便于替换与测试。

  1. 确定一个对象对另一对象的确切需求
    列出“调用方真正用到的能力”,而不是“被依赖类现有的全部方法”。
  2. 在新的接口或抽象类中描述这些方法
    用最小、稳定的契约表达上述需求。
  3. 让被依赖的类实现该接口
    具体实现类实现该接口,保持对外契约一致。
  4. 让有需求的类依赖于接口,而不依赖于具体类
    调用方只依赖接口类型,运行时注入具体实现(构造、setter 或容器注入)。

这样,替换实现(如换一种存储、换一个算法)时,只需换实现类,调用方无需修改。

  • 接口要小且稳定,避免“大而全”的接口倒逼调用方依赖不需要的方法(可结合接口隔离原则 ISP)。
  • 与依赖倒置原则(DIP)一致:高层模块不依赖低层模块的实现,都依赖抽象。

核心:在“复用行为/扩展能力”时,优先用组合(持有其他对象并委托),慎用继承,以降低耦合、避免继承带来的各种限制。

  1. 子类不能减少超类的接口
    子类会继承所有 public 方法,无法隐藏父类中不需要的能力,容易违反接口隔离。
  2. 重写方法时必须与基类行为兼容
    否则会破坏里氏替换(LSP),调用方在“按父类使用”时期望的契约可能被打破。
  3. 继承打破超类的封装
    子类可访问父类的 protected 成员,父类内部细节一旦变化,可能波及子类。
  4. 子类与超类紧密耦合
    父类改动(方法签名、行为、构造方式)容易导致子类连锁修改或编译失败。
  5. 通过继承复用代码可能导致平行继承体系
    为给 A 的多个子类各自加一种“能力”,往往要再为“能力”建一整套子类,导致类层次膨胀、难以维护。
  • 优先组合:把“可替换的能力”做成接口,当前类持有一个该接口的引用,通过委托调用;需要新行为时,换一个实现类或新对象即可,不必新建子类。
  • 继承用于“是一个”的严格 is-a 关系:当子类在语义上就是父类的一种,且不会削弱契约、不需要隐藏父类方法时,再考虑继承。
  • 很多设计模式(如策略、状态、装饰器)都是用组合 + 接口替代继承,达到扩展行为的目的。

原则要解决的问题要点
封装变化的内容变化散落、难以修改和扩展方法级或类级隔离“会变”的部分,使变更局部化
面向接口开发依赖具体实现、难以替换和测试依赖接口/抽象,按最小需求定义契约,实现可替换
组合优于继承继承带来的耦合、契约与封装问题优先用组合+接口复用行为,继承仅用于真正的 is-a 且不破坏契约

这三条与 SOLID、设计模式一起使用,能更好地实现高内聚、低耦合、易复用、易扩展的设计;在阅读和编写代码时,可以有意识地从“哪里在变”“依赖的是抽象还是具体”“这里是继承更合适还是组合更合适”三个角度做检查。