通用设计原则
本文整理一组通用设计原则:它们不限于面向对象,常与 SOLID、设计模式一起,共同支撑可复用、可扩展的代码。内容主要围绕三点:封装变化的内容、面向接口开发、组合优于继承。
优秀设计的特征
Section titled “优秀设计的特征”在谈具体原则前,先看我们通常希望设计具备哪些特征:
- 代码复用
- 最底层:类、类库、容器
- 最高层:框架
- 中间层:设计原则与设计模式(承上启下,指导如何拆分类、如何抽象)
- 扩展性
- 主观因素:性能与结构优化
- 外部因素:技术栈演进、需求变更
通用设计原则正是为了提升复用与扩展能力,减少“改一处动全身”和“难以测试、难以替换”的问题。
一、封装变化的内容
Section titled “一、封装变化的内容”思路:把会变化的部分隔离出来,让变化局部化,避免散落各处。
方法层面的封装
Section titled “方法层面的封装”- 将「可能随需求或配置而变」的逻辑抽成独立方法,通过参数或策略控制行为,而不是在流程里写死分支。
- 例如:算法步骤中的“比较规则”“计算方式”抽成方法或可替换的实现,便于单测和扩展。
类层面的封装
Section titled “类层面的封装”- 将「变化维度」拆成独立类,通过组合或多态切换不同实现,而不是在一个类里用大量 if/switch。
- 例如:把“如何持久化”“如何发送通知”从业务类中抽离,让业务类依赖抽象,具体存储或通知方式由不同类实现。
实践要点:先识别“什么在变”,再决定是方法级封装即可,还是需要上升到类/接口的抽象。
二、面向接口开发
Section titled “二、面向接口开发”核心:依赖接口或抽象类,而不是依赖具体实现类,从而降低耦合、便于替换与测试。
实施步骤(概要)
Section titled “实施步骤(概要)”- 确定一个对象对另一对象的确切需求
列出“调用方真正用到的能力”,而不是“被依赖类现有的全部方法”。 - 在新的接口或抽象类中描述这些方法
用最小、稳定的契约表达上述需求。 - 让被依赖的类实现该接口
具体实现类实现该接口,保持对外契约一致。 - 让有需求的类依赖于接口,而不依赖于具体类
调用方只依赖接口类型,运行时注入具体实现(构造、setter 或容器注入)。
这样,替换实现(如换一种存储、换一个算法)时,只需换实现类,调用方无需修改。
- 接口要小且稳定,避免“大而全”的接口倒逼调用方依赖不需要的方法(可结合接口隔离原则 ISP)。
- 与依赖倒置原则(DIP)一致:高层模块不依赖低层模块的实现,都依赖抽象。
三、组合优于继承
Section titled “三、组合优于继承”核心:在“复用行为/扩展能力”时,优先用组合(持有其他对象并委托),慎用继承,以降低耦合、避免继承带来的各种限制。
继承带来的问题
Section titled “继承带来的问题”- 子类不能减少超类的接口
子类会继承所有 public 方法,无法隐藏父类中不需要的能力,容易违反接口隔离。 - 重写方法时必须与基类行为兼容
否则会破坏里氏替换(LSP),调用方在“按父类使用”时期望的契约可能被打破。 - 继承打破超类的封装
子类可访问父类的 protected 成员,父类内部细节一旦变化,可能波及子类。 - 子类与超类紧密耦合
父类改动(方法签名、行为、构造方式)容易导致子类连锁修改或编译失败。 - 通过继承复用代码可能导致平行继承体系
为给 A 的多个子类各自加一种“能力”,往往要再为“能力”建一整套子类,导致类层次膨胀、难以维护。
- 优先组合:把“可替换的能力”做成接口,当前类持有一个该接口的引用,通过委托调用;需要新行为时,换一个实现类或新对象即可,不必新建子类。
- 继承用于“是一个”的严格 is-a 关系:当子类在语义上就是父类的一种,且不会削弱契约、不需要隐藏父类方法时,再考虑继承。
- 很多设计模式(如策略、状态、装饰器)都是用组合 + 接口替代继承,达到扩展行为的目的。
| 原则 | 要解决的问题 | 要点 |
|---|---|---|
| 封装变化的内容 | 变化散落、难以修改和扩展 | 方法级或类级隔离“会变”的部分,使变更局部化 |
| 面向接口开发 | 依赖具体实现、难以替换和测试 | 依赖接口/抽象,按最小需求定义契约,实现可替换 |
| 组合优于继承 | 继承带来的耦合、契约与封装问题 | 优先用组合+接口复用行为,继承仅用于真正的 is-a 且不破坏契约 |
这三条与 SOLID、设计模式一起使用,能更好地实现高内聚、低耦合、易复用、易扩展的设计;在阅读和编写代码时,可以有意识地从“哪里在变”“依赖的是抽象还是具体”“这里是继承更合适还是组合更合适”三个角度做检查。