程序设计原则
好的,程序设计原则是编写高质量、可维护、可扩展软件的基础。它们指导我们如何组织代码(类、包/模块)以及它们之间的关系。下面分别介绍类级别和包级别的主要设计原则:
一、类级别的设计原则 (SOLID + 其他关键原则)
这些原则主要关注单个类内部的职责以及类与类之间如何交互。
单一职责原则(SRP - Single Responsibility Principle)
- 核心思想: 一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责。
- 为什么重要: 职责越单一,类就越内聚,越容易被理解、修改和测试。修改一个职责不会意外影响其他不相关的功能。
- 违反示例: 一个
Order
类既负责处理订单数据(如计算总额),又负责将订单保存到数据库,还负责发送订单确认邮件。
开闭原则 (OCP - Open/Close Principle)
- 核心思想: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 为什么重要: 允许在不修改现有代码的情况下添加新功能,提高了系统的稳定性和可扩展性,降低了引入新错误的风险。
- 如何实现: 通常通过抽象(接口、抽象类)和依赖注入来实现。新功能通过实现新的具体类来添加,而不是修改已有的类。
- 违反示例: 添加一个新的支付方式(如PayPal)需要修改现有的
PaymentProcessor
类的代码。
里氏替换原则 (LSP - Liskov Substitution Principle)
- 核心思想: 子类型必须能够替换掉它们的基类型,而程序的行为不会发生改变。即任何基类出现的地方,子类都可以出现。
- 为什么重要: 保证继承关系的正确性,确保多态行为符合预期。子类不应该破坏父类的契约(前置条件、后置条件、不变式)。
- 违反示例: 正方形类继承自长方形类。长方形有
SetWidth
和SetHeight
方法可以独立设置。但正方形要求宽高相等,覆盖这些方法强制宽高一致。当一段期望独立设置宽高的代码(适用于长方形)接收到一个正方形对象时,行为会出错(违反了长方形的契约)。
接口隔离原则 (ISP - Interface Segregation Principle)
- 核心思想: 客户端不应该被迫依赖于它们不使用的接口。多个特定功能的接口比一个庞大臃肿的总接口要好。
- 为什么重要: 避免客户端依赖不需要的方法,减少耦合,提高系统的灵活性和可维护性。防止接口污染。
- 违反示例: 一个庞大的
Worker
接口包含了Work
,Eat
,Sleep
方法。Robot
类实现了Worker
,但它不需要Eat
和Sleep
,被迫提供了空实现或抛出异常。
依赖倒置原则 (DIP - Dependency Inversion Principle)
- 核心思想:
- 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象。
- 为什么重要: 解耦高层业务逻辑与底层实现细节(如数据库访问、网络通信)。提高系统的灵活性、可测试性(更容易Mock)和可维护性。
- 如何实现: 高层模块通过接口(抽象)定义它需要的服务,低层模块实现这些接口。依赖关系通过构造函数、Setter方法或依赖注入容器注入。
- 违反示例:
ReportGenerator
类直接实例化并调用MySQLDatabase
类的具体方法。
- 核心思想:
迪米特法则 (Law of Demeter, The Least Knowledge Principle)
- 核心思想: 一个对象应该对其他对象有最少的了解。只与你的“直接朋友”交谈,不要跟“陌生人”说话。
- 核心规则:
- 只调用自身的方法。
- 只调用传入参数的方法。
- 只调用自身创建的对象的方法。
- 只调用自身持有的成员对象的方法。
- 为什么重要: 降低类之间的耦合度,提高模块化程度。一个类的修改不会轻易传播到大量其他类。
- 违反示例:
Customer
对象通过Order
对象拿到Item
对象,再直接调用Item.GetPrice()
。Customer
应该只与Order
交互,由Order
提供计算总价等方法(Order.GetTotalPrice()
),Order
内部再与Item
交互。
组合优于继承原则(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++),以及包与包之间应该如何耦合。
重用发布等价原则(Reuse-Release Equivalence Principle - REP)
- 核心思想: 重用的粒度就是发布的粒度。一个包(模块)中的所有类应该是内聚的、可作为一个整体单元被重用的。它们应该属于同一个重用范畴。
- 为什么重要: 确保包是可独立发布和版本控制的单元。包内的类应该是为了共同的重用目的而聚集在一起的。
- 实践: 如果一个包会被其他项目复用,那么包内的所有类都应该一起被复用。避免将不相关或不同重用频率的类塞进同一个包。
共同封闭原则(Common Closure Principle - CCP)
- 核心思想: 包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则对该包中的所有类产生影响,而对于其他包则不造成任何影响。
- 为什么重要: 提高可维护性。将可能因相同原因(如需求变更、技术栈升级)而需要一起修改的类放在同一个包中。这样,修改可以局限在一个包内,减少影响范围。
- 实践: 将服务于同一功能域或业务能力的类放在一起。例如,所有与“用户管理”相关的类(
User
,UserRepository
,UserService
,UserController
)放入com.example.user
包。
共同重用原则 (Common Reuse Principle - CRP)
- 核心思想: 一个包中的所有类应该是被一起重用的。如果重用了包中的一个类,就应该重用包中的所有类。
- 为什么重要: 避免使用者被迫依赖他们不需要的类,减少不必要的依赖和编译/部署负担。防止包内存在无关紧要的类。
- 实践: 如果一个包中的某些类很少被一起使用,或者使用者只需要其中一部分类,就应该考虑将这些类拆分到不同的包中。CRP 和 CCP 经常需要权衡:CCP 倾向于把一起变化的类放一起(利于修改),CRP 倾向于把一起使用的类放一起(利于重用)。好的设计需要平衡这两者。
无环依赖原则 (Acyclic Dependencies Principle - ADP)
- 核心思想: 在包的依赖关系图中不应该存在环(循环依赖)。包的依赖结构必须是一个有向无环图。
- 为什么重要: 循环依赖会导致:
- 编译/构建困难: 需要同时编译或特殊处理。
- 部署困难: 需要同时部署。
- 理解困难: 包之间高度耦合,难以独立修改。
- 单元测试困难: 难以隔离测试。
- 如何解决: 应用依赖倒置原则(DIP),引入接口/抽象层;重构代码,将引起循环的类提取到新包(可能是共同依赖的包);使用事件、回调等机制解耦。
稳定依赖原则 (Stable Dependencies Principle - SDP)
- 核心思想: 朝着稳定的方向进行依赖。一个包应该只依赖于比它更稳定(更不容易改变)的包。
- 为什么重要: 不稳定的包(经常变化的包)如果被许多其他包依赖,那么它的任何修改都会产生广泛的涟漪效应,导致系统脆弱。SDP 将易变性隔离在不稳定的包中,保护稳定的核心部分。
- 衡量稳定性: 可以用不稳定性
I = Fan-out / (Fan-in + Fan-out)
来衡量(Fan-out:依赖的外部包数量,Fan-in:依赖此包的包数量)。I=0
表示非常稳定(很多包依赖它,它不依赖别人),I=1
表示非常不稳定(它依赖很多包,没有包依赖它)。SDP 要求I
值应该沿着依赖方向递减。
稳定抽象原则 (Stable Abstractions Principle - SAP)
- 核心思想: 一个包的抽象程度应该与其稳定程度一致。稳定的包(高 Fan-in)应该是抽象的包(包含接口、抽象类);不稳定的包(高 Fan-out)应该是具体的包(包含具体实现)。
- 为什么重要: 稳定包(不易改变)如果是抽象的(接口),那么依赖它的包只依赖于抽象契约,具体实现可以灵活变化(甚至替换),这符合开闭原则(OCP)。如果稳定包是具体的,那么它的任何具体实现的修改都会强制依赖它的包重新编译/修改,违背了OCP。
- 衡量抽象性: 可以用抽象性
A = (#抽象类+ #接口) / (#类总数)
来衡量。SAP 要求稳定包(低I
)应该有高A
(抽象),不稳定包(高I
)可以有低A
(具体)。理想情况下,A
和I
应该形成一个平衡(如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 进行包划分。
- 应用这些原则需要权衡和判断,没有绝对完美的方案。理解原则背后的目标(降低耦合、提高内聚、增强灵活性、可维护性、可扩展性)比机械套用更重要。结合具体的项目需求、规模和上下文来应用这些原则是关键。