8架构整洁之道

组件内聚原则

REP, CCP, CRP

复用发布原则共同闭包原则共同复用原则

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第8章 组件内聚原则 - REP, CCP, CRP (Component Cohesion Principles)

一、导读

1.1 本章学习目标

通过本章的学习,你将能够:

理解软件组件的概念及其在大型系统中的重要性

掌握三个组件内聚原则:REP、CCP、CRP 的定义和含义

理解每个原则关注的不同利益相关者视角

识别组件设计中违反内聚原则的模式

学会在不同原则之间进行权衡和决策

理解组件内聚原则与 SOLID 原则的关系和区别

掌握组件演进的策略和常见陷阱

理解这些原则在现代包管理和微服务架构中的应用

1.2 为什么本章如此重要

在前七章中,我们主要关注了类级别的设计原则(SOLID)。然而,当软件系统规模扩大时,单个类的设计质量不足以保证整个系统的可维护性。我们需要在更高的抽象层次——组件(Component)层次——考虑设计问题。

组件是比类更大的代码组织单元,通常对应于包(Package)、模块(Module)、库(Library)或服务(Service)。组件级别的决策影响整个系统的结构、部署方式和团队协作模式。

Robert C. Martin 提出了六个组件设计原则,分为两类:内聚原则(REP、CCP、CRP)和耦合原则(ADP、SDP、SAP)。本章聚焦于内聚原则,它们指导我们如何将类和模块组合成有意义的组件。

这三个原则分别代表不同利益相关者的视角:

  • REP(复用/发布等价原则):从复用者角度,组件应该是可复用的最小单位
  • CCP(共同闭包原则):从开发者角度,一起变化的类应该放在一起
  • CRP(共同复用原则):从使用者角度,不应该被迫依赖不需要的部分

关键在于,这三个原则往往是相互冲突的。遵循 CCP 可能导致违反 CRP,遵循 REP 可能导致违反 CCP。优秀的组件设计需要在这些原则之间找到平衡点,并根据项目的阶段和目标做出适当的权衡。

1.3 核心问题引导

在深入本章内容之前,请思考以下问题:

什么是软件组件?它与类、包、模块有什么区别?

为什么组件级别的設計与类级别的设计不同?

REP、CCP、CRP 分别关注什么问题?它们的视角有何不同?

为什么这三个原则会相互冲突?如何在冲突中做决策?

在项目初期和成熟期,应该优先遵循哪个原则?

如何判断一个组件是否太大或太小?

组件拆分和合并的策略是什么?

这些原则如何应用于微服务架构?


二、核心概念详解

2.1 软件组件的定义

在讨论组件原则之前,首先需要明确"组件"的含义。

软件组件是比类更大的代码组织单元,具有以下特征:

  • 组件由多个类或模块组成
  • 组件是最小的部署单位(可以独立发布版本)
  • 组件有明确的公共接口和内部实现
  • 组件可以被其他组件依赖

在不同语言和生态系统中,组件有不同的名称:

  • Java:JAR 包、Maven 模块
  • Python:Package、Distribution
  • JavaScript:npm Package
  • .NET:Assembly、NuGet Package
  • 微服务:Service

组件级别的设计决策包括:

  • 哪些类应该放在同一个组件中?
  • 组件的公共 API 是什么?
  • 组件之间的依赖关系如何?
  • 组件的版本策略是什么?

2.2 REP:复用/发布等价原则(The Reuse/Release Equivalence Principle)

表述:复用的粒度就是发布的粒度(The granule of reuse is the granule of release)。

含义:如果一个组件要被他人复用,它必须有正式的发布流程,包括版本号、发布说明、变更日志等。反过来,发布的组件应该是可以被复用的。

REP 的核心洞察是:用户不会复用单个类,而是复用整个组件。当你引入一个第三方库时,你引入的是整个 JAR/npm package,而不是其中的某个类。因此,组件内的所有类应该作为一个整体被管理和发布。

REP 的要求:

组件应该有明确的版本号

组件应该有发布说明和变更日志

组件内的类应该协同工作,形成有意义的功能集合

组件应该有稳定的公共 API

REP 的好处:

  • 便于复用和分发
  • 清晰的版本管理
  • 用户可以跟踪变更

REP 的挑战:

  • 需要维护发布流程
  • 组件内的类必须兼容,不能随意添加破坏性变更
  • 可能需要向后兼容

2.3 CCP:共同闭包原则(The Common Closure Principle)

表述:一个组件中所有的类,应该对同一类型的外部变化保持封闭(A component should contain only those classes that are closed for the same types of changes against which it is open)。

含义:CCP 是开闭原则(OCP)在组件层面的体现。如果某些类因为相同的原因而一起变化,它们应该放在同一个组件中。这样,当变化发生时,只需要修改一个组件,而不是分散在多个组件中。

CCP 的核心洞察:将一起变化的类放在一起,可以最小化因变化而需要重新发布和重新测试的组件数量。

CCP 的要求:

识别可能的变化方向

将因相同原因变化的类分组到同一组件

确保组件内的类有高度的内聚性

CCP 的好处:

  • 降低变化的影响范围
  • 减少需要同时修改和发布的组件数量
  • 提高开发效率

CCP 的挑战:

  • 需要预测变化方向
  • 可能与 CRP 冲突(导致组件包含不需要的类)

示例:

假设有一个电商系统,税务计算规则经常因法规变化而修改。
所有与税务计算相关的类(TaxCalculator、TaxRate、TaxRule 等)
应该放在同一个组件中。这样,当税法变化时,只需要修改和发布
这个税务组件,而不影响其他组件。

2.4 CRP:共同复用原则(The Common Reuse Principle)

表述:一个组件中的所有类,应该被一起复用(The classes in a component are reused together)。

含义:如果你复用了组件中的一个类,你就复用了整个组件。因此,组件中的所有类应该是相关的,客户端应该需要组件中的所有(或大部分)类。

CRP 的核心洞察是:组件不应该包含无关的类,因为这会迫使客户端依赖它们不需要的东西。这实际上是接口隔离原则(ISP)在组件层面的体现。

CRP 的要求:

组件中的类应该有紧密的功能关联

客户端使用组件时,应该需要其中的大部分类

避免将无关的类打包在一起

CRP 的好处:

  • 减少不必要的依赖
  • 降低组件间的耦合
  • 使组件更轻量

CRP 的挑战:

  • 可能与 CCP 冲突(一起变化的类可能不被一起复用)
  • 可能导致组件过细,增加管理负担

示例:

违反 CRP:一个 "utils" 组件包含 StringUtils、DateUtils、FileUtils、MathUtils 等。
客户端只需要 StringUtils,但被迫依赖整个 utils 组件,包括它不需要的 DateUtils 等。

符合 CRP:将 utils 拆分为 string-utils、date-utils、file-utils、math-utils 等独立组件。
客户端只依赖它需要的组件。

2.5 三原则的张力与权衡

REP、CCP、CRP 三个原则往往是相互冲突的:

REP vs CCP:

  • REP 要求组件是可复用的完整单元,倾向于较大的组件
  • CCP 要求将一起变化的类放在一起,可能导致组件包含多种功能
  • 冲突点:REP 希望组件稳定,CCP 接受组件因变化而频繁发布

CCP vs CRP:

  • CCP 要求将一起变化的类放在一起,可能导致组件变大
  • CRP 要求组件中的类被一起复用,倾向于较小的组件
  • 冲突点:一起变化的类不一定被一起复用

REP vs CRP:

  • REP 倾向于较大的、功能完整的组件
  • CRP 倾向于较小的、专注的组件
  • 冲突点:复用粒度 vs 依赖精确性

权衡策略:

项目阶段优先原则理由
项目初期CCP快速迭代,减少修改范围
项目成熟期REP稳定发布,便于复用
库/框架开发REP + CRP便于用户按需引用
应用开发CCP便于团队并行开发

Uncle Bob 的建议是:开始时遵循 CCP 和 CRP,随着项目成熟逐渐向 REP 倾斜。在项目早期,开发速度和灵活性更重要;随着系统稳定,发布和复用的重要性增加。

2.6 组件内聚的度量

如何判断一个组件的内聚性是否良好?以下是一些度量指标:

1. 功能性内聚:组件中的类是否共同完成一个明确的功能?

2. 变化相关性:组件中的类是否因相同原因而变化?

3. 复用相关性:组件中的类是否被一起复用?

4. 依赖分析:

  • 传入依赖(Afferent Coupling, Ca):有多少其他组件依赖此组件
  • 传出依赖(Efferent Coupling, Ce):此组件依赖多少其他组件
  • 不稳定性(Instability, I = Ce / (Ca + Ce)):值越接近 1 越不稳定
  • 抽象度(Abstractness, A):抽象类和接口占总类的比例

理想的组件应该在稳定性和抽象度之间取得平衡(位于主序列附近)。


三、程序员的视角

3.1 在实际项目中的应用

在日常工作中,应用组件内聚原则可以从以下几个方面入手:

设计新组件时:

  • 问自己:"这个组件的职责是什么?"
  • 识别可能的变化方向(CCP)
  • 考虑谁会复用这个组件(REP、CRP)
  • 避免创建"杂物抽屉"式的通用组件

审查现有组件时:

  • 检查组件大小是否合理
  • 分析组件内类的变化相关性
  • 查看是否有客户端只使用组件中的一小部分
  • 评估组件的发布频率和原因

重构组件结构时:

  • 从变化最频繁的组件开始
  • 将无关的类提取到新组件
  • 合并高度相关的组件
  • 逐步演进,避免大规模重组

3.2 常见陷阱与误区

陷阱一:创建"utils"或"common"组件

这类组件往往成为各种无关类的聚集地,违反 CRP。应该按功能拆分。

陷阱二:过早追求 REP

在项目初期就建立严格的发布流程,增加了不必要的负担。先关注 CCP 和 CRP。

陷阱三:忽视组件边界

允许组件间随意依赖,导致循环依赖和紧耦合。

陷阱四:过度拆分

将组件拆得过细,增加了管理和集成的复杂度。

3.3 与其他层级的联系

组件内聚原则与软件工程的其他方面密切相关:

  • SOLID 原则:CCP 是 OCP 的组件级体现,CRP 是 ISP 的组件级体现
  • 微服务:服务边界划分需要考虑内聚原则
  • 包管理:npm、Maven 等包管理器体现了 REP
  • 团队协作:组件边界影响团队分工

四、代码实战

4.1 示例一:违反内聚原则的组件设计

// 项目结构(违反内聚原则)

my-app/
├── src/
│   ├── common/           # 违反 CRP:杂物抽屉
│   │   ├── StringUtils.java
│   │   ├── DateUtils.java
│   │   ├── FileUtils.java
│   │   ├── MathUtils.java
│   │   └── JsonUtils.java
│   │
│   ├── data/             # 违反 CCP:不同变化原因混在一起
│   │   ├── UserDAO.java       # 用户数据访问(变化原因:用户模型变化)
│   │   ├── OrderDAO.java      # 订单数据访问(变化原因:订单模型变化)
│   │   ├── ProductDAO.java    # 产品数据访问(变化原因:产品模型变化)
│   │   ├── DatabaseConfig.java # 数据库配置(变化原因:基础设施变化)
│   │   └── ConnectionPool.java # 连接池(变化原因:基础设施变化)
│   │
│   └── service/          # 违反 REP:没有明确的发布边界
│       ├── UserService.java
│       ├── OrderService.java
│       └── ProductService.java

问题分析:

common 组件包含无关的工具类,违反 CRP

data 组件混合了不同实体的 DAO 和基础设施代码,违反 CCP

整个项目没有明确的组件边界和版本管理,违反 REP

4.2 示例二:符合内聚原则的组件设计

// 重构后的项目结构

my-app/
├── components/
│   ├── string-utils/         # 符合 CRP:专注字符串处理
│   │   ├── src/
│   │   │   ├── StringUtils.java
│   │   │   └── StringValidator.java
│   │   ├── pom.xml           # 独立版本:1.0.0
│   │   └── CHANGELOG.md
│   │
│   ├── date-utils/           # 符合 CRP:专注日期处理
│   │   ├── src/
│   │   │   ├── DateUtils.java
│   │   │   └── DateFormatter.java
│   │   ├── pom.xml           # 独立版本:1.0.0
│   │   └── CHANGELOG.md
│   │
│   ├── user-domain/          # 符合 CCP:用户相关变化集中
│   │   ├── src/
│   │   │   ├── User.java
│   │   │   ├── UserRepository.java
│   │   │   ├── UserService.java
│   │   │   └── UserValidator.java
│   │   ├── pom.xml           # 独立版本:2.1.0
│   │   └── CHANGELOG.md
│   │
│   ├── order-domain/         # 符合 CCP:订单相关变化集中
│   │   ├── src/
│   │   │   ├── Order.java
│   │   │   ├── OrderItem.java
│   │   │   ├── OrderRepository.java
│   │   │   ├── OrderService.java
│   │   │   └── OrderCalculator.java
│   │   ├── pom.xml           # 独立版本:1.5.0
│   │   └── CHANGELOG.md
│   │
│   ├── database-infra/       # 符合 CCP:基础设施变化集中
│   │   ├── src/
│   │   │   ├── DatabaseConfig.java
│   │   │   ├── ConnectionPool.java
│   │   │   └── MigrationRunner.java
│   │   ├── pom.xml           # 独立版本:1.0.0
│   │   └── CHANGELOG.md
│   │
│   └── my-app/               # 主应用,依赖上述组件
│       ├── src/
│       │   └── Application.java
│       └── pom.xml
│           # 依赖声明:
│           # - string-utils: ^1.0.0
│           # - user-domain: ^2.0.0
│           # - order-domain: ^1.5.0
│           # - database-infra: ^1.0.0

改进点:

工具类按功能拆分为独立组件,符合 CRP

领域逻辑按业务实体分组,符合 CCP

基础设施代码独立成组件,符合 CCP

每个组件有独立版本号和变更日志,符合 REP

4.3 示例三:Python 项目中的组件内聚

python
"""
展示如何在 Python 项目中应用组件内聚原则

Python 中,组件通常对应于 Distribution(pip installable package)
"""

# === 项目结构 ===

"""
ecommerce/
├── pyproject.toml          # 主项目配置
├── packages/
│   ├── ecommerce-string-utils/
│   │   ├── pyproject.toml  # name = "ecommerce-string-utils", version = "1.0.0"
│   │   └── src/
│   │       └── ecommerce_string_utils/
│   │           ├── __init__.py
│   │           ├── strings.py
│   │           └── validators.py
│   │
│   ├── ecommerce-date-utils/
│   │   ├── pyproject.toml  # name = "ecommerce-date-utils", version = "1.0.0"
│   │   └── src/
│   │       └── ecommerce_date_utils/
│   │           ├── __init__.py
│   │           ├── dates.py
│   │           └── formatters.py
│   │
│   ├── user-domain/
│   │   ├── pyproject.toml  # name = "user-domain", version = "2.1.0"
│   │   └── src/
│   │       └── user_domain/
│   │           ├── __init__.py
│   │           ├── models.py
│   │           ├── repository.py
│   │           ├── service.py
│   │           └── validators.py
│   │
│   └── order-domain/
│       ├── pyproject.toml  # name = "order-domain", version = "1.5.0"
│       └── src/
│           └── order_domain/
│               ├── __init__.py
│               ├── models.py
│               ├── items.py
│               ├── repository.py
│               ├── service.py
│               └── calculator.py
"""

# === string-utils 组件 ===

# packages/ecommerce-string-utils/src/ecommerce_string_utils/strings.py
def capitalize_words(text: str) -> str:
    """将每个单词首字母大写"""
    return ' '.join(word.capitalize() for word in text.split())


def truncate(text: str, max_length: int, suffix: str = '...') -> str:
    """截断文本到指定长度"""
    if len(text) <= max_length:
        return text
    return text[:max_length - len(suffix)] + suffix


# packages/ecommerce-string-utils/src/ecommerce_string_utils/validators.py
def is_valid_email(email: str) -> bool:
    """简单的邮箱验证"""
    return '@' in email and '.' in email.split('@')[1]


def is_valid_phone(phone: str) -> bool:
    """简单的手机号验证"""
    digits = ''.join(c for c in phone if c.isdigit())
    return len(digits) >= 10


# === user-domain 组件 ===

# packages/user-domain/src/user_domain/models.py
from dataclasses import dataclass
from datetime import datetime


@dataclass
class User:
    id: int
    username: str
    email: str
    created_at: datetime
    
    def validate(self) -> list[str]:
        """验证用户数据"""
        errors = []
        if not self.username or len(self.username) < 3:
            errors.append("Username must be at least 3 characters")
        if not self.email or '@' not in self.email:
            errors.append("Invalid email")
        return errors


# packages/user-domain/src/user_domain/repository.py
from typing import Optional
from .models import User


class UserRepository:
    """用户数据仓库接口"""
    
    def find_by_id(self, user_id: int) -> Optional[User]:
        raise NotImplementedError
    
    def find_by_username(self, username: str) -> Optional[User]:
        raise NotImplementedError
    
    def save(self, user: User) -> User:
        raise NotImplementedError


# packages/user-domain/src/user_domain/service.py
from .models import User
from .repository import UserRepository


class UserService:
    def __init__(self, repository: UserRepository):
        self.repository = repository
    
    def register_user(self, username: str, email: str) -> User:
        # 业务逻辑
        user = User(
            id=0,  # 将由 repository 分配
            username=username,
            email=email,
            created_at=datetime.now()
        )
        
        errors = user.validate()
        if errors:
            raise ValueError(f"Invalid user data: {errors}")
        
        return self.repository.save(user)


# === 主应用使用组件 ===

# src/application.py
from user_domain.service import UserService
from user_domain.repository import UserRepository
from order_domain.service import OrderService


class Application:
    def __init__(
        self,
        user_repo: UserRepository,
        # ... 其他依赖
    ):
        self.user_service = UserService(user_repo)
        # self.order_service = OrderService(...)
    
    def register_and_order(self, username: str, email: str):
        user = self.user_service.register_user(username, email)
        # 创建订单等后续逻辑
        pass

五、深入思考

5.1 历史背景

组件内聚原则由 Robert C. Martin 在 2000 年代初提出,作为其组件设计原则体系的一部分。这些原则反映了从类级别到组件级别的设计思维扩展。

REP 的思想源于软件复用和包管理的实践。CCP 是开闭原则在组件层面的自然延伸。CRP 则是接口隔离原则的组件级体现。

5.2 前沿方向

在现代软件工程中,组件内聚原则的应用正在演变:

Monorepo vs Polyrepo:单体仓库和多仓库策略对内聚原则有不同影响。

微服务:服务边界划分是组件内聚原则在分布式系统中的应用。

前端组件化:React/Vue 组件也面临类似的内聚性问题。

Serverless:函数即服务(FaaS)将组件粒度进一步细化。

5.3 延伸阅读

  • 《Clean Architecture》- Robert C. Martin:组件原则的详细讲解
  • 《Building Microservices》- Sam Newman:服务拆分策略
  • 《Monorepo Patterns》- 相关博客文章

六、实验笔记

实验一:分析现有项目的组件结构

目标:分析一个真实项目的组件结构,评估其内聚性。

步骤:

选择一个中等规模的项目

列出所有组件(包/模块)

对每个组件,分析:

- 组件内的类是否相关(CRP)

- 组件内的类是否一起变化(CCP)

- 组件是否有明确的版本管理(REP)

识别违反内聚原则的地方

提出改进建议

实验二:重新设计组件结构

目标:为一个给定的系统设计符合内聚原则的组件结构。

任务:设计一个博客平台的组件结构。


七、本章小结

本章深入探讨了组件内聚原则(REP、CCP、CRP),这是关于如何将类组合成组件的原则。我们学习了:

REP 要求复用的粒度等于发布的粒度,组件应该有明确的版本管理和发布流程。

CCP 要求将一起变化的类放在同一组件中,以最小化变化的影响范围。它是 OCP 在组件层面的体现。

CRP 要求组件中的类被一起复用,避免客户端依赖不需要的部分。它是 ISP 在组件层面的体现。

这三个原则往往相互冲突,需要根据项目阶段和目标进行权衡。项目初期优先 CCP,成熟期优先 REP,库开发优先 CRP。

良好的组件内聚性可以提高代码的可维护性、可复用性和团队协作效率。


八、习题

概念理解:解释 REP、CCP、CRP 分别关注的利益相关者视角。

代码分析:分析你当前项目的组件结构,识别违反内聚原则的地方。

设计练习:为一个电商系统设计组件结构,说明如何应用 REP、CCP、CRP。

权衡分析:在项目初期,为什么 CCP 比 REP 更重要?给出具体理由。

重构实践:将一个"utils"组件拆分为符合 CRP 的多个小组件。

研究题:调研 Monorepo 和 Polyrepo 策略对内聚原则的影响。

批判性思考:有人说"CRP 导致组件过细,增加管理负担"。你同意吗?

微服务应用:如何将组件内聚原则应用于微服务边界划分?

8/12组件内聚
0%