Skip to content

程序设计原则

好的,程序设计原则是编写高质量、可维护、可扩展软件的基础。它们指导我们如何组织代码(类、包/模块)以及它们之间的关系。下面分别介绍类级别和包级别的主要设计原则:

一、类级别的设计原则 (SOLID + 其他关键原则)

这些原则主要关注单个类内部的职责以及类与类之间如何交互。

  1. 单一职责原则(SRP - Single Responsibility Principle)

    • 核心思想: 一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责。
    • 为什么重要: 职责越单一,类就越内聚,越容易被理解、修改和测试。修改一个职责不会意外影响其他不相关的功能。
    • 违反示例: 一个Order类既负责处理订单数据(如计算总额),又负责将订单保存到数据库,还负责发送订单确认邮件。
  2. 开闭原则 (OCP - Open/Close Principle)

    • 核心思想: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
    • 为什么重要: 允许在不修改现有代码的情况下添加新功能,提高了系统的稳定性和可扩展性,降低了引入新错误的风险。
    • 如何实现: 通常通过抽象(接口、抽象类)和依赖注入来实现。新功能通过实现新的具体类来添加,而不是修改已有的类。
    • 违反示例: 添加一个新的支付方式(如PayPal)需要修改现有的PaymentProcessor类的代码。
  3. 里氏替换原则 (LSP - Liskov Substitution Principle)

    • 核心思想: 子类型必须能够替换掉它们的基类型,而程序的行为不会发生改变。即任何基类出现的地方,子类都可以出现。
    • 为什么重要: 保证继承关系的正确性,确保多态行为符合预期。子类不应该破坏父类的契约(前置条件、后置条件、不变式)。
    • 违反示例: 正方形类继承自长方形类。长方形有SetWidthSetHeight方法可以独立设置。但正方形要求宽高相等,覆盖这些方法强制宽高一致。当一段期望独立设置宽高的代码(适用于长方形)接收到一个正方形对象时,行为会出错(违反了长方形的契约)。
  4. 接口隔离原则 (ISP - Interface Segregation Principle)

    • 核心思想: 客户端不应该被迫依赖于它们不使用的接口。多个特定功能的接口比一个庞大臃肿的总接口要好。
    • 为什么重要: 避免客户端依赖不需要的方法,减少耦合,提高系统的灵活性和可维护性。防止接口污染。
    • 违反示例: 一个庞大的Worker接口包含了Work, Eat, Sleep方法。Robot类实现了Worker,但它不需要EatSleep,被迫提供了空实现或抛出异常。
  5. 依赖倒置原则 (DIP - Dependency Inversion Principle)

    • 核心思想:
      • 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
      • 抽象不应该依赖于细节。细节应该依赖于抽象。
    • 为什么重要: 解耦高层业务逻辑与底层实现细节(如数据库访问、网络通信)。提高系统的灵活性、可测试性(更容易Mock)和可维护性。
    • 如何实现: 高层模块通过接口(抽象)定义它需要的服务,低层模块实现这些接口。依赖关系通过构造函数、Setter方法或依赖注入容器注入。
    • 违反示例: ReportGenerator类直接实例化并调用MySQLDatabase类的具体方法。
  6. 迪米特法则 (Law of Demeter, The Least Knowledge Principle)

    • 核心思想: 一个对象应该对其他对象有最少的了解。只与你的“直接朋友”交谈,不要跟“陌生人”说话。
    • 核心规则:
      • 只调用自身的方法。
      • 只调用传入参数的方法。
      • 只调用自身创建的对象的方法。
      • 只调用自身持有的成员对象的方法。
    • 为什么重要: 降低类之间的耦合度,提高模块化程度。一个类的修改不会轻易传播到大量其他类。
    • 违反示例: Customer对象通过Order对象拿到Item对象,再直接调用Item.GetPrice()Customer应该只与Order交互,由Order提供计算总价等方法(Order.GetTotalPrice()),Order内部再与Item交互。
  7. 组合优于继承原则(Composition Over Inheritance Principle)

    • 核心思想: 在代码复用时,优先考虑使用对象组合(has-a关系),而不是类继承(is-a关系)。
    • 为什么重要: 继承在编译时就固定了关系,可能导致脆弱的基类问题(父类修改影响所有子类),并且限制了灵活性。组合在运行时建立关系,更灵活,更容易改变行为(通过替换组件),更符合松耦合原则。
    • 何时用继承: 当两个类之间确实是严格的“是一种”关系,并且子类确实是父类的特化,且需要利用多态时。
    • 示例: 给一个Car类添加日志功能,应通过组合一个Logger对象(Car has a Logger),而不是创建一个LoggableCar父类。

二、包/模块级别的设计原则 (REP, CCP, CRP, ADP, SDP, SAP)

这些原则(主要源自Robert C. Martin的《敏捷软件开发:原则、模式与实践》)关注如何将相关的类组织成包(Java)/模块(如Python, ES6, .NET)/命名空间(C++),以及包与包之间应该如何耦合。

  1. 重用发布等价原则(Reuse-Release Equivalence Principle - REP)

    • 核心思想: 重用的粒度就是发布的粒度。一个包(模块)中的所有类应该是内聚的、可作为一个整体单元被重用的。它们应该属于同一个重用范畴。
    • 为什么重要: 确保包是可独立发布和版本控制的单元。包内的类应该是为了共同的重用目的而聚集在一起的。
    • 实践: 如果一个包会被其他项目复用,那么包内的所有类都应该一起被复用。避免将不相关或不同重用频率的类塞进同一个包。
  2. 共同封闭原则(Common Closure Principle - CCP)

    • 核心思想: 包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则对该包中的所有类产生影响,而对于其他包则不造成任何影响。
    • 为什么重要: 提高可维护性。将可能因相同原因(如需求变更、技术栈升级)而需要一起修改的类放在同一个包中。这样,修改可以局限在一个包内,减少影响范围。
    • 实践: 将服务于同一功能域或业务能力的类放在一起。例如,所有与“用户管理”相关的类(User, UserRepository, UserService, UserController)放入com.example.user包。
  3. 共同重用原则 (Common Reuse Principle - CRP)

    • 核心思想: 一个包中的所有类应该是被一起重用的。如果重用了包中的一个类,就应该重用包中的所有类。
    • 为什么重要: 避免使用者被迫依赖他们不需要的类,减少不必要的依赖和编译/部署负担。防止包内存在无关紧要的类。
    • 实践: 如果一个包中的某些类很少被一起使用,或者使用者只需要其中一部分类,就应该考虑将这些类拆分到不同的包中。CRP 和 CCP 经常需要权衡:CCP 倾向于把一起变化的类放一起(利于修改),CRP 倾向于把一起使用的类放一起(利于重用)。好的设计需要平衡这两者。
  4. 无环依赖原则 (Acyclic Dependencies Principle - ADP)

    • 核心思想: 在包的依赖关系图中不应该存在环(循环依赖)。包的依赖结构必须是一个有向无环图。
    • 为什么重要: 循环依赖会导致:
      • 编译/构建困难: 需要同时编译或特殊处理。
      • 部署困难: 需要同时部署。
      • 理解困难: 包之间高度耦合,难以独立修改。
      • 单元测试困难: 难以隔离测试。
    • 如何解决: 应用依赖倒置原则(DIP),引入接口/抽象层;重构代码,将引起循环的类提取到新包(可能是共同依赖的包);使用事件、回调等机制解耦。
  5. 稳定依赖原则 (Stable Dependencies Principle - SDP)

    • 核心思想: 朝着稳定的方向进行依赖。一个包应该只依赖于比它更稳定(更不容易改变)的包。
    • 为什么重要: 不稳定的包(经常变化的包)如果被许多其他包依赖,那么它的任何修改都会产生广泛的涟漪效应,导致系统脆弱。SDP 将易变性隔离在不稳定的包中,保护稳定的核心部分。
    • 衡量稳定性: 可以用不稳定性 I = Fan-out / (Fan-in + Fan-out) 来衡量(Fan-out:依赖的外部包数量,Fan-in:依赖此包的包数量)。I=0 表示非常稳定(很多包依赖它,它不依赖别人),I=1 表示非常不稳定(它依赖很多包,没有包依赖它)。SDP 要求 I 值应该沿着依赖方向递减。
  6. 稳定抽象原则 (Stable Abstractions Principle - SAP)

    • 核心思想: 一个包的抽象程度应该与其稳定程度一致。稳定的包(高 Fan-in)应该是抽象的包(包含接口、抽象类);不稳定的包(高 Fan-out)应该是具体的包(包含具体实现)。
    • 为什么重要: 稳定包(不易改变)如果是抽象的(接口),那么依赖它的包只依赖于抽象契约,具体实现可以灵活变化(甚至替换),这符合开闭原则(OCP)。如果稳定包是具体的,那么它的任何具体实现的修改都会强制依赖它的包重新编译/修改,违背了OCP。
    • 衡量抽象性: 可以用抽象性 A = (#抽象类+ #接口) / (#类总数) 来衡量。SAP 要求稳定包(低 I)应该有高 A(抽象),不稳定包(高 I)可以有低 A(具体)。理想情况下,AI 应该形成一个平衡(如 A + I ≈ 1),即主序列(Main Sequence)。

包原则总结表

原则名称缩写核心关注点关键目标
重用发布等价原则REP包内类的重用性包是可独立发布、重用的单元
共同封闭原则CCP包内类的可维护性(变化)将一起变化的类放在一起,减少修改影响
共同重用原则CRP包内类的可重用性(使用)避免使用者依赖不需要的类
无环依赖原则ADP包间依赖结构消除循环依赖,构建有向无环图
稳定依赖原则SDP依赖方向依赖稳定的包,隔离不稳定变化
稳定抽象原则SAP包的稳定性与抽象程度的关系稳定包应是抽象的,便于扩展

总结

  • 类级别原则 (SOLID + LoD + 组合) 指导如何设计健壮、灵活、可维护的单个类以及类间关系。
  • 包/模块级别原则 (REP, CCP, CRP, ADP, SDP, SAP) 指导如何将类组织成更大的、内聚的、可管理的单元,并管理这些单元之间的依赖关系,确保整个系统架构的稳定性和可维护性。
  • 这些原则相互关联,共同作用。例如,遵循 DIP 有助于实现 ADP 和 SDP;遵循 SRP 有助于更好地应用 REP、CCP、CRP 进行包划分。
  • 应用这些原则需要权衡和判断,没有绝对完美的方案。理解原则背后的目标(降低耦合、提高内聚、增强灵活性、可维护性、可扩展性)比机械套用更重要。结合具体的项目需求、规模和上下文来应用这些原则是关键。