详细设计
AI WARNING
未进行对照审核。
详细设计概述
什么是详细设计
详细设计(Detail Design)是软件开发过程中的一个关键阶段,它承接软件体系结构设计,并为后续的编码实现提供具体指导。它关注的是将体系结构中定义的模块或组件进一步细化,明确其内部实现机制。
详细设计
详细设计的主要任务是将宏观的架构蓝图转化为微观的实现细节,包括确定类/对象的具体职责、它们之间的交互方式、数据结构和算法的选择等。
详细设计通常包含两个层面:
- 中层设计:针对特定的模块,进行面向对象的设计,定义模块内部包含的主要类及其规格说明(接口、属性、主要方法)。
- 低层设计:针对具体的类,设计其内部实现,包括选择合适的数据结构(DS)和算法(ALG),明确方法的实现逻辑、控制流等。
graph TB
A[软件体系结构设计] --> B;
B --> C[编码实现];
subgraph B[详细设计]
direction LR
B1(中层设计) -- 定义 --> B2(类/对象规格);
B2 -- 细化 --> B3(低层设计);
B3 -- 包含 --> B4(数据结构 + 算法);
end
style A fill:#ccf,stroke:#333,stroke-width:2px
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#9cf,stroke:#333,stroke-width:2px
style B1 fill:#ffc,stroke:#333,stroke-width:1px
style B2 fill:#ffc,stroke:#333,stroke-width:1px
style B3 fill:#ffc,stroke:#333,stroke-width:1px
style B4 fill:#ffc,stroke:#333,stroke-width:1px
详细设计的上下文与输入输出
详细设计不是凭空进行的,它建立在前期工作的基础上,并为后续工作提供输入。
-
输入:
- 需求规格说明:尤其是功能性需求和非功能性需求(如性能、可维护性),它们是设计的最终目标。分析阶段产生的用例、领域模型、系统顺序图、状态图等也是重要输入。
- 软件体系结构设计:定义了系统的宏观结构、模块划分、模块规格以及模块间的接口。这是详细设计的直接出发点。
- 实现决策:可能来自技术选型、平台限制等。
-
输出:
- 详细设计模型:通常使用 UML 类图、顺序图、状态图等来精确描述类的结构、职责、关系和交互。
- 详细设计规格说明:对类、接口、方法、数据结构、算法等的文字描述和注解。
- 集成测试用例:基于类间协作的设计,可以开发相应的测试用例。
- 详细设计文档:汇总上述所有设计结果,作为编码和评审的依据。
关注点
详细设计不仅要考虑功能的正确实现,还需要关注软件的质量属性,如可修改性、可维护性、性能、可复用性、可理解性 等。这些属性的权衡是设计过程中的重要考量。
面向对象详细设计的核心思想
面向对象设计(Object-Oriented Design, OOD)的核心在于如何将系统的功能合理地分配给对象,并定义它们之间的协作方式。两个基本概念是职责和协作。
职责
职责
职责是一个类或对象所承担的义务,它可以是执行某项任务(行为职责)或维护某些数据(数据职责)。
- 行为职责:通常由类的方法来实现。例如,「计算订单总价」是一个行为职责。
- 数据职责:通常由类的属性来实现。例如,「维护订单包含的商品列表」是一个数据职责。
职责驱动分解:面向对象设计常常以职责为驱动力来进行系统分解。
- 职责可以在不同的抽象层次上描述,并且可以被分解。
- 高层职责分配给高层组件(模块),然后进一步分解并将子职责分配给内部的类或对象。
- 这种分解方式与纯粹的功能分解不同,因为它同时考虑了数据和行为的归属。
职责分配启发式规则:良好的职责分配有助于实现高内聚和低耦合(后续会详细介绍)。
- 确保模块/类的职责不重叠。
- 一个模块/类中的操作和数据应该仅仅是为了帮助其履行自身职责。
- 委托:当一个模块(委托者, Delegator)自身无法完成某个职责时,可以将该职责委托给另一个模块(被委托者, Delegate)来完成。这是一种常见的协作机制。
协作
协作
协作是指对象之间为了完成某个特定行为(通常是实现一个用例或一个较大的职责)而进行的交互与合作。
- 必要性:如果对象间不协作,整个系统要么无法工作,要么就会退化成一个包揽一切的巨大对象,失去面向对象设计的优势。
- 本质:协作体现为对象网络中消息传递的模式。一个特定的系统行为通常由一组对象通过明确的协作模式来实现。
- 分布式:协作逻辑通常分布在参与协作的多个对象中,而不是集中在单一位置。
- 重要性:协作设计直接关系到系统行为的正确性和健壮性。如果协作设计不当,应用程序可能会不准确或变得脆弱。
面向对象详细设计的过程
面向对象详细设计通常遵循一个迭代的过程,主要包括设计模型的建立和重构。
设计模型建立
1. 通过职责建立静态设计模型
静态设计模型主要关注系统的结构,通常使用 UML 类图来表示。
抽象类的职责:
- 识别关键的业务概念,将它们抽象为类。
- 为每个类明确其数据职责(需要维护哪些信息,体现为属性)和行为职责(需要执行哪些操作,体现为方法)。
- 示例:
User
类负责维护用户 ID、姓名、密码(数据职责),并提供验证密码、获取/设置用户名等操作(行为职责)。
classDiagram
class User {
-int userId
-string name
-string password
+validatePassword(password: string): boolean
+getUsername(): string
+setUsername(name: string): void
}
Mermaid 代码
1 | classDiagram |
抽象类之间的关系:
-
识别并定义类之间的静态关系,这有助于理解系统的结构和依赖。
-
常见的关系类型包括:
关系类型 关系短语(示例) 解释 UML 表示(非代码) 多重性(示例) 普通关联 A has a B 对象间存在某种连接,通常表示为实例变量。关系较弱。 ->
(实线开放箭头)A: 0..*
B:0..*
聚合 A owns B has-a 关系,整体与部分。部分可以独立于整体存在,可以被共享。 -<>
(实线空心菱形箭头)A: 0..1
B:0..*
组合 B is a part of A is-part-of 关系,强聚合。部分与整体生命周期绑定,不能被共享。 -<*>
(实线实心菱形箭头)A: 0..1
B:1..1
继承 B is a A is-a 关系,子类继承父类的接口和实现,体现一般与特殊。 -\>
(实线空心三角箭头)无 实现 B implements A 类实现接口定义的操作,强制实现契约。 --\>
(虚线空心三角箭头)无 -
示例:
Sales
类(销售单)聚合了多个SalesLineItem
类(销售项),SalesLineItem
关联到ProductSpecification
类(产品规格)。
classDiagram
Sales "0..1" o-- "0..*" SalesLineItem : 聚合
SalesLineItem "0..*" --> "1" ProductSpecification : 关联
class Sales {
-List~SalesLineItem~ lineItems
+calculateTotal(): float
}
class SalesLineItem {
-int quantity
-float price
+getSubtotal(): float
}
class ProductSpecification {
-string productName
-float unitPrice
+getPrice(): float
}
Mermaid 代码
1 | classDiagram |
添加辅助类:
- 在核心业务类之外,通常需要一些辅助类来完成特定任务或改善设计。
- 常见的辅助类类型:
- 接口类:定义服务契约。
- 记录类/数据类:主要用于封装和传递数据(如 DTO - Data Transfer Object)。
- 启动类:负责系统或模块的初始化。
- 控制器类:协调其他对象完成任务(详见 GRASP Controller)。
- 实现数据类型的类:封装基本数据类型或提供特定数据操作。
- 容器类:管理对象的集合(如 List, Map)。
- 示例:在销售系统中,可能需要
SalesController
来处理用户交互,SalesList
作为SalesLineItem
的容器,SalesPO
(Persistent Object) 或SalesVO
(Value Object) 作为数据传输对象。
classDiagram
SalesController --> Sales : 协调
SalesController --> SalesList : 使用
SalesList "1" -- "0..*" SalesLineItem : 包含
Sales --> SalesPO : 持久化
Sales --> SalesVO : 数据传输
class SalesController {
+processSale(sale: Sale): void
}
class SalesList {
-List~SalesLineItem~ items
+addItem(item: SalesLineItem): void
+removeItem(item: SalesLineItem): void
}
class SalesPO {
-int saleId
-Date saleDate
+saveToDatabase(): void
}
class SalesVO {
-float totalAmount
-Date saleDate
+convertToDTO(): DTO
}
Mermaid 代码
1 | classDiagram |
2. 通过协作建立动态设计模型
动态设计模型关注系统在运行时的行为和对象间的交互,通常使用 UML 顺序图和状态图来表示。
-
抽象对象之间的协作:
-
识别为了完成某个系统行为(如一个用例场景),哪些对象需要参与以及它们如何交互。
-
协作的抽象可以通过两种方式进行:
- 自底向上:将对象的细小职责聚合成更大的职责。
- 自顶向下:将宏观的职责(如用例)分解并分配给具体的对象。
-
顺序图:
- 是表示对象间协作的常用工具。
- 清晰地展示了对象之间消息传递的时间顺序,有助于理解一个行为是如何通过对象交互完成的。
- 示例:计算销售总额的顺序图会展示
SalesController
如何调用Sale
对象的getTotal()
方法,Sale
对象又如何遍历其包含的SalesLineItem
并调用getSubtotal()
,SalesLineItem
再调用ProductSpecification
的getPrice()
等。
sequenceDiagram participant User as 用户 participant SalesController as 销售控制器 participant Sale as 销售单 participant SalesLineItem as 销售项 participant ProductSpecification as 产品规格 User ->> SalesController: 请求计算销售总额 SalesController ->> Sale: 调用 getTotal() Sale ->> SalesLineItem: 遍历调用 getSubtotal() SalesLineItem ->> ProductSpecification: 调用 getPrice() ProductSpecification -->> SalesLineItem: 返回价格 SalesLineItem -->> Sale: 返回小计 Sale -->> SalesController: 返回总金额 SalesController -->> User: 显示总金额
Mermaid 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15sequenceDiagram
participant User as 用户
participant SalesController as 销售控制器
participant Sale as 销售单
participant SalesLineItem as 销售项
participant ProductSpecification as 产品规格
User ->> SalesController: 请求计算销售总额
SalesController ->> Sale: 调用 getTotal()
Sale ->> SalesLineItem: 遍历调用 getSubtotal()
SalesLineItem ->> ProductSpecification: 调用 getPrice()
ProductSpecification -->> SalesLineItem: 返回价格
SalesLineItem -->> Sale: 返回小计
Sale -->> SalesController: 返回总金额
SalesController -->> User: 显示总金额 -
状态图:
- 用于描述一个复杂对象在其生命周期内所经历的状态序列、引起状态转移的事件 以及伴随状态转移的动作。
- 适用于对具有复杂状态行为的对象进行建模。
- 协作体现在:状态图中的 Event 通常对应对象间的消息传递,Action 则体现了消息引发的对象状态改变。
stateDiagram-v2 [*] --> New : 创建订单 New --> Paid : 支付(payOrder) Paid --> Shipped : 发货(shipOrder) Shipped --> Delivered : 完成配送(deliverOrder) Delivered --> [*] : 订单完成
Mermaid 代码
stateDiagram-v2 [*] --> New : 创建订单 New --> Paid : 支付 (payOrder) Paid --> Shipped : 发货 (shipOrder) Shipped --> Delivered : 完成配送 (deliverOrder) Delivered --> [*] : 订单完成
-
-
明确对象的创建:
- 对象在使用前必须被创建。需要明确哪个对象负责创建另一个对象。
- GRASP Creator 模式 提供了指导原则(详见后续 GRASP 部分)。
-
选择合适的控制风格:
- 确定如何组织对象间的协作逻辑,即由谁来主导和控制交互流程。
- GRASP Controller 模式 提供了指导原则(详见后续 GRASP 部分)。
- 不同的控制风格(集中式、委托式、分散式)会影响系统的耦合度和内聚性(详见后续控制风格部分)。
设计模型重构
设计是一个迭代的过程,初步建立的模型往往不是最优的。需要根据设计原则进行重构,以提高质量。
- 根据模块化思想重构:目标是高内聚、低耦合。检查类的职责是否单一、关联是否必要、依赖是否过强等。
- 根据信息隐藏思想重构:目标是隐藏职责与变更。识别系统中可能变化的部分(「秘密」),将其封装在独立的模块或类中,并通过稳定的接口暴露必要信息。
- 利用设计模式重构:应用成熟的设计模式(如 GoF 模式、GRASP 模式)来解决常见的设计问题,改善设计的灵活性、可复用性和可维护性。(后续章节会涉及更多 GoF 设计模式,如策略模式、工厂模式等)
模块化与信息隐藏(核心设计原则)
模块化和信息隐藏是软件设计中两个最基本且重要的原则,由 David Parnas 等先驱提出,旨在构建易于管理、理解、修改和维护的软件系统。
动机与目标
早期软件开发面临诸多挑战,促使人们思考如何设计出「好」的软件。
- Parnas(1972) 强调:
- 管理:便于分工协作。
- 产品灵活性:易于修改和演化。
- 可理解性:易于理解系统。
- 特征:允许模块在不了解其他模块内部代码的情况下编写;允许模块被重新组装和替换,而无需重新组装整个系统。
- Stevens(1974) 关注:
- 简洁性:易于调试和分解。
- 可观察性:易于修改。
- Boehm(1976) 提出:
- 可维护性
- 可扩展性
- 可理解性
- 可复用性
这些目标共同指向了通过模块化和信息隐藏来控制复杂性的思想。
核心概念
模块化
模块化
模块化是指将一个复杂的计算机系统分解为多个相互作用、但相对独立的模块的过程和特性。
- 什么是模块?
- 不仅仅是一段代码。
- 可以是一个编译单元、一个工作单元。
- 更广泛地,是一组具有良好定义接口和目标的编程单元(如类、过程)的集合,可以独立分配给开发者。
- 为何要模块化?
- 管理:分而治之,便于团队开发。
- 演化:解耦系统各部分,隔离变更影响(连续性/局部性原则:需求的微小变更只影响少量模块;直接性原则:需求与模块清晰对应)。
- 理解:将系统分解为可管理的「块」(符合 7±2 法则),每次关注一个问题(局部性、封装、关注点分离原则)。
信息隐藏
信息隐藏
信息隐藏是一种设计原则,主张每个模块都应该向其他模块隐藏其内部的设计决策(称为「秘密」),只通过明确定义的接口暴露必要的信息。
- 什么是「秘密」?
- 最常见的秘密是那些未来可能发生变化的设计决策。
- 易变的设计领域示例:
- 硬件依赖
- 外部系统接口
- 输入输出格式
- 数据库模式、UI 实现细节
- 非标准的语言特性或库
- 平台细节(操作系统、中间件、框架)
- 困难的设计和实现区域(可能需要重构)
- 复杂的算法、调度逻辑、性能关键代码
- 复杂的数据结构
- 全局变量(应尽量避免,若必须使用则通过访问例程隐藏)
- 数据大小约束(如数组大小、循环限制)
- 业务规则
- 如何隐藏?
- 识别:找出系统中可能变化的设计决策(秘密)。
- 分离:将每个秘密分配给独立的模块(类、子程序等)。
- 隔离/封装:通过接口限制对模块内部细节的访问,使得当秘密发生变化时,影响仅限于模块内部,不波及其他部分。
- 接口 vs. 实现:
- 接口:模块用户的视图,描述了模块能做什么(提供的服务),但不关心如何做。接口应尽可能稳定。
- 句法接口:如何调用操作(签名、参数、顺序)。
- 语义接口:操作做什么(前置条件、后置条件、用例)。
- 实现:模块内部的细节,包括数据结构、算法、私有方法等,这些是需要隐藏的。
- 接口:模块用户的视图,描述了模块能做什么(提供的服务),但不关心如何做。接口应尽可能稳定。
关键原则:高内聚、低耦合
这两个概念是衡量模块化设计质量的核心标准,由 Stevens 等人提出。
耦合
耦合
耦合衡量的是模块之间相互依赖或关联的强度。目标是实现低耦合,即模块间依赖尽可能少而弱。
- 衡量维度:
- 连接的复杂度(例如,通过全局变量 vs. 参数传递)。
- 连接是引用模块本身还是模块内部元素。
- 连接传递的是什么类型的信息(数据 vs. 控制)。
- 结构化设计中的耦合类型(强度从高到低):
- 内容耦合:一个模块直接访问或修改另一个模块的内部数据或代码。最差的耦合,应完全避免。
- 公共耦合:多个模块共享同一个全局数据区。全局变量是其典型形式。
- 缺陷:错误和变更易传播;难以理解单个模块;复用困难;增加系统复杂度。
- 原则 1:全局变量有害。尽量避免使用,若必须使用,通过封装(如访问器方法)来隐藏。
- 控制耦合:一个模块向另一个模块传递控制信息(如标志位、开关),决定后者的执行路径。
- 问题:调用者需要了解被调用者的内部逻辑;降低了被调用者的可复用性。
- 标记耦合:模块间传递复合数据结构(如对象、记录),但接收模块仅使用了其中的一部分数据。
- 问题:传递了不必要的数据,增加了依赖;若数据结构变化,可能影响未使用该部分数据的模块。
- 数据耦合:模块间仅通过参数传递必需的基本数据类型。最理想的耦合形式。
- 无耦合:模块间完全独立。理论上的最佳状态,实践中较少见。
具体例子说明
-
内容耦合(最差)
- 场景:电商系统的订单模块直接修改用户模块的数据库表。
- 代码示例:
1
2
3
4class OrderModule:
def cancel_order(self):
# 直接修改用户表的积分字段(内容耦合)
execute_sql("UPDATE users SET points = 0 WHERE user_id = 123")- 问题:订单模块需要了解用户模块的内部实现(如字段名),用户表结构变化会导致订单模块崩溃。
-
公共耦合(全局变量)
- 场景:多个模块共享一个全局配置对象。
- 代码示例:
1
2
3
4
5
6
7
8
9
10# 全局变量(公共耦合)
config = {"timeout": 30}
class NetworkModule:
def request(self):
if config["timeout"] > 10: ...
class LogModule:
def write_log(self):
if config["timeout"] == 0: ...- 问题:若
config
结构变更(如timeout
改为request_timeout
),所有依赖它的模块均需修改。
-
控制耦合(传递控制标志)
- 场景:支付模块通过参数控制是否记录日志。
- 代码示例:
1
2
3
4class PaymentModule:
def pay(self, amount, is_debug_mode):
if is_debug_mode: # 控制耦合
print("Debug: Payment started")- 问题:调用方需知道
is_debug_mode
的作用,且支付模块的逻辑受外部控制,难以复用。
-
标记耦合(传递多余数据)
- 场景:用户模块传递整个用户对象给通知模块,但通知模块只需邮箱。
- 代码示例:
1
2
3
4
5
6
7
8
9class User {
String name, email, address;
}
class NotificationModule {
void sendEmail(User user) { // 标记耦合
System.out.println("Sending to: " + user.email);
}
}- 问题:若
User
类新增字段(如phone
),即使通知模块不用,仍需重新编译。
-
数据耦合(理想)
- 场景:计算模块仅接收必要的数值参数。
- 代码示例:
1
2
3class Calculator:
def add(self, a: float, b: float) -> float: # 仅传递必需数据
return a + b- 优点:模块完全独立,参数变化不影响内部逻辑。
-
无耦合(理论最佳)
- 场景:两个工具类完全独立。
- 示例:
1
2
3
4
5
6
7class MathUtils:
def sqrt(x): ...
class StringUtils:
def reverse(s): ...- 特点:彼此无调用、无共享数据,但实际系统中模块间通常需要协作。
耦合类型 | 关键特征 | 案例 |
---|---|---|
内容耦合 | 直接修改其他模块内部 | 订单模块篡改用户数据库 |
公共耦合 | 共享全局数据 | 多个模块读写同一配置对象 |
控制耦合 | 传递控制逻辑的标志位 | pay(amount, is_debug=True) |
标记耦合 | 传递复杂结构但只用部分数据 | sendEmail(User) 仅用邮箱 |
数据耦合 | 仅传递基本数据 | add(a, b) |
无耦合 | 模块完全独立 | 数学工具类 vs 字符串工具类 |
- 降低耦合的策略:
- 面向接口编程:依赖抽象接口而非具体实现。原则 4:面向接口编程!
- 显式接口:所有依赖都通过接口明确声明,无隐藏耦合。原则 2:显式表达。
- 最小化接口:接口应尽可能小而专注。
- 信息隐藏:隐藏内部实现细节,减少外部依赖。
- 避免传递控制信息。
- 只传递必要的数据。
- 消除重复(Don't Repeat Yourself - DRY):重复代码可能导致隐式耦合。原则 3:不要重复!
内聚
内聚
内聚衡量的是一个模块内部各个元素(代码、数据)之间关联的紧密程度,即模块执行单一、明确定义任务的程度。目标是实现高内聚,即模块内部元素紧密相关,共同完成一个清晰的目标。
- 实现独立模块的方法:
- 减少模块外部元素的关系(降低耦合)。
- 增加模块内部元素的关系(提高内聚)。
- 结构化设计中的内聚类型(强度从低到高):
- 偶然内聚:模块内各元素之间没有任何有意义的联系,只是碰巧放在一起。最差的内聚。
- 示例:一个包含
findPattern
,average
,openFile
等无关方法的工具类Rous
。
- 示例:一个包含
- 逻辑内聚:模块内包含一组逻辑上相关的功能,通过传入的控制参数选择执行其中一个。
- 示例:一个
sample(flag)
方法,根据flag
的值执行不同的操作(如 ON, OFF, CLOSE)。 - 问题:接口难以理解;代码交织;难以复用单个功能。
- 示例:一个
- 时间内聚:模块内各元素因为需要在同一时间段执行而被组合在一起(如初始化、清理)。
- 示例:一个
initialize()
方法,包含多个对象的创建和设置;一个shutdown()
方法包含资源释放操作。 - 问题:模块职责不单一;代码修改可能影响不相关操作;复用性差。
- 示例:一个
- 过程内聚:模块内各元素按照特定的执行顺序组合在一起。
- 示例:一个函数先读取数据,再处理数据,最后写入数据。
- 优于时间/逻辑内聚,但仍可能将多个不同职责的操作绑定在一起。
- 通信内聚:模块内各元素操作于相同的数据结构或 I/O。
- 示例:一个模块包含对同一个
Customer
对象进行查找、更新、打印地址标签等所有操作。 - 比过程内聚好,但仍可能包含多个独立的功能。
- 示例:一个模块包含对同一个
- 顺序内聚:模块内一个元素的输出是另一个元素的输入,形成执行链。
- 示例:一个模块先读取原始数据,然后处理数据,再格式化数据。
- 高内聚,但不如功能内聚灵活。
- 功能内聚:模块内所有元素共同协作,完成一个单一的、明确定义的功能。最理想的内聚形式。
- 示例:计算平方根、读取配置文件、验证用户登录。
- 信息内聚:(面向对象中常见)模块执行多个操作,每个操作代表一个独立的功能入口点,但所有操作都作用于同一份数据结构(该数据结构是模块的秘密,对外部隐藏)。这是实现抽象数据类型 或类的基础。
- 示例:一个
Stack
类,提供push()
,pop()
,isEmpty()
等操作,内部维护栈的数据结构。
- 示例:一个
- 偶然内聚:模块内各元素之间没有任何有意义的联系,只是碰巧放在一起。最差的内聚。
具体例子说明
-
偶然内聚
- 特点:模块内的代码毫无关联,纯粹因为「方便」被放在一起。
- 示例:
1
2
3
4
5
6class MiscellaneousUtils {
// 完全无关的方法堆砌
public void generateRandomNumber() { ... }
public void parseCSV(String file) { ... }
public void sendEmail(String recipient) { ... }
}- 问题:难以维护,调用者无法预测模块的行为。
-
逻辑内聚
- 特点:通过参数控制执行不同逻辑,但功能间无直接关联。
- 示例:
1
2
3
4
5
6
7def handle_operation(operation_type):
if operation_type == "LOGIN":
check_credentials()
elif operation_type == "LOGOUT":
clear_session()
elif operation_type == "REGISTER":
create_account()- 问题:新增操作类型需修改函数,复用单个功能困难(如只想用
check_credentials
但必须调用整个函数)。
-
时间内聚
- 特点:代码因「执行时间相同」被组合。
- 示例:
1
2
3
4
5
6
7
8
9
10
11void startup() {
init_database();
load_config();
start_background_threads();
}
void shutdown() {
stop_threads();
save_logs();
close_database();
}- 问题:若只需修改日志保存逻辑,仍需理解整个
shutdown
函数。
-
过程内聚
- 特点:代码按固定流程执行,但步骤间无数据依赖。
- 示例:
1
2
3
4
5function processOrder() {
validatePayment(); // 验证支付
updateInventory(); // 更新库存
sendConfirmation(); // 发送邮件
}- 改进方向:若
updateInventory
和sendConfirmation
不需要validatePayment
的结果,应考虑拆分。
-
通信内聚
- 特点:操作同一数据源,但功能独立。
- 示例:
1
2
3
4
5class CustomerService {
public void addCustomer(Customer c) { ... }
public void deleteCustomer(int id) { ... }
public void printCustomerReport(Customer c) { ... }
}- 优点:比过程内聚更聚焦(都围绕
Customer
对象)。 - 缺点:打印报告和删除客户本质是独立功能。
-
顺序内聚
- 特点:前一步的输出是后一步的输入。
- 示例:
1
2
3
4
5def process_data():
raw_data = read_file("input.txt") # 读取
cleaned_data = clean(raw_data) # 清洗(依赖读取)
result = analyze(cleaned_data) # 分析(依赖清洗)
save_to_database(result) # 存储(依赖分析)- 优点:比通信内聚更高内聚,但若只想复用
analyze
仍需解耦。
-
功能内聚
- 特点:所有代码只为完成一个明确功能。
- 示例:
1
2
3float calculateBMI(float weight, float height) {
return weight / (height * height);
}- 关键:输入、输出、功能均单一,无副作用,易于复用和测试。
-
信息内聚
- 特点:面向对象中的「类」,封装数据及相关操作。
- 示例:
1
2
3
4
5
6
7class BankAccount {
private double balance;
public void deposit(double amount) { ... }
public void withdraw(double amount) { ... }
public double getBalance() { ... }
}- 优势:数据(
balance
)和操作高度内聚,对外隐藏实现细节。
内聚类型 | 关键特征 | 示例场景 | 可维护性 |
---|---|---|---|
偶然内聚 | 代码随机堆砌 | 「万能工具类」 | ❌ 最差 |
逻辑内聚 | 通过参数分支执行不同逻辑 | 多功能开关函数 | ❌ |
时间内聚 | 同一时间段执行 | 初始化/清理模块 | ⚠️ |
过程内聚 | 按流程步骤组织 | 订单处理流水线 | ⚠️ |
通信内聚 | 操作同一数据源 | 客户信息管理模块 | ✅ |
顺序内聚 | 前一步输出是后一步输入 | 数据读取 清洗 分析 | ✅ |
功能内聚 | 单一明确功能 | 计算 BMI、验证密码强度 | ✅ 最佳 |
信息内聚 | 封装数据及相关操作 | Stack 类(push/pop) | ✅ 最佳 |
设计原则:尽量追求功能内聚和信息内聚,避免偶然/逻辑内聚。
应用与启发式规则
模块化思想的应用
- 低耦合处理:
- 分层架构:不同层之间仅通过明确接口调用和数据传递交互,避免共享数据(如 Model 层对象直接传递给 Logic 层使用可能导致公共耦合)。
- 逻辑包设计:将功能相关的类组织在包内,通过包分割实现接口最小化,减少不必要的跨包依赖。
- 物理包设计:将不同包的重复内容独立为单独包,消除重复,避免隐式重复耦合。
- 对象创建(Creator 模式):如果 A 和 B 已有较高耦合度,让 A 创建 B (或反之) 不会引入额外耦合。
- 控制风格:使用 Controller 解耦界面与逻辑对象。
- 高内聚处理:
- 分层架构:每一层都应是高内聚的(如 UI 层处理交互,Logic 层处理业务,Data 层处理持久化)。
- 逻辑包设计:每个包都应聚焦于一组紧密相关的功能。
- 抽象类职责:类的状态(属性)和方法应紧密联系,共同服务于类的核心职责(信息内聚/功能内聚)。
- 控制风格:将控制逻辑封装在 Controller 中(功能内聚),但 Controller 自身可能承载顺序/通信/逻辑内聚,需控制其规模。
信息隐藏思想的应用
- 分层架构:每层隐藏其内部实现细节,只暴露服务接口。层间依赖关系体现了决策稳定性的差异(UI 最易变,数据最稳定)。
- 物理包设计:将特定决策(如安全处理、网络通信、数据库访问)封装在独立包内。
- 接口定义:严格要求定义模块和类的接口,是实现信息隐藏的基础,也便利开发。
- 控制风格:使用 Controller 封装业务逻辑相关的设计决策,避免其散布到整个对象网络中。
Parnas 的模块指南
Parnas 提出用 Module Guide 文档来清晰描述模块设计,作为信息隐藏实践的工具。每个模块的描述应包含:
- 主要秘密:模块所隐藏的核心设计决策,通常对应要实现的用户需求或系统特性。这是模块存在的根本原因。
- 次要秘密:实现主要秘密所涉及的具体实现细节,如数据结构、算法、硬件平台信息等。
- 模块角色:模块在整个系统中所承担的角色、作用以及与其他模块的关联关系。
- 对外接口:模块提供给其他模块使用的接口(方法签名、功能描述)。
示例:KWIC 系统中的 CircularShifter 模块
- 主要秘密:实现字符串的循环位移功能。
- 次要秘密:循环位移的具体算法;存储位移结果的数据结构(如是否存储所有位移,还是按需计算)。
- 角色:接收原始行数据,生成所有循环位移,提供给 Alphabetizer 模块进行排序。依赖 LineStorage 模块获取原始行。
- 对外接口:
setup(lines)
,getChar(line, word, char)
,getWord(line, word)
,getLine(index)
,getLineAsString(index)
,getLineCount()
等。
KWIC 案例分析
KWIC (Key Word In Context) 索引系统是一个经典的用于说明模块化设计原则的例子。
-
功能:输入多行文本,对每行进行循环移位(将第一个单词移到末尾,重复此过程),然后按字母顺序对所有移位后的行进行排序并输出。
-
Parnas 提出的两种模块化方案:
- 模块化方案 1(基于功能步骤):
- 模块:Master Control, Input, Circular Shift, Alphabetizer, Output。
- 数据共享:通过共享数据区(Characters, Index, Alphabetized Index)传递中间结果。
- 问题:模块间存在公共耦合和可能的标记/控制耦合。数据结构的改变(如行存储方式)会影响多个模块。不符合信息隐藏原则。
- 模块化方案 2(基于信息隐藏):
- 模块:Master Control, Line Storage, Circular Shifter, Alphabetizer, Output。
- 核心思想:每个模块隐藏一个主要的设计决策(秘密)。
Line Storage
:隐藏字符/行的具体存储方式。Circular Shifter
:隐藏循环位移的算法和存储方式。Alphabetizer
:隐藏排序算法。
- 交互:通过明确定义的接口进行。
- 优点:
- 可修改性:设计决策的变化(如改变行存储方式、位移算法、排序算法)只影响单个模块。
- 独立开发:定义好接口后,各模块可并行开发。
- 可理解性:模块职责清晰,接口明确,耦合度低。
- 模块化方案 1(基于功能步骤):
-
结论:基于信息隐藏的模块化方案(方案 2)在可修改性、独立开发和可理解性方面均优于基于功能步骤的方案(方案 1)。它更好地体现了低耦合、高内聚的设计目标。
GRASP 模式(通用职责分配软件模式)
GRASP (General Responsibility Assignment Software Patterns) 由 Craig Larman 提出,它不是像 GoF 那样的具体设计模式,而是一组指导如何在面向对象设计中分配职责的基本原则。它们旨在帮助创建低耦合、高内聚、易于维护的设计。
GRASP 概述
GRASP 关注对象设计中最重要的问题之一:将职责分配给类。核心模式包括信息专家、创建者、控制器、低耦合和高内聚。后两者是目标,前三者是实现目标的具体手段。
信息专家
- 问题:将职责分配给对象的最基本原则是什么?
- 解决方案:将一个职责分配给拥有履行该职责所需信息的类。
- 核心思想:如果一个类拥有完成某个任务所必需的数据,那么这个任务(职责)就应该由这个类来承担。这通常意味着操作(方法)应该和它所操作的数据(属性)放在同一个类中。
- 优点:
- 维护信息的封装性。
- 促进低耦合(因为信息和操作它的行为在一起,减少了跨类访问数据的需要)。
- 促进高内聚(类的方法和属性紧密相关)。
计算销售总额(POS 系统)
- 职责:计算一次销售的总金额。
- 所需信息:需要知道该次销售包含的所有销售项以及每个销售项的小计。
- 信息专家:
Sale
类拥有其包含的所有SalesLineItem
的列表。因此,计算总金额的职责(getTotal()
)应分配给Sale
类。 - 进一步分析:计算每个
SalesLineItem
的小计需要知道数量和单价。 - 信息专家:
SalesLineItem
类知道自己的数量,并且它关联了ProductSpecification
(知道价格)。因此,计算小计(getSubtotal()
)的职责应分配给SalesLineItem
。 - 信息专家:
ProductSpecification
类知道产品的价格。因此,提供价格(getPrice()
)的职责应分配给ProductSpecification
。 - 协作:
Sale.getTotal()
遍历SalesLineItem
调用SalesLineItem.getSubtotal()
调用ProductSpecification.getPrice()
。
sequenceDiagram
participant C as Client
participant S as :Sale
participant SLI as :SalesLineItem
participant PS as :ProductSpecification
C->>S: getTotal()
activate S
loop for each SalesLineItem
S->>SLI: getSubtotal()
activate SLI
SLI->>PS: getPrice()
activate PS
PS-->>SLI: price
deactivate PS
SLI-->>S: subtotal
deactivate SLI
end
S-->>C: total
deactivate S
创建者
- 问题:应该由哪个类来负责创建某个类(A)的实例?
- 解决方案:如果 B 类满足以下一个或多个条件(越靠前越优先),则将创建 A 类实例的职责分配给 B 类:
- B 聚合 A 对象。
- B 包含 A 对象(组合关系)。
- B 记录 A 的实例。
- B 密切使用 A 对象。
- B 拥有创建 A 对象所需的初始化数据。
- 核心思想:让与被创建对象关系最紧密的类负责创建它。
- 优点:促进低耦合。创建者类已经与被创建类存在关联,让它负责创建可以避免引入新的依赖关系(例如,避免让一个不相关的工厂类来创建,从而引入对工厂类的依赖)。
创建销售项(POS 系统)
- 职责:创建
SalesLineItem
对象。 - 分析:
Sale
类聚合了SalesLineItem
对象(条件 1 满足)。Sale
类需要使用SalesLineItem
对象来计算总价等(条件 4 满足)。
- 创建者:根据 Creator 模式,
Sale
类是创建SalesLineItem
的合适候选者。通常会有一个makeLineItem()
或addItem()
类似的方法在Sale
类中。
Monopoly 游戏
- 谁创建棋盘格?
Board
类包含多个Square
,因此Board
是 Creator。
- 谁创建棋子?
Player
拥有Piece
,因此Player
是 Creator。
- 谁创建玩家?
MonopolyGame
类聚合或记录Player
,因此MonopolyGame
是 Creator。
控制器
- 问题:如何分配处理系统事件(通常来自 UI 或外部系统)的职责?直接让 UI 对象处理业务逻辑吗?
- 解决方案:将处理系统事件消息的职责分配给一个单独的控制器类。这个类代表了整个系统、业务场景或用例。
- 核心思想:解耦表示层(UI)和领域/业务逻辑层。UI 只负责显示信息和捕获用户输入,将业务处理请求转发给 Controller,由 Controller 协调领域对象完成任务。
- 优点:
- 提高了领域逻辑的可复用性(不依赖于特定 UI)。
- 提高了 UI 的可替换性。
- 使系统结构更清晰,职责更明确。
- Controller 的类型(选择):
- 门面控制器:代表整个业务或组织(如
POSSystem
类)或整个系统(如Register
类)。适用于简单场景或只有一个主要控制点的系统。 - 角色控制器:代表在现实世界中执行该任务的人或角色(如
Cashier
类)。设计更直观,但可能与领域模型角色混淆。 - 用例控制器 或 会话控制器:为每个用例或用户会话创建一个专门的 Controller(如
ProcessSaleController
,BuyItemsHandler
)。这是最常见的方式,可以保证 Controller 的高内聚(只负责一个用例),但可能导致 Controller 数量增多。通常是推荐的选择。- 这种 Controller 通常是「人造」的,在领域模型中没有直接对应物,纯粹为了实现低耦合高内聚的设计目标而创建。
- 门面控制器:代表整个业务或组织(如
- 缺点/风险:
- Controller 可能变得臃肿,承担过多职责,导致自身低内聚、高耦合。需要注意将业务逻辑委托给真正的领域专家对象。Controller 主要负责协调和委托。
具体例子说明
假设我们有一个零售商店的销售系统,需要处理以下用例:
-
处理销售:顾客购买商品,生成订单。
-
退货:顾客退回商品,退款或换货。
-
库存管理:店员更新商品库存。
-
门面控制器:代表整个系统或业务,所有请求都通过一个统一的入口处理。
- 示例:
POSSystem
类作为唯一控制器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class POSSystem {
public void processSale(List<Item> items, Payment payment) {
// 处理销售逻辑
Order order = new Order(items);
payment.process(order.getTotal());
Inventory.update(items, Operation.SALE);
}
public void handleReturn(Order order, String reason) {
// 处理退货逻辑
order.refund();
Inventory.update(order.getItems(), Operation.RETURN);
}
public void addInventory(Item item, int quantity) {
// 更新库存
Inventory.add(item, quantity);
}
}- 优点:简单直接,适合小型系统。
- 缺点:随着功能增加,
POSSystem
会变得臃肿(比如未来添加会员管理、促销活动等逻辑)。
- 示例:
-
角色控制器:代表现实中的角色,比如收银员、店长。
- 示例:
Cashier
和StoreManager
类分别处理销售和库存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Cashier {
public void processSale(List<Item> items, Payment payment) {
Order order = new Order(items);
payment.process(order.getTotal());
}
public void handleReturn(Order order) {
order.refund();
}
}
public class StoreManager {
public void addInventory(Item item, int quantity) {
Inventory.add(item, quantity);
}
}- 优点:符合现实直觉(收银员处理交易,店长管理库存)。
- 缺点:可能与领域模型混淆(比如领域模型中已有
Cashier
实体,导致职责重叠)。
- 示例:
-
用例控制器:每个用例对应一个控制器,高内聚、低耦合。
- 示例:
ProcessSaleController
,HandleReturnController
,ManageInventoryController
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 处理销售用例
public class ProcessSaleController {
public void execute(List<Item> items, Payment payment) {
Order order = new Order(items);
payment.process(order.getTotal());
Inventory.decrease(items); // 委托给领域对象
}
}
// 处理退货用例
public class HandleReturnController {
public void execute(Order order, String reason) {
if (order.isRefundable(reason)) {
order.refund();
Inventory.increase(order.getItems());
}
}
}
// 管理库存用例
public class ManageInventoryController {
public void addItem(Item item, int quantity) {
Inventory.add(item, quantity);
}
}- 优点:
- 职责单一(每个控制器只做一件事)。
- 易于扩展(新增用例时只需添加新控制器)。
- 与领域模型解耦(控制器是「协调者」,逻辑在领域对象中)。
- 缺点:控制器数量可能较多(但这是值得的)。
- 示例:
类型 | 示例 | 适用场景 | 风险 |
---|---|---|---|
门面控制器 | POSSystem |
简单系统,功能少 | 控制器膨胀成「上帝对象」 |
角色控制器 | Cashier , Manager |
角色职责清晰的小型系统 | 与领域模型混淆 |
用例控制器 | ProcessSaleController |
复杂系统,需长期维护 | 控制器数量多(但可管理) |
最佳实践建议:
- 优先选择用例控制器:现代软件工程中,用例控制器是最推荐的方式,尤其适合遵循 Clean Architecture 或 DDD(领域驱动设计) 的系统。
- 避免「贫血控制器」:控制器应只负责协调,核心逻辑应放在领域对象中(比如
Order
,Inventory
)。
处理商品录入事件(POS 系统)
- 事件:用户在 UI 上输入商品 ID 和数量,点击「录入商品」按钮(
enterItem
事件)。 - 错误设计(UI 耦合领域):UI 按钮的事件处理器直接调用
Sale
对象的makeLineItem
方法。导致 UI 依赖领域对象Sale
。 - 正确设计(使用 Controller):
- UI 按钮事件处理器将请求(商品 ID、数量)发送给
ProcessSaleController
。 ProcessSaleController
接收请求,调用Sale
对象的makeLineItem
方法。Sale
对象(Creator)创建SalesLineItem
实例。- Controller 可能还需要协调其他领域对象(如更新库存、获取商品描述等)。
- Controller 将结果返回给 UI 进行显示。
- UI 按钮事件处理器将请求(商品 ID、数量)发送给
sequenceDiagram
participant UI as :POSTerminal (UI)
participant Ctrl as :ProcessSaleController
participant S as :Sale
participant SLI as :SalesLineItem
UI->>Ctrl: enterItem(itemID, quantity)
activate Ctrl
Ctrl->>S: makeLineItem(itemID, quantity)
activate S
S->>SLI: <<create>>
S-->>Ctrl: (acknowledgement)
deactivate S
Ctrl-->>UI: (update display info)
deactivate Ctrl
控制风格
控制风格描述了系统中行为逻辑(特别是决策逻辑)在对象网络中的分布方式。它与 Controller 模式密切相关,影响着系统的整体协作模式。
控制风格
控制风格是指系统行为逻辑在对象(或组件)网络中的分布方式。
主要有三种风格:
-
集中式:大部分或所有的决策逻辑和控制流都集中在一个或少数几个「控制器」对象中。其他对象主要作为数据容器或简单执行者。
- 优点:
- 容易找到决策点。
- 容易理解和修改整体流程。
- 缺点:
- 控制器容易变得臃肿、复杂,难以理解、维护和测试。
- 控制器可能将其他对象视为简单的数据仓库,破坏其封装和信息隐藏。
- 导致高耦合(控制器依赖许多其他对象)和低内聚(控制器承担过多不相关职责)。
- 表现:顺序图中,消息通常从一个中心控制器发出到多个其他对象。
sequenceDiagram participant Controller participant Database participant Logger participant Validator Controller->>Database: queryData() Database-->>Controller: data Controller->>Validator: validate(data) Validator-->>Controller: validationResult Controller->>Logger: log(validationResult) Note right of Controller: 所有决策由 Controller 发起<br/>其他对象被动执行
- 优点:
-
委托式:决策逻辑分布在多个对象中,但主要的、高层的决策仍然由少数几个控制器做出。控制器会将具体的子任务和决策委托给其他更专业的对象(通常是 Information Expert)。这是推荐的风格,是集中式和分散式之间的一种平衡。
- 优点:
- 控制器职责相对清晰,避免过度臃肿。
- 更好地利用了领域对象的智能。
- 相对较好的耦合和内聚。
- 表现:顺序图中,控制器调用其他对象,这些对象内部可能还有自己的决策和协作。
sequenceDiagram participant OrderController participant OrderValidator participant PaymentProcessor participant InventoryManager OrderController->>OrderValidator: validate(order) OrderValidator->>PaymentProcessor: checkPayment(order.payment) OrderValidator->>InventoryManager: checkStock(order.items) OrderValidator-->>OrderController: validationResult OrderController->>PaymentProcessor: processPayment() OrderController->>InventoryManager: updateStock() Note left of OrderValidator: 子任务被委托给专业对象<br/>如 Validator 协调支付和库存检查
- 优点:
-
分散式:系统行为逻辑广泛地、均匀地分布在大量对象中,没有明显的中心控制器。每个对象只承担很小一部分职责和决策。
- 优点:(理论上)可能更符合某些去中心化的模型。
- 缺点:
- 难以理解控制流,逻辑分散各处。
- 对象功能过于简单,需要频繁交互,导致高耦合。
- 难以隐藏信息。
- 内聚通常很差。
- 很少能满足模块化原则。
- 表现:顺序图中,消息在多个对象之间频繁传递,形成复杂的网状结构。
sequenceDiagram participant A participant B participant C participant D A->>B: requestX() B->>C: getDataForX() C->>D: fetchRawData() D-->>C: rawData C-->>B: processedData B->>A: responseX() A->>C: notifyCompletion() Note over A,D: 消息在多个对象间频繁传递<br/>无明确控制中心
控制启发式规则
- 避免交互设计中大部分消息都源自单一组件(倾向于委托式或适度分散)。
- 保持组件/类小而专注。
- 确保操作职责与数据职责一致。
- 避免需要每个组件发送大量消息的交互(倾向于更集中的协调)。
在面向对象设计中,通常追求委托式控制风格,利用 Controller 进行高层协调和解耦,同时将具体业务逻辑和决策委托给相应的领域专家对象,以达到较好的低耦合和高内聚。