详细设计
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..1B:0..*组合 B is a part of A is-part-of 关系,强聚合。部分与整体生命周期绑定,不能被共享。 -<*>(实线实心菱形箭头)A: 0..1B: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 进行高层协调和解耦,同时将具体业务逻辑和决策委托给相应的领域专家对象,以达到较好的低耦合和高内聚。