跳转到内容

六大设计原则

单一职责原则 (Single Responsibility Principle, SRP):一个类应该只有一个引起它变化的原因,即只负责一项职责。

当违反单一职责时,容易出现:

  • 为了用很少的代码实现很复杂的设计,需要费尽心思。
  • 当程序规模不断扩大、变更不断增加后,类会变得很庞大,导致很难记住里面的细节。
  • 当类负责的东西太多,来了新需求,可能需要对类进行修改,影响面难以控制。
  • 按职责拆分类,让每个类只做一件事。
  • 若一个类因多种原因被修改(例如既因报表格式又因业务规则而改),考虑拆成多个类。
  • 命名是很好的信号:若类名需要用「和」「与」「及」才能说清,往往已承担多重职责。

开闭原则 (Open-Closed Principle, OCP)

  • 对扩展开放:可以对一个类进行扩展,可以创建它的子类并对其做任何事情(如新增方法或成员变量、重写基类行为等)。
  • 对修改封闭:类已做好了充分的准备并可供其他类使用(即接口已明确定义且以后不会修改)。

通过抽象与多态增加新行为,而不是修改已有类的实现。新需求尽量用新子类、新实现来满足,避免改动稳定、已在用的代码,从而降低回归风险。

  • 用抽象(接口、抽象类)定义稳定扩展点。
  • 新功能通过实现新子类或新实现类完成,而不是在原有类上堆逻辑。
  • 对「会因需求而变」的部分做抽象,对「相对稳定」的部分保持封闭。

里氏替换原则 (Liskov Substitution Principle, LSP):子类型必须能够替换其基类型,且不破坏程序的正确性。

  • 参数类型:子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象
  • 返回值类型:子类方法的返回值类型必须与超类方法的返回值类型或是其子类型相匹配。
  • 异常:子类中的方法不应抛出基础方法预期之外的异常类型。
  • 前置条件与后置条件:子类不应该加强其前置条件;子类不能削弱其后置条件。
  • 不变量:超类的不变量必须保留。

遵守 LSP 能保证多态与继承被安全使用:凡是依赖父类/接口的代码,换成任意正确实现的子类,行为仍符合预期,不会出现「子类替换后悄悄出错」的问题。


接口隔离原则 (Interface Segregation Principle, ISP)

  • 客户端不应被迫依赖于其不使用的方法。
  • 尽量缩小接口的范围,使得客户端的类不必实现其不需要的行为。

若接口过于庞大(例如一个「上帝接口」包含很多方法),则:

  • 实现类被迫实现大量用不到的方法(空实现或抛异常)。
  • 客户端依赖了整份接口,耦合高,且难以演进。
  • 接口任一方法变更都可能影响所有实现与调用方。
  • 使用方/角色拆分接口,让每个接口只包含某一类客户端真正需要的方法。
  • 宁可多个小接口,也不要一个包含所有方法的大接口。
  • 实现类可以实现多个小接口,组合出所需能力,避免「为了一两个方法实现一整块大接口」。

依赖倒置原则 (Dependency Inversion Principle, DIP)

  • 高层模块不应该依赖低层模块,两者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。
  • 高层模块:业务逻辑、流程编排等。
  • 低层模块:具体实现(如数据库访问、文件 IO、第三方服务封装等)。
  • 抽象:接口、抽象类等稳定契约。

高层通过抽象调用能力,低层实现抽象;这样高层不直接依赖具体实现,低层也可以替换(例如换数据库、换实现)而不影响高层。

  • 定义稳定的接口/抽象类,由高层依赖这些抽象。
  • 具体实现(细节)实现抽象并注入到高层,而不是高层直接 new 具体类。
  • 依赖注入(构造函数、属性、容器)是落实 DIP 的常见手段;面向接口编程是 DIP 的直接体现。

迪米特法则 (Law of Demeter):一个对象应该对其他对象有最少的了解

即:只与直接朋友通信,不要与「朋友的朋友」产生依赖;减少对内部实现与对象链的暴露。

  • 降低耦合:模块/类之间尽量通过稳定、必要的接口交互,不依赖过多内部细节。
  • 减少传播式修改:若 A 依赖 B 的内部结构,B 一改可能波及 A;遵守迪米特法则可缩小变更影响面。
  • 提高可读性与可维护性:调用链短、依赖关系清晰,代码更易理解和修改。
  • 类应尽量只调用自身、成员对象、方法参数、以及自身创建的对象的方法,避免「链式穿透」一长串 getter(如 a.getB().getC().doSomething())。
  • 必要时引入门面/中介,由某个对象统一对外提供能力,而不是让调用方直接接触多个内部对象。
  • 与「高内聚、低耦合」一致:对外暴露最少必要信息,内部实现可自由演进。