详细设计

AI WARNING

未进行对照审核。

详细设计概述

什么是详细设计

详细设计(Detail Design)是软件开发过程中的一个关键阶段,它承接软件体系结构设计,并为后续的编码实现提供具体指导。它关注的是将体系结构中定义的模块或组件进一步细化,明确其内部实现机制。

详细设计

详细设计的主要任务是将宏观的架构蓝图转化为微观的实现细节,包括确定类/对象的具体职责、它们之间的交互方式、数据结构和算法的选择等。

详细设计通常包含两个层面:

  1. 中层设计:针对特定的模块,进行面向对象的设计,定义模块内部包含的主要类及其规格说明(接口、属性、主要方法)。
  2. 低层设计:针对具体的类,设计其内部实现,包括选择合适的数据结构(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)的核心在于如何将系统的功能合理地分配给对象,并定义它们之间的协作方式。两个基本概念是职责和协作。

职责

职责

职责是一个类或对象所承担的义务,它可以是执行某项任务(行为职责)或维护某些数据(数据职责)。

  • 行为职责:通常由类的方法来实现。例如,「计算订单总价」是一个行为职责。
  • 数据职责:通常由类的属性来实现。例如,「维护订单包含的商品列表」是一个数据职责。

职责驱动分解:面向对象设计常常以职责为驱动力来进行系统分解。

  • 职责可以在不同的抽象层次上描述,并且可以被分解。
  • 高层职责分配给高层组件(模块),然后进一步分解并将子职责分配给内部的类或对象。
  • 这种分解方式与纯粹的功能分解不同,因为它同时考虑了数据和行为的归属。

职责分配启发式规则:良好的职责分配有助于实现高内聚和低耦合(后续会详细介绍)。

  1. 确保模块/类的职责不重叠。
  2. 一个模块/类中的操作和数据应该仅仅是为了帮助其履行自身职责。
  3. 委托:当一个模块(委托者, 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
2
3
4
5
6
7
8
9
classDiagram
    class User {
        -int userId
        -string name
        -string password
        +validatePassword(password: string): boolean
        +getUsername(): string
        +setUsername(name: string): void
    }

抽象类之间的关系:

  • 识别并定义类之间的静态关系,这有助于理解系统的结构和依赖。

  • 常见的关系类型包括:

    关系类型 关系短语(示例) 解释 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
    }

添加辅助类:

  • 在核心业务类之外,通常需要一些辅助类来完成特定任务或改善设计。
  • 常见的辅助类类型:
    • 接口类:定义服务契约。
    • 记录类/数据类:主要用于封装和传递数据(如 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
    }

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
      15
      
      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: 显示总金额
    • 状态图:

      • 用于描述一个复杂对象在其生命周期内所经历的状态序列、引起状态转移的事件 以及伴随状态转移的动作。
      • 适用于对具有复杂状态行为的对象进行建模。
      • 协作体现在:状态图中的 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 实现细节
      • 非标准的语言特性或库
      • 平台细节(操作系统、中间件、框架)
      • 困难的设计和实现区域(可能需要重构)
      • 复杂的算法、调度逻辑、性能关键代码
      • 复杂的数据结构
      • 全局变量(应尽量避免,若必须使用则通过访问例程隐藏)
      • 数据大小约束(如数组大小、循环限制)
      • 业务规则
  • 如何隐藏?
    1. 识别:找出系统中可能变化的设计决策(秘密)。
    2. 分离:将每个秘密分配给独立的模块(类、子程序等)。
    3. 隔离/封装:通过接口限制对模块内部细节的访问,使得当秘密发生变化时,影响仅限于模块内部,不波及其他部分。
  • 接口 vs. 实现:
    • 接口:模块用户的视图,描述了模块能做什么(提供的服务),但不关心如何做。接口应尽可能稳定。
      • 句法接口:如何调用操作(签名、参数、顺序)。
      • 语义接口:操作做什么(前置条件、后置条件、用例)。
    • 实现:模块内部的细节,包括数据结构、算法、私有方法等,这些是需要隐藏的。

关键原则:高内聚、低耦合

这两个概念是衡量模块化设计质量的核心标准,由 Stevens 等人提出。

耦合

耦合

耦合衡量的是模块之间相互依赖或关联的强度。目标是实现低耦合,即模块间依赖尽可能少而弱。

  • 衡量维度:
    • 连接的复杂度(例如,通过全局变量 vs. 参数传递)。
    • 连接是引用模块本身还是模块内部元素。
    • 连接传递的是什么类型的信息(数据 vs. 控制)。
  • 结构化设计中的耦合类型(强度从高到低):
    1. 内容耦合:一个模块直接访问或修改另一个模块的内部数据或代码。最差的耦合,应完全避免。
    2. 公共耦合:多个模块共享同一个全局数据区。全局变量是其典型形式。
      • 缺陷:错误和变更易传播;难以理解单个模块;复用困难;增加系统复杂度。
      • 原则 1:全局变量有害。尽量避免使用,若必须使用,通过封装(如访问器方法)来隐藏。
    3. 控制耦合:一个模块向另一个模块传递控制信息(如标志位、开关),决定后者的执行路径。
      • 问题:调用者需要了解被调用者的内部逻辑;降低了被调用者的可复用性。
    4. 标记耦合:模块间传递复合数据结构(如对象、记录),但接收模块仅使用了其中的一部分数据。
      • 问题:传递了不必要的数据,增加了依赖;若数据结构变化,可能影响未使用该部分数据的模块。
    5. 数据耦合:模块间仅通过参数传递必需的基本数据类型。最理想的耦合形式。
    6. 无耦合:模块间完全独立。理论上的最佳状态,实践中较少见。
具体例子说明
  1. 内容耦合(最差)

    • 场景:电商系统的订单模块直接修改用户模块的数据库表。
    • 代码示例:
    1
    2
    3
    4
    
    class OrderModule:
        def cancel_order(self):
            # 直接修改用户表的积分字段(内容耦合)
            execute_sql("UPDATE users SET points = 0 WHERE user_id = 123")
    • 问题:订单模块需要了解用户模块的内部实现(如字段名),用户表结构变化会导致订单模块崩溃。
  2. 公共耦合(全局变量)

    • 场景:多个模块共享一个全局配置对象。
    • 代码示例:
    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),所有依赖它的模块均需修改。
  3. 控制耦合(传递控制标志)

    • 场景:支付模块通过参数控制是否记录日志。
    • 代码示例:
    1
    2
    3
    4
    
    class PaymentModule:
        def pay(self, amount, is_debug_mode):
            if is_debug_mode:  # 控制耦合
                print("Debug: Payment started")
    • 问题:调用方需知道 is_debug_mode 的作用,且支付模块的逻辑受外部控制,难以复用。
  4. 标记耦合(传递多余数据)

    • 场景:用户模块传递整个用户对象给通知模块,但通知模块只需邮箱。
    • 代码示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    class User {
        String name, email, address;
    }
    
    class NotificationModule {
        void sendEmail(User user) {  // 标记耦合
            System.out.println("Sending to: " + user.email);
        }
    }
    • 问题:若 User 类新增字段(如 phone),即使通知模块不用,仍需重新编译。
  5. 数据耦合(理想)

    • 场景:计算模块仅接收必要的数值参数。
    • 代码示例:
    1
    2
    3
    
    class Calculator:
        def add(self, a: float, b: float) -> float:  # 仅传递必需数据
            return a + b
    • 优点:模块完全独立,参数变化不影响内部逻辑。
  6. 无耦合(理论最佳)

    • 场景:两个工具类完全独立。
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    
    class MathUtils:
        @staticmethod
        def sqrt(x): ...
    
    class StringUtils:
        @staticmethod
        def reverse(s): ...
    • 特点:彼此无调用、无共享数据,但实际系统中模块间通常需要协作。
耦合类型 关键特征 案例
内容耦合 直接修改其他模块内部 订单模块篡改用户数据库
公共耦合 共享全局数据 多个模块读写同一配置对象
控制耦合 传递控制逻辑的标志位 pay(amount, is_debug=True)
标记耦合 传递复杂结构但只用部分数据 sendEmail(User) 仅用邮箱
数据耦合 仅传递基本数据 add(a, b)
无耦合 模块完全独立 数学工具类 vs 字符串工具类
  • 降低耦合的策略:
    • 面向接口编程:依赖抽象接口而非具体实现。原则 4:面向接口编程!
    • 显式接口:所有依赖都通过接口明确声明,无隐藏耦合。原则 2:显式表达。
    • 最小化接口:接口应尽可能小而专注。
    • 信息隐藏:隐藏内部实现细节,减少外部依赖。
    • 避免传递控制信息。
    • 只传递必要的数据。
    • 消除重复(Don't Repeat Yourself - DRY):重复代码可能导致隐式耦合。原则 3:不要重复!

内聚

内聚

内聚衡量的是一个模块内部各个元素(代码、数据)之间关联的紧密程度,即模块执行单一、明确定义任务的程度。目标是实现高内聚,即模块内部元素紧密相关,共同完成一个清晰的目标。

  • 实现独立模块的方法:
    • 减少模块外部元素的关系(降低耦合)。
    • 增加模块内部元素的关系(提高内聚)。
  • 结构化设计中的内聚类型(强度从低到高):
    1. 偶然内聚:模块内各元素之间没有任何有意义的联系,只是碰巧放在一起。最差的内聚。
      • 示例:一个包含 findPattern, average, openFile 等无关方法的工具类 Rous。
    2. 逻辑内聚:模块内包含一组逻辑上相关的功能,通过传入的控制参数选择执行其中一个。
      • 示例:一个 sample(flag) 方法,根据 flag 的值执行不同的操作(如 ON, OFF, CLOSE)。
      • 问题:接口难以理解;代码交织;难以复用单个功能。
    3. 时间内聚:模块内各元素因为需要在同一时间段执行而被组合在一起(如初始化、清理)。
      • 示例:一个 initialize() 方法,包含多个对象的创建和设置;一个 shutdown() 方法包含资源释放操作。
      • 问题:模块职责不单一;代码修改可能影响不相关操作;复用性差。
    4. 过程内聚:模块内各元素按照特定的执行顺序组合在一起。
      • 示例:一个函数先读取数据,再处理数据,最后写入数据。
      • 优于时间/逻辑内聚,但仍可能将多个不同职责的操作绑定在一起。
    5. 通信内聚:模块内各元素操作于相同的数据结构或 I/O。
      • 示例:一个模块包含对同一个 Customer 对象进行查找、更新、打印地址标签等所有操作。
      • 比过程内聚好,但仍可能包含多个独立的功能。
    6. 顺序内聚:模块内一个元素的输出是另一个元素的输入,形成执行链。
      • 示例:一个模块先读取原始数据,然后处理数据,再格式化数据。
      • 高内聚,但不如功能内聚灵活。
    7. 功能内聚:模块内所有元素共同协作,完成一个单一的、明确定义的功能。最理想的内聚形式。
      • 示例:计算平方根、读取配置文件、验证用户登录。
    8. 信息内聚:(面向对象中常见)模块执行多个操作,每个操作代表一个独立的功能入口点,但所有操作都作用于同一份数据结构(该数据结构是模块的秘密,对外部隐藏)。这是实现抽象数据类型 或类的基础。
      • 示例:一个 Stack 类,提供 push(), pop(), isEmpty() 等操作,内部维护栈的数据结构。
具体例子说明
  1. 偶然内聚

    • 特点:模块内的代码毫无关联,纯粹因为「方便」被放在一起。
    • 示例:
    1
    2
    3
    4
    5
    6
    
    class MiscellaneousUtils {
        // 完全无关的方法堆砌
        public void generateRandomNumber() { ... }
        public void parseCSV(String file) { ... }
        public void sendEmail(String recipient) { ... }
    }
    • 问题:难以维护,调用者无法预测模块的行为。
  2. 逻辑内聚

    • 特点:通过参数控制执行不同逻辑,但功能间无直接关联。
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    
    def handle_operation(operation_type):
        if operation_type == "LOGIN":
            check_credentials()
        elif operation_type == "LOGOUT":
            clear_session()
        elif operation_type == "REGISTER":
            create_account()
    • 问题:新增操作类型需修改函数,复用单个功能困难(如只想用 check_credentials 但必须调用整个函数)。
  3. 时间内聚

    • 特点:代码因「执行时间相同」被组合。
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    void startup() {
        init_database();
        load_config();
        start_background_threads();
    }
    
    void shutdown() {
        stop_threads();
        save_logs();
        close_database();
    }
    • 问题:若只需修改日志保存逻辑,仍需理解整个 shutdown 函数。
  4. 过程内聚

    • 特点:代码按固定流程执行,但步骤间无数据依赖。
    • 示例:
    1
    2
    3
    4
    5
    
    function processOrder() {
        validatePayment();  // 验证支付
        updateInventory();  // 更新库存
        sendConfirmation(); // 发送邮件
    }
    • 改进方向:若 updateInventory 和 sendConfirmation 不需要 validatePayment 的结果,应考虑拆分。
  5. 通信内聚

    • 特点:操作同一数据源,但功能独立。
    • 示例:
    1
    2
    3
    4
    5
    
    class CustomerService {
        public void addCustomer(Customer c) { ... }
        public void deleteCustomer(int id) { ... }
        public void printCustomerReport(Customer c) { ... }
    }
    • 优点:比过程内聚更聚焦(都围绕 Customer 对象)。
    • 缺点:打印报告和删除客户本质是独立功能。
  6. 顺序内聚

    • 特点:前一步的输出是后一步的输入。
    • 示例:
    1
    2
    3
    4
    5
    
    def process_data():
        raw_data = read_file("input.txt")  # 读取
        cleaned_data = clean(raw_data)     # 清洗(依赖读取)
        result = analyze(cleaned_data)     # 分析(依赖清洗)
        save_to_database(result)           # 存储(依赖分析)
    • 优点:比通信内聚更高内聚,但若只想复用 analyze 仍需解耦。
  7. 功能内聚

    • 特点:所有代码只为完成一个明确功能。
    • 示例:
    1
    2
    3
    
    float calculateBMI(float weight, float height) {
        return weight / (height * height);
    }
    • 关键:输入、输出、功能均单一,无副作用,易于复用和测试。
  8. 信息内聚

    • 特点:面向对象中的「类」,封装数据及相关操作。
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    
    class BankAccount {
        private double balance;
    
        public void deposit(double amount) { ... }
        public void withdraw(double amount) { ... }
        public double getBalance() { ... }
    }
    • 优势:数据(balance)和操作高度内聚,对外隐藏实现细节。
内聚类型 关键特征 示例场景 可维护性
偶然内聚 代码随机堆砌 「万能工具类」 ❌ 最差
逻辑内聚 通过参数分支执行不同逻辑 多功能开关函数 ❌
时间内聚 同一时间段执行 初始化/清理模块 ⚠️
过程内聚 按流程步骤组织 订单处理流水线 ⚠️
通信内聚 操作同一数据源 客户信息管理模块 ✅
顺序内聚 前一步输出是后一步输入 数据读取 →\to 清洗 →\to 分析 ✅
功能内聚 单一明确功能 计算 BMI、验证密码强度 ✅ 最佳
信息内聚 封装数据及相关操作 Stack 类(push/pop) ✅ 最佳

设计原则:尽量追求功能内聚和信息内聚,避免偶然/逻辑内聚。

应用与启发式规则

模块化思想的应用

  • 低耦合处理:
    • 分层架构:不同层之间仅通过明确接口调用和数据传递交互,避免共享数据(如 Model 层对象直接传递给 Logic 层使用可能导致公共耦合)。
    • 逻辑包设计:将功能相关的类组织在包内,通过包分割实现接口最小化,减少不必要的跨包依赖。
    • 物理包设计:将不同包的重复内容独立为单独包,消除重复,避免隐式重复耦合。
    • 对象创建(Creator 模式):如果 A 和 B 已有较高耦合度,让 A 创建 B (或反之) 不会引入额外耦合。
    • 控制风格:使用 Controller 解耦界面与逻辑对象。
  • 高内聚处理:
    • 分层架构:每一层都应是高内聚的(如 UI 层处理交互,Logic 层处理业务,Data 层处理持久化)。
    • 逻辑包设计:每个包都应聚焦于一组紧密相关的功能。
    • 抽象类职责:类的状态(属性)和方法应紧密联系,共同服务于类的核心职责(信息内聚/功能内聚)。
    • 控制风格:将控制逻辑封装在 Controller 中(功能内聚),但 Controller 自身可能承载顺序/通信/逻辑内聚,需控制其规模。

信息隐藏思想的应用

  • 分层架构:每层隐藏其内部实现细节,只暴露服务接口。层间依赖关系体现了决策稳定性的差异(UI 最易变,数据最稳定)。
  • 物理包设计:将特定决策(如安全处理、网络通信、数据库访问)封装在独立包内。
  • 接口定义:严格要求定义模块和类的接口,是实现信息隐藏的基础,也便利开发。
  • 控制风格:使用 Controller 封装业务逻辑相关的设计决策,避免其散布到整个对象网络中。

Parnas 的模块指南

Parnas 提出用 Module Guide 文档来清晰描述模块设计,作为信息隐藏实践的工具。每个模块的描述应包含:

  1. 主要秘密:模块所隐藏的核心设计决策,通常对应要实现的用户需求或系统特性。这是模块存在的根本原因。
  2. 次要秘密:实现主要秘密所涉及的具体实现细节,如数据结构、算法、硬件平台信息等。
  3. 模块角色:模块在整个系统中所承担的角色、作用以及与其他模块的关联关系。
  4. 对外接口:模块提供给其他模块使用的接口(方法签名、功能描述)。

示例: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. 模块化方案 1(基于功能步骤):
      • 模块:Master Control, Input, Circular Shift, Alphabetizer, Output。
      • 数据共享:通过共享数据区(Characters, Index, Alphabetized Index)传递中间结果。
      • 问题:模块间存在公共耦合和可能的标记/控制耦合。数据结构的改变(如行存储方式)会影响多个模块。不符合信息隐藏原则。
    2. 模块化方案 2(基于信息隐藏):
      • 模块:Master Control, Line Storage, Circular Shifter, Alphabetizer, Output。
      • 核心思想:每个模块隐藏一个主要的设计决策(秘密)。
        • Line Storage:隐藏字符/行的具体存储方式。
        • Circular Shifter:隐藏循环位移的算法和存储方式。
        • Alphabetizer:隐藏排序算法。
      • 交互:通过明确定义的接口进行。
      • 优点:
        • 可修改性:设计决策的变化(如改变行存储方式、位移算法、排序算法)只影响单个模块。
        • 独立开发:定义好接口后,各模块可并行开发。
        • 可理解性:模块职责清晰,接口明确,耦合度低。
  • 结论:基于信息隐藏的模块化方案(方案 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() →\to 遍历 SalesLineItem →\to 调用 SalesLineItem.getSubtotal() →\to 调用 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 类:
    1. B 聚合 A 对象。
    2. B 包含 A 对象(组合关系)。
    3. B 记录 A 的实例。
    4. B 密切使用 A 对象。
    5. 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 的类型(选择):
    1. 门面控制器:代表整个业务或组织(如 POSSystem 类)或整个系统(如 Register 类)。适用于简单场景或只有一个主要控制点的系统。
    2. 角色控制器:代表在现实世界中执行该任务的人或角色(如 Cashier 类)。设计更直观,但可能与领域模型角色混淆。
    3. 用例控制器 或 会话控制器:为每个用例或用户会话创建一个专门的 Controller(如 ProcessSaleController, BuyItemsHandler)。这是最常见的方式,可以保证 Controller 的高内聚(只负责一个用例),但可能导致 Controller 数量增多。通常是推荐的选择。
      • 这种 Controller 通常是「人造」的,在领域模型中没有直接对应物,纯粹为了实现低耦合高内聚的设计目标而创建。
  • 缺点/风险:
    • Controller 可能变得臃肿,承担过多职责,导致自身低内聚、高耦合。需要注意将业务逻辑委托给真正的领域专家对象。Controller 主要负责协调和委托。
具体例子说明

假设我们有一个零售商店的销售系统,需要处理以下用例:

  1. 处理销售:顾客购买商品,生成订单。

  2. 退货:顾客退回商品,退款或换货。

  3. 库存管理:店员更新商品库存。

  4. 门面控制器:代表整个系统或业务,所有请求都通过一个统一的入口处理。

    • 示例:POSSystem 类作为唯一控制器。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public 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 会变得臃肿(比如未来添加会员管理、促销活动等逻辑)。
  5. 角色控制器:代表现实中的角色,比如收银员、店长。

    • 示例:Cashier 和 StoreManager 类分别处理销售和库存。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    public 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 实体,导致职责重叠)。
  6. 用例控制器:每个用例对应一个控制器,高内聚、低耦合。

    • 示例: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):
    1. UI 按钮事件处理器将请求(商品 ID、数量)发送给 ProcessSaleController。
    2. ProcessSaleController 接收请求,调用 Sale 对象的 makeLineItem 方法。
    3. Sale 对象(Creator)创建 SalesLineItem 实例。
    4. Controller 可能还需要协调其他领域对象(如更新库存、获取商品描述等)。
    5. Controller 将结果返回给 UI 进行显示。
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 模式密切相关,影响着系统的整体协作模式。

控制风格

控制风格是指系统行为逻辑在对象(或组件)网络中的分布方式。

主要有三种风格:

  1. 集中式:大部分或所有的决策逻辑和控制流都集中在一个或少数几个「控制器」对象中。其他对象主要作为数据容器或简单执行者。

    • 优点:
      • 容易找到决策点。
      • 容易理解和修改整体流程。
    • 缺点:
      • 控制器容易变得臃肿、复杂,难以理解、维护和测试。
      • 控制器可能将其他对象视为简单的数据仓库,破坏其封装和信息隐藏。
      • 导致高耦合(控制器依赖许多其他对象)和低内聚(控制器承担过多不相关职责)。
    • 表现:顺序图中,消息通常从一个中心控制器发出到多个其他对象。
    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/>其他对象被动执行
  2. 委托式:决策逻辑分布在多个对象中,但主要的、高层的决策仍然由少数几个控制器做出。控制器会将具体的子任务和决策委托给其他更专业的对象(通常是 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 协调支付和库存检查
  3. 分散式:系统行为逻辑广泛地、均匀地分布在大量对象中,没有明显的中心控制器。每个对象只承担很小一部分职责和决策。

    • 优点:(理论上)可能更符合某些去中心化的模型。
    • 缺点:
      • 难以理解控制流,逻辑分散各处。
      • 对象功能过于简单,需要频繁交互,导致高耦合。
      • 难以隐藏信息。
      • 内聚通常很差。
      • 很少能满足模块化原则。
    • 表现:顺序图中,消息在多个对象之间频繁传递,形成复杂的网状结构。
    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 进行高层协调和解耦,同时将具体业务逻辑和决策委托给相应的领域专家对象,以达到较好的低耦合和高内聚。