11架构整洁之道

整洁架构

同心圆模型

实体用例接口适配器框架与驱动依赖规则

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

第11章 整洁架构 - 同心圆模型 (The Clean Architecture)

一、导读

1.1 本章学习目标

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

理解整洁架构的核心理念和设计原则

掌握同心圆模型的四个层次及其职责

学会应用依赖规则确保架构的完整性

理解实体、用例、接口适配器和框架驱动层的区别

掌握在不同规模项目中应用整洁架构的方法

学会识别和解决违反整洁架构原则的问题

理解整洁架构与其他架构风格(六边形架构、洋葱架构)的关系

能够在实际项目中设计和实现符合整洁架构的系统

1.2 为什么本章如此重要

在前面章节中,我们学习了 SOLID 原则、组件原则和架构边界。这些知识为我们提供了设计高质量软件的理论基础。然而,理论需要落地,原则需要具体的实践框架。整洁架构(Clean Architecture)正是这样一个将理论转化为实践的完整框架。

Robert C. Martin 在 2012 年提出的整洁架构,不是一个新的发明,而是对过去几十年软件架构最佳实践的系统化总结。它融合了以下经典架构思想:

  • 六边形架构(Hexagonal Architecture) by Alistair Cockburn:强调端口与适配器
  • 洋葱架构(Onion Architecture) by Jeffrey Palermo:强调领域为中心的分层
  • 整洁代码(Clean Code):强调代码的可读性和可维护性
  • 领域驱动设计(DDD):强调业务领域的核心地位

整洁架构的独特价值在于它提供了一个通用的、语言无关的、技术无关的架构模板。无论你在构建 Web 应用、移动应用、桌面应用还是微服务,无论使用 Java、Python、JavaScript 还是 Go,整洁架构的原则都适用。

更重要的是,整洁架构解决了软件开发中最根本的矛盾:业务逻辑的稳定性与技术变化的频繁性之间的矛盾。通过将业务逻辑置于架构的中心,并严格控制依赖方向,整洁架构确保核心业务规则不受外部技术变化的影响。这使得系统能够:

  • 独立于框架:不绑定于特定框架,可以替换
  • 可测试:业务逻辑可以在没有 UI、数据库或网络的情况下测试
  • 独立于 UI:UI 可以变化而不影响业务规则
  • 独立于数据库:可以切换数据库而不影响业务逻辑
  • 独立于外部机构:业务规则不依赖于外部服务

在实际开发中,整洁架构帮助我们做出关于代码组织、依赖管理和模块划分的关键决策。它是应对复杂性的有力武器,也是保证系统长期可维护性的基石。

1.3 核心问题引导

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

什么是整洁架构?它解决了什么问题?

同心圆模型的四个层次分别是什么?各自的职责是什么?

依赖规则的具体含义是什么?为什么依赖必须指向内部?

实体(Entities)和用例(Use Cases)有什么区别?

接口适配器层的作用是什么?如何实现端口与适配器模式?

框架和驱动层应该包含什么内容?

整洁架构与 MVC、MVVM 等模式有什么关系?

在小项目中应用整洁架构是否会过度设计?

如何逐步将现有系统重构为整洁架构?

整洁架构在微服务和分布式系统中如何应用?


二、核心概念详解

2.1 整洁架构的起源与理念

2.1.1 历史背景

整洁架构的概念最早由 Robert C. Martin(Uncle Bob)在 2012 年的一篇博客文章《The Clean Architecture》中提出。这篇文章迅速在软件工程社区引起广泛关注,成为现代软件架构的重要参考。

Martin 提出整洁架构的动机是观察到许多软件项目面临的共同问题:

框架锁定:系统紧密绑定于特定框架,难以迁移

测试困难:业务逻辑与 UI、数据库紧耦合,难以单元测试

技术债务:随着时间推移,系统变得越来越难维护

业务逻辑泄漏:核心业务规则分散在各处,难以理解和修改

整洁架构的目标是解决这些问题,提供一个能够抵御时间侵蚀的架构方案。

2.1.2 核心理念

整洁架构的核心理念可以概括为一句话:将软件划分为不同的层次,确保依赖关系从外层指向内层,使核心业务逻辑独立于外部细节。

这个理念包含三个关键要素:

分层:软件被划分为同心圆式的层次

依赖方向:依赖只能从外层指向内层

独立性:内层不应该知道外层的任何细节

2.1.3 与其他架构的关系

整洁架构不是孤立的,它与许多经典架构风格有密切联系:

架构风格核心思想与整洁架构的关系
六边形架构端口与适配器整洁架构吸收了端口与适配器的概念
洋葱架构领域为中心整洁架构的同心圆模型源自洋葱架构
分层架构水平分层整洁架构是分层的特例,增加了依赖规则
DDD领域模型为核心整洁架构的实体层对应 DDD 的领域层

可以说,整洁架构是这些经典架构思想的集大成者,它提供了一个统一的视角来理解和应用这些原则。

2.2 同心圆模型

整洁架构最直观的表示是同心圆模型:

┌─────────────────────────────────────┐
                    │     Frameworks & Drivers            │
                    │     (框架和驱动层)                   │
                    │  ┌───────────────────────────────┐  │
                    │  │   Interface Adapters          │  │
                    │  │   (接口适配器层)               │  │
                    │  │  ┌─────────────────────────┐  │  │
                    │  │  │   Application Business  │  │  │
                    │  │  │   Rules                 │  │  │
                    │  │  │   (应用业务规则层)       │  │  │
                    │  │  │  ┌───────────────────┐  │  │  │
                    │  │  │  │   Enterprise      │  │  │  │
                    │  │  │  │   Business Rules  │  │  │  │
                    │  │  │  │   (企业业务规则层)  │  │  │  │
                    │  │  │  └───────────────────┘  │  │  │
                    │  │  └─────────────────────────┘  │  │
                    │  └───────────────────────────────┘  │
                    └─────────────────────────────────────┘

从内到外,四个层次分别是:

2.2.1 第一层:企业业务规则层(Entities)

别名:实体层、领域层、核心层

职责:包含系统最关键的业务逻辑和业务规则。这些规则是企业级别的,不会因为应用方式的变化而变化。

特点:

  • 最稳定,变化最少
  • 不依赖任何其他层
  • 可以被多个应用共享
  • 通常是纯对象,没有框架依赖

包含内容:

  • 实体类(Entity Classes)
  • 领域模型(Domain Models)
  • 业务规则(Business Rules)
  • 领域服务(Domain Services)

示例:

java
// 银行系统的账户实体
public class BankAccount {
    private String accountId;
    private String ownerName;
    private BigDecimal balance;
    private AccountStatus status;
    
    public BankAccount(String accountId, String ownerName) {
        this.accountId = accountId;
        this.ownerName = ownerName;
        this.balance = BigDecimal.ZERO;
        this.status = AccountStatus.ACTIVE;
    }
    
    // 业务规则:取款不能超过余额
    public void withdraw(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        if (amount.compareTo(balance) > 0) {
            throw new InsufficientFundsException("Insufficient funds");
        }
        if (status != AccountStatus.ACTIVE) {
            throw new AccountInactiveException("Account is inactive");
        }
        this.balance = this.balance.subtract(amount);
    }
    
    // 业务规则:存款必须为正数
    public void deposit(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        if (status != AccountStatus.ACTIVE) {
            throw new AccountInactiveException("Account is inactive");
        }
        this.balance = this.balance.add(amount);
    }
    
    // Getters
    public String getAccountId() { return accountId; }
    public String getOwnerName() { return ownerName; }
    public BigDecimal getBalance() { return balance; }
    public AccountStatus getStatus() { return status; }
}

注意:这个实体类没有任何框架注解、数据库映射或 UI 相关代码。它是纯粹的业务逻辑。

2.2.2 第二层:应用业务规则层(Use Cases)

别名:用例层、应用层、服务层

职责:协调实体完成特定的应用功能。它实现了系统的所有用例,如"用户注册"、"创建订单"、"发送通知"等。

特点:

  • 依赖实体层,但不依赖外层
  • 封装了应用特有的业务规则
  • 不包含 UI、数据库或框架代码
  • 通常以用例或服务的形式组织

包含内容:

  • 用例类(Use Case Classes)
  • 应用服务(Application Services)
  • 输入/输出端口(Input/Output Ports)
  • DTO(Data Transfer Objects)

示例:

java
// 转账用例
public class TransferMoneyUseCase {
    private final AccountRepository accountRepository;
    private final TransactionHistoryRepository transactionRepository;
    private final NotificationService notificationService;
    
    public TransferMoneyUseCase(
            AccountRepository accountRepository,
            TransactionHistoryRepository transactionRepository,
            NotificationService notificationService) {
        this.accountRepository = accountRepository;
        this.transactionRepository = transactionRepository;
        this.notificationService = notificationService;
    }
    
    public TransferResult execute(TransferRequest request) {
        // 1. 获取源账户和目标账户
        BankAccount sourceAccount = accountRepository.findById(request.getSourceAccountId());
        BankAccount targetAccount = accountRepository.findById(request.getTargetAccountId());
        
        // 2. 执行业务规则
        sourceAccount.withdraw(request.getAmount());
        targetAccount.deposit(request.getAmount());
        
        // 3. 保存账户状态
        accountRepository.save(sourceAccount);
        accountRepository.save(targetAccount);
        
        // 4. 记录交易历史
        Transaction transaction = new Transaction(
            request.getSourceAccountId(),
            request.getTargetAccountId(),
            request.getAmount(),
            LocalDateTime.now()
        );
        transactionRepository.save(transaction);
        
        // 5. 发送通知
        notificationService.notifyTransferCompleted(
            request.getSourceAccountId(),
            request.getTargetAccountId(),
            request.getAmount()
        );
        
        return new TransferResult(true, "Transfer completed successfully");
    }
}

注意:用例层通过接口(Repository、Service)与外层交互,不直接依赖具体实现。

2.2.3 第三层:接口适配器层(Interface Adapters)

别名:适配器层、基础设施层、端口实现层

职责:将外层的数据格式转换为内层需要的格式,反之亦然。它实现了内层定义的接口,连接业务逻辑与外部世界。

特点:

  • 实现内层定义的接口
  • 处理数据转换
  • 可能依赖外部库,但对外屏蔽细节
  • 可以包含控制器、Presenter、Gateway 等

包含内容:

  • 控制器(Controllers)
  • Presenter / ViewModel
  • Gateway 实现
  • Repository 实现
  • 数据转换器(Mappers)

示例:

java
// Repository 接口的 JPA 实现
@Repository
public class JpaAccountRepository implements AccountRepository {
    
    @Autowired
    private AccountJpaRepository jpaRepository;
    
    @Override
    public BankAccount findById(String accountId) {
        AccountEntity entity = jpaRepository.findByAccountId(accountId);
        return mapToDomain(entity);
    }
    
    @Override
    public void save(BankAccount account) {
        AccountEntity entity = mapToEntity(account);
        jpaRepository.save(entity);
    }
    
    // 数据转换
    private BankAccount mapToDomain(AccountEntity entity) {
        BankAccount account = new BankAccount(
            entity.getAccountId(),
            entity.getOwnerName()
        );
        // 设置其他属性...
        return account;
    }
    
    private AccountEntity mapToEntity(BankAccount account) {
        AccountEntity entity = new AccountEntity();
        entity.setAccountId(account.getAccountId());
        entity.setOwnerName(account.getOwnerName());
        entity.setBalance(account.getBalance());
        // 设置其他属性...
        return entity;
    }
}

注意:适配器层负责数据格式的转换,确保内层不需要知道外部数据的结构。

2.2.4 第四层:框架和驱动层(Frameworks & Drivers)

别名:框架层、外部层、工具层

职责:包含所有外部框架、库、工具和平台。这一层是实现细节,可以随时替换而不影响内层。

特点:

  • 完全依赖外部技术
  • 最容易变化
  • 对内层透明
  • 包括数据库、Web 框架、消息队列等

包含内容:

  • 数据库(MySQL、PostgreSQL、MongoDB 等)
  • Web 框架(Spring Boot、Express、Django 等)
  • UI 框架(React、Angular、Vue 等)
  • 消息队列(Kafka、RabbitMQ 等)
  • 云服务(AWS、Azure、GCP 等)
  • 第三方 API

示例:

java
// Spring Boot 主应用类 - 框架层
@SpringBootApplication
@EntityScan("com.bank.infrastructure.persistence")
@EnableJpaRepositories("com.bank.infrastructure.persistence")
public class BankApplication {
    public static void main(String[] args) {
        SpringApplication.run(BankApplication.class, args);
    }
}

// REST Controller - 框架层
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
    
    @Autowired
    private CreateAccountUseCase createAccountUseCase;
    
    @PostMapping
    public ResponseEntity<AccountResponse> createAccount(
            @RequestBody CreateAccountRequest request) {
        AccountResponse response = createAccountUseCase.execute(
            mapToRequest(request)
        );
        return ResponseEntity.ok(response);
    }
}

注意:框架层只负责接收请求和返回响应,真正的业务逻辑在用例层。

2.3 依赖规则

整洁架构的核心规则是:源代码依赖只能从外层指向内层。

外层 --> 内层  ✓ 允许
内层 --> 外层  ✗ 禁止

这意味着:

  • 实体层不依赖任何其他层
  • 用例层只依赖实体层
  • 适配器层依赖用例层和实体层
  • 框架层可以依赖所有内层

如何实现反向依赖?

当内层需要调用外层的功能时(如用例层需要访问数据库),使用依赖倒置:

在内层定义接口(端口)

在外层实现接口(适配器)

通过依赖注入将实现传入内层

┌─────────────────────────────────────┐
│         框架层                       │
│  ┌───────────────────────────────┐  │
│  │       适配器层                 │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │     用例层              │  │  │
│  │  │  依赖 ↑                 │  │  │
│  │  │  接口 ↓ 实现            │  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

依赖规则的益处:

稳定性:内层不受外层变化的影响

可测试性:可以用 mock 替代外层进行测试

可替换性:可以轻松替换外层实现

清晰性:依赖方向明确,易于理解

2.4 跨越边界的策略

在整洁架构中,数据跨越边界时需要转换格式。常见的策略包括:

2.4.1 数据结构转换

java
// 外层 DTO
public class CreateAccountRequestDto {
    private String ownerName;
    private String email;
    private String initialDeposit;
}

// 内层请求对象
public class CreateAccountRequest {
    private String ownerName;
    private String email;
    private BigDecimal initialDeposit;
}

// 转换器
public class AccountRequestMapper {
    public static CreateAccountRequest toDomain(CreateAccountRequestDto dto) {
        CreateAccountRequest request = new CreateAccountRequest();
        request.setOwnerName(dto.getOwnerName());
        request.setEmail(dto.getEmail());
        request.setInitialDeposit(new BigDecimal(dto.getInitialDeposit()));
        return request;
    }
}

2.4.2 接口适配

java
// 内层定义的接口
public interface NotificationService {
    void sendEmail(String to, String subject, String body);
}

// 外层实现
@Service
public class SendGridNotificationService implements NotificationService {
    
    @Autowired
    private SendGridClient sendGridClient;
    
    @Override
    public void sendEmail(String to, String subject, String body) {
        sendGridClient.send(new Email(to, subject, body));
    }
}

2.4.3 事件解耦

java
// 内层发布事件
public class OrderCreatedEvent {
    private String orderId;
    private String customerId;
    private BigDecimal amount;
}

// 外层监听事件
@Component
public class OrderEventListener {
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 发送通知、更新索引等...
    }
}

三、程序员的视角

3.1 在实际项目中的应用

3.1.1 项目结构组织

在实际项目中,整洁架构的代码组织有多种方式:

方式一:按层组织

src/
├── entities/           # 实体层
├── usecases/           # 用例层
├── adapters/           # 适配器层
└── frameworks/         # 框架层

优点:层次清晰,符合整洁架构的概念模型。

缺点:同一功能的代码分散在不同目录,导航不便。

方式二:按功能组织(推荐)

src/
├── account/
│   ├── domain/         # 账户实体
│   ├── usecase/        # 账户用例
│   ├── adapter/        # 账户适配器
│   └── framework/      # 账户框架集成
├── transfer/
│   ├── domain/
│   ├── usecase/
│   ├── adapter/
│   └── framework/
└── shared/
    ├── domain/
    ├── usecase/
    ├── adapter/
    └── framework/

优点:同一功能的代码在一起,便于开发和导航。

缺点:需要确保跨功能的依赖遵循整洁架构规则。

方式三:混合组织

src/
├── domain/             # 共享实体
├── application/        # 共享用例
├── features/           # 按功能组织的特性
│   ├── account/
│   ├── transfer/
│   └── ...
└── infrastructure/     # 共享基础设施

这是大型项目的常见组织方式,平衡了层次清晰和功能内聚。

3.1.2 渐进式采用

对于已有项目,可以采用渐进式方式引入整洁架构:

阶段一:识别核心业务逻辑

  • 找出系统中的核心业务规则
  • 将它们提取到独立的类中
  • 移除框架依赖

阶段二:定义用例

  • 识别系统的主要用例
  • 将用例逻辑从控制器中提取出来
  • 定义输入/输出接口

阶段三:实现适配器

  • 为外部依赖定义接口
  • 实现适配器
  • 通过依赖注入连接

阶段四:重构框架层

  • 清理框架特定的代码
  • 确保框架代码只在外层
  • 建立清晰的入口点

3.1.3 测试策略

整洁架构天然支持分层测试:

层次测试类型测试重点工具
实体层单元测试业务规则正确性JUnit, pytest
用例层单元测试用例流程正确性Mock 框架
适配器层集成测试数据转换正确性TestContainers
框架层E2E 测试端到端流程Selenium, Cypress

实体层测试示例:

java
@Test
public void testWithdrawInsufficientFunds() {
    BankAccount account = new BankAccount("123", "John");
    account.deposit(new BigDecimal("100"));
    
    assertThrows(InsufficientFundsException.class, () -> {
        account.withdraw(new BigDecimal("150"));
    });
}

用例层测试示例:

java
@Test
public void testTransferSuccess() {
    // Mock 依赖
    AccountRepository accountRepo = mock(AccountRepository.class);
    TransactionRepository txRepo = mock(TransactionRepository.class);
    NotificationService notifier = mock(NotificationService.class);
    
    // 准备测试数据
    BankAccount source = new BankAccount("S1", "Alice");
    source.deposit(new BigDecimal("1000"));
    BankAccount target = new BankAccount("T1", "Bob");
    
    when(accountRepo.findById("S1")).thenReturn(source);
    when(accountRepo.findById("T1")).thenReturn(target);
    
    // 执行用例
    TransferMoneyUseCase useCase = new TransferMoneyUseCase(accountRepo, txRepo, notifier);
    TransferResult result = useCase.execute(new TransferRequest("S1", "T1", new BigDecimal("500")));
    
    // 验证结果
    assertTrue(result.isSuccess());
    assertEquals(new BigDecimal("500"), source.getBalance());
    assertEquals(new BigDecimal("500"), target.getBalance());
}

3.2 常见陷阱与误区

3.2.1 过度工程化

陷阱:在小项目中强行套用完整的四层结构。

症状:

  • 简单的 CRUD 操作也分成四层
  • 大量的接口和抽象类
  • 开发者抱怨"太复杂"

解决方案:根据项目规模调整架构粒度。小项目可以合并某些层,只要保持依赖方向正确即可。

3.2.2 贫血领域模型

陷阱:实体只有 getter/setter,业务逻辑在用例层。

症状:

  • 实体类没有行为方法
  • 用例类包含大量业务逻辑
  • 违反面向对象原则

解决方案:将业务规则放回实体,用例只负责协调。

3.2.3 泄漏的抽象

陷阱:内层暴露了外层实现的细节。

症状:

  • 实体中包含 JPA 注解
  • 用例返回 Entity 对象
  • 适配器直接操作 DTO

解决方案:严格进行数据转换,确保每层只暴露自己的数据结构。

3.2.4 错误的依赖方向

陷阱:内层直接或间接依赖外层。

症状:

  • 实体导入 Spring 注解
  • 用例导入 HTTP 相关类
  • 循环依赖

解决方案:使用依赖倒置,在内层定义接口,外层实现。

3.3 与其他层级的联系

与 SOLID 原则的联系:

  • SRP:每层有单一职责
  • OCP:通过接口扩展功能
  • LSP:适配器可以互换
  • ISP:细粒度的接口定义
  • DIP:依赖倒置是整洁架构的核心机制

与组件原则的联系:

  • REP/CCP/CRP:决定类的分组
  • ADP/SDP/SAP:决定组件间的依赖

与架构边界的联系:

  • 每一层都是一个边界
  • 依赖规则就是边界穿越规则

四、代码实战

4.1 示例一:博客系统的整洁架构实现

让我们构建一个简单的博客系统,完整展示整洁架构的应用。

需求:

  • 用户可以创建、编辑、删除文章
  • 用户可以评论文章
  • 系统需要支持多种存储后端(内存、文件、数据库)
  • 系统需要支持多种输出格式(JSON、HTML)

项目结构:

blog/
├── domain/
│   ├── model/
│   │   ├── Article.java
│   │   ├── Comment.java
│   │   └── Author.java
│   ├── repository/
│   │   ├── ArticleRepository.java
│   │   └── CommentRepository.java
│   └── service/
│       └── DomainService.java
├── usecase/
│   ├── CreateArticleUseCase.java
│   ├── UpdateArticleUseCase.java
│   ├── DeleteArticleUseCase.java
│   ├── GetArticleUseCase.java
│   ├── AddCommentUseCase.java
│   └── ListArticlesUseCase.java
├── adapter/
│   ├── repository/
│   │   ├── InMemoryArticleRepository.java
│   │   ├── FileArticleRepository.java
│   │   └── JpaArticleRepository.java
│   ├── presenter/
│   │   ├── JsonArticlePresenter.java
│   │   └── HtmlArticlePresenter.java
│   └── controller/
│       └── ArticleController.java
└── framework/
    ├── config/
    │   └── AppConfig.java
    └── BlogApplication.java

领域层 - 实体:

java
// domain/model/Article.java
package blog.domain.model;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class Article {
    private final String id;
    private String title;
    private String content;
    private final Author author;
    private final List<Comment> comments;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    public Article(String title, String content, Author author) {
        validateTitle(title);
        validateContent(content);
        
        this.id = UUID.randomUUID().toString();
        this.title = title;
        this.content = content;
        this.author = author;
        this.comments = new ArrayList<>();
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }
    
    private void validateTitle(String title) {
        if (title == null || title.trim().isEmpty()) {
            throw new IllegalArgumentException("Title cannot be empty");
        }
        if (title.length() > 200) {
            throw new IllegalArgumentException("Title too long");
        }
    }
    
    private void validateContent(String content) {
        if (content == null || content.trim().isEmpty()) {
            throw new IllegalArgumentException("Content cannot be empty");
        }
    }
    
    public void update(String title, String content) {
        validateTitle(title);
        validateContent(content);
        this.title = title;
        this.content = content;
        this.updatedAt = LocalDateTime.now();
    }
    
    public void addComment(Comment comment) {
        this.comments.add(comment);
    }
    
    // Getters
    public String getId() { return id; }
    public String getTitle() { return title; }
    public String getContent() { return content; }
    public Author getAuthor() { return author; }
    public List<Comment> getComments() { return comments; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public LocalDateTime getUpdatedAt() { return updatedAt; }
}
java
// domain/model/Author.java
package blog.domain.model;

public class Author {
    private final String id;
    private final String name;
    private final String email;
    
    public Author(String id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // Getters
    public String getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}
java
// domain/model/Comment.java
package blog.domain.model;

import java.time.LocalDateTime;

public class Comment {
    private final String id;
    private final String content;
    private final String authorName;
    private final LocalDateTime createdAt;
    
    public Comment(String content, String authorName) {
        if (content == null || content.trim().isEmpty()) {
            throw new IllegalArgumentException("Comment cannot be empty");
        }
        this.id = java.util.UUID.randomUUID().toString();
        this.content = content;
        this.authorName = authorName;
        this.createdAt = LocalDateTime.now();
    }
    
    // Getters
    public String getId() { return id; }
    public String getContent() { return content; }
    public String getAuthorName() { return authorName; }
    public LocalDateTime getCreatedAt() { return createdAt; }
}

领域层 - 仓储接口:

java
// domain/repository/ArticleRepository.java
package blog.domain.repository;

import blog.domain.model.Article;
import java.util.List;
import java.util.Optional;

public interface ArticleRepository {
    Article save(Article article);
    Optional<Article> findById(String id);
    List<Article> findAll();
    void deleteById(String id);
}
java
// domain/repository/CommentRepository.java
package blog.domain.repository;

import blog.domain.model.Comment;
import java.util.List;

public interface CommentRepository {
    Comment save(Comment comment);
    List<Comment> findByArticleId(String articleId);
}

用例层:

java
// usecase/CreateArticleUseCase.java
package blog.usecase;

import blog.domain.model.Article;
import blog.domain.model.Author;
import blog.domain.repository.ArticleRepository;

public class CreateArticleUseCase {
    private final ArticleRepository articleRepository;
    
    public CreateArticleUseCase(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }
    
    public Article execute(String title, String content, Author author) {
        Article article = new Article(title, content, author);
        return articleRepository.save(article);
    }
}
java
// usecase/GetArticleUseCase.java
package blog.usecase;

import blog.domain.model.Article;
import blog.domain.repository.ArticleRepository;

public class GetArticleUseCase {
    private final ArticleRepository articleRepository;
    
    public GetArticleUseCase(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }
    
    public Article execute(String articleId) {
        return articleRepository.findById(articleId)
            .orElseThrow(() -> new IllegalArgumentException("Article not found: " + articleId));
    }
}
java
// usecase/AddCommentUseCase.java
package blog.usecase;

import blog.domain.model.Article;
import blog.domain.model.Comment;
import blog.domain.repository.ArticleRepository;

public class AddCommentUseCase {
    private final ArticleRepository articleRepository;
    
    public AddCommentUseCase(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }
    
    public Comment execute(String articleId, String content, String authorName) {
        Article article = articleRepository.findById(articleId)
            .orElseThrow(() -> new IllegalArgumentException("Article not found"));
        
        Comment comment = new Comment(content, authorName);
        article.addComment(comment);
        articleRepository.save(article);
        
        return comment;
    }
}

适配器层 - 仓储实现:

java
// adapter/repository/InMemoryArticleRepository.java
package blog.adapter.repository;

import blog.domain.model.Article;
import blog.domain.repository.ArticleRepository;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class InMemoryArticleRepository implements ArticleRepository {
    private final Map<String, Article> store = new ConcurrentHashMap<>();
    
    @Override
    public Article save(Article article) {
        store.put(article.getId(), article);
        return article;
    }
    
    @Override
    public Optional<Article> findById(String id) {
        return Optional.ofNullable(store.get(id));
    }
    
    @Override
    public List<Article> findAll() {
        return new ArrayList<>(store.values());
    }
    
    @Override
    public void deleteById(String id) {
        store.remove(id);
    }
}

适配器层 - Presenter:

java
// adapter/presenter/JsonArticlePresenter.java
package blog.adapter.presenter;

import blog.domain.model.Article;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonArticlePresenter {
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    public String present(Article article) {
        try {
            return objectMapper.writeValueAsString(article);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize article", e);
        }
    }
    
    public String presentList(List<Article> articles) {
        try {
            return objectMapper.writeValueAsString(articles);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize articles", e);
        }
    }
}
java
// adapter/presenter/HtmlArticlePresenter.java
package blog.adapter.presenter;

import blog.domain.model.Article;

public class HtmlArticlePresenter {
    
    public String present(Article article) {
        return String.format(
            "<article>" +
            "<h1>%s</h1>" +
            "<div class='meta'>By %s on %s</div>" +
            "<div class='content'>%s</div>" +
            "</article>",
            escapeHtml(article.getTitle()),
            escapeHtml(article.getAuthor().getName()),
            article.getCreatedAt(),
            escapeHtml(article.getContent())
        );
    }
    
    private String escapeHtml(String text) {
        if (text == null) return "";
        return text.replace("&", "&amp;")
                   .replace("<", "&lt;")
                   .replace(">", "&gt;")
                   .replace("\"", "&quot;");
    }
}

适配器层 - Controller:

java
// adapter/controller/ArticleController.java
package blog.adapter.controller;

import blog.domain.model.Article;
import blog.domain.model.Author;
import blog.usecase.CreateArticleUseCase;
import blog.usecase.GetArticleUseCase;
import blog.adapter.presenter.JsonArticlePresenter;

import java.util.HashMap;
import java.util.Map;

public class ArticleController {
    private final CreateArticleUseCase createArticleUseCase;
    private final GetArticleUseCase getArticleUseCase;
    private final JsonArticlePresenter presenter;
    
    public ArticleController(
            CreateArticleUseCase createArticleUseCase,
            GetArticleUseCase getArticleUseCase,
            JsonArticlePresenter presenter) {
        this.createArticleUseCase = createArticleUseCase;
        this.getArticleUseCase = getArticleUseCase;
        this.presenter = presenter;
    }
    
    public Map<String, Object> createArticle(Map<String, String> request) {
        Author author = new Author("1", request.get("authorName"), request.get("authorEmail"));
        Article article = createArticleUseCase.execute(
            request.get("title"),
            request.get("content"),
            author
        );
        
        Map<String, Object> response = new HashMap<>();
        response.put("success", true);
        response.put("data", presenter.present(article));
        return response;
    }
    
    public Map<String, Object> getArticle(String articleId) {
        Article article = getArticleUseCase.execute(articleId);
        
        Map<String, Object> response = new HashMap<>();
        response.put("success", true);
        response.put("data", presenter.present(article));
        return response;
    }
}

框架层:

java
// framework/config/AppConfig.java
package blog.framework.config;

import blog.adapter.presenter.JsonArticlePresenter;
import blog.adapter.repository.InMemoryArticleRepository;
import blog.domain.repository.ArticleRepository;
import blog.usecase.CreateArticleUseCase;
import blog.usecase.GetArticleUseCase;
import blog.adapter.controller.ArticleController;

public class AppConfig {
    
    public ArticleController createArticleController() {
        ArticleRepository repository = new InMemoryArticleRepository();
        CreateArticleUseCase createUseCase = new CreateArticleUseCase(repository);
        GetArticleUseCase getUseCase = new GetArticleUseCase(repository);
        JsonArticlePresenter presenter = new JsonArticlePresenter();
        
        return new ArticleController(createUseCase, getUseCase, presenter);
    }
}

4.2 示例二:Python 实现的待办事项应用

python
# domain/model.py
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Optional


class TodoStatus(Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"


@dataclass
class TodoItem:
    id: str
    title: str
    description: Optional[str]
    status: TodoStatus
    created_at: datetime
    updated_at: datetime
    
    def __post_init__(self):
        if not self.title or not self.title.strip():
            raise ValueError("Title cannot be empty")
    
    def update(self, title: str, description: Optional[str]):
        if not title or not title.strip():
            raise ValueError("Title cannot be empty")
        self.title = title
        self.description = description
        self.updated_at = datetime.now()
    
    def mark_completed(self):
        self.status = TodoStatus.COMPLETED
        self.updated_at = datetime.now()
    
    def mark_in_progress(self):
        if self.status == TodoStatus.PENDING:
            self.status = TodoStatus.IN_PROGRESS
            self.updated_at = datetime.now()
python
# domain/repository.py
from abc import ABC, abstractmethod
from typing import List, Optional
from domain.model import TodoItem


class TodoRepository(ABC):
    @abstractmethod
    def save(self, todo: TodoItem) -> TodoItem:
        pass
    
    @abstractmethod
    def find_by_id(self, todo_id: str) -> Optional[TodoItem]:
        pass
    
    @abstractmethod
    def find_all(self) -> List[TodoItem]:
        pass
    
    @abstractmethod
    def delete(self, todo_id: str):
        pass
python
# usecase/create_todo.py
from domain.model import TodoItem, TodoStatus
from domain.repository import TodoRepository
from datetime import datetime
import uuid


class CreateTodoUseCase:
    def __init__(self, repository: TodoRepository):
        self._repository = repository
    
    def execute(self, title: str, description: str = None) -> TodoItem:
        todo = TodoItem(
            id=str(uuid.uuid4()),
            title=title,
            description=description,
            status=TodoStatus.PENDING,
            created_at=datetime.now(),
            updated_at=datetime.now()
        )
        return self._repository.save(todo)
python
# adapter/repository/in_memory.py
from domain.model import TodoItem
from domain.repository import TodoRepository
from typing import List, Optional


class InMemoryTodoRepository(TodoRepository):
    def __init__(self):
        self._store = {}
    
    def save(self, todo: TodoItem) -> TodoItem:
        self._store[todo.id] = todo
        return todo
    
    def find_by_id(self, todo_id: str) -> Optional[TodoItem]:
        return self._store.get(todo_id)
    
    def find_all(self) -> List[TodoItem]:
        return list(self._store.values())
    
    def delete(self, todo_id: str):
        if todo_id in self._store:
            del self._store[todo_id]
python
# adapter/presenter/json_presenter.py
import json
from domain.model import TodoItem


class JsonTodoPresenter:
    def present(self, todo: TodoItem) -> str:
        data = {
            'id': todo.id,
            'title': todo.title,
            'description': todo.description,
            'status': todo.status.value,
            'created_at': todo.created_at.isoformat(),
            'updated_at': todo.updated_at.isoformat()
        }
        return json.dumps(data)
    
    def present_list(self, todos: list) -> str:
        data = [self._to_dict(todo) for todo in todos]
        return json.dumps(data)
    
    def _to_dict(self, todo: TodoItem) -> dict:
        return {
            'id': todo.id,
            'title': todo.title,
            'description': todo.description,
            'status': todo.status.value,
            'created_at': todo.created_at.isoformat(),
            'updated_at': todo.updated_at.isoformat()
        }

4.3 示例三:测试驱动开发中的整洁架构

java
// 测试:实体层
public class ArticleTest {
    
    @Test
    public void testCreateArticleWithValidData() {
        Author author = new Author("1", "John", "john@example.com");
        Article article = new Article("Test Title", "Test Content", author);
        
        assertNotNull(article.getId());
        assertEquals("Test Title", article.getTitle());
        assertEquals("Test Content", article.getContent());
    }
    
    @Test
    public void testCreateArticleWithEmptyTitle() {
        Author author = new Author("1", "John", "john@example.com");
        
        assertThrows(IllegalArgumentException.class, () -> {
            new Article("", "Content", author);
        });
    }
    
    @Test
    public void testUpdateArticle() {
        Author author = new Author("1", "John", "john@example.com");
        Article article = new Article("Old Title", "Old Content", author);
        
        article.update("New Title", "New Content");
        
        assertEquals("New Title", article.getTitle());
        assertEquals("New Content", article.getContent());
    }
}

// 测试:用例层
public class CreateArticleUseCaseTest {
    
    private ArticleRepository repository;
    private CreateArticleUseCase useCase;
    
    @BeforeEach
    public void setUp() {
        repository = mock(ArticleRepository.class);
        useCase = new CreateArticleUseCase(repository);
    }
    
    @Test
    public void testCreateArticleSuccess() {
        Author author = new Author("1", "John", "john@example.com");
        when(repository.save(any(Article.class))).thenAnswer(invocation -> invocation.getArgument(0));
        
        Article result = useCase.execute("Title", "Content", author);
        
        assertNotNull(result);
        assertEquals("Title", result.getTitle());
        verify(repository).save(any(Article.class));
    }
}

五、深入思考

5.1 历史背景

整洁架构的演进历程:

  • 1970s:结构化编程奠定了模块化基础
  • 1990s:面向对象编程引入了封装和抽象
  • 2003:Evans 的 DDD 强调了领域模型的核心地位
  • 2005:Cockburn 的六边形架构提出了端口与适配器
  • 2008:Palermo 的洋葱架构展示了同心圆分层
  • 2012:Martin 正式提出整洁架构,整合了上述思想

整洁架构不是革命,而是进化。它将经过时间检验的最佳实践整合为一个连贯的框架。

5.2 前沿方向

1. 响应式整洁架构

结合响应式编程(Reactive Programming)与整洁架构:

  • 用例返回 Flux/Mono 而不是阻塞结果
  • 适配器处理异步数据流
  • 保持依赖规则不变

2. 函数式整洁架构

使用函数式编程范式实现整洁架构:

  • 实体是不可变数据结构
  • 用例是纯函数
  • 适配器处理副作用

3. 事件溯源与整洁架构

将事件溯源融入整洁架构:

  • 实体从事件流重建状态
  • 用例发布领域事件
  • 适配器持久化事件

5.3 延伸阅读

  • 《Clean Architecture》by Robert C. Martin - 原著
  • 《Domain-Driven Design》by Eric Evans - DDD 经典
  • 《Implementing Domain-Driven Design》by Vaughn Vernon - DDD 实践指南
  • 《Hexagonal Architecture》by Alistair Cockburn - 原始论文

六、实验笔记

实验一:从零构建整洁架构的博客系统

目标:使用整洁架构构建一个完整的博客系统,支持文章的 CRUD 操作。

步骤:

设计领域模型(Article、Author、Comment)

定义仓储接口

实现核心用例(CreateArticle、GetArticle、UpdateArticle、DeleteArticle)

实现至少两种仓储适配器(内存、文件)

实现至少两种 Presenter(JSON、HTML)

编写完整的单元测试

集成到一个简单的 Web 框架中

预期产出:一个可运行的博客系统,包含完整的测试覆盖。

实验二:重构遗留系统为整洁架构

目标:选择一个现有的简单项目,将其重构为整洁架构。

步骤:

分析现有代码结构

识别核心业务逻辑

设计新的分层结构

逐步重构:

- 提取实体

- 提取用例

- 定义接口

- 实现适配器

确保测试通过

对比重构前后的代码质量

预期产出:重构报告,包括前后对比、遇到的问题和解决方案。


七、本章小结

整洁架构是一个经过时间检验的软件架构框架,它通过同心圆模型将软件划分为四个层次:实体层、用例层、接口适配器层和框架驱动层。其核心规则是依赖必须从外层指向内层,确保核心业务逻辑的独立性。

本章详细介绍了整洁架构的起源、理念、层次结构和依赖规则。通过博客系统、待办事项应用和测试驱动开发三个示例,展示了如何在不同语言和场景中应用整洁架构。

整洁架构的价值在于它提供了一个通用的、技术无关的架构模板,使系统能够独立于框架、UI、数据库和外部机构。这使得系统更易于测试、维护和演化。

然而,整洁架构不是银弹。它需要根据项目规模调整粒度,避免过度工程化。关键在于理解其核心原则(依赖方向、边界隔离),而不是机械地套用四层结构。

记住:架构的目的是支持业务,而不是展示技巧。整洁架构帮助我们构建能够随业务演化的系统,这才是它的真正价值。


八、习题

概念理解:解释整洁架构的四个层次及其职责,并举例说明每层应包含的内容。

依赖分析:给定一段代码,判断其是否违反整洁架构的依赖规则,并说明如何修复。

设计练习:为一个在线购物系统设计整洁架构,包括商品管理、购物车、订单处理等功能模块。

代码重构:将一个 MVC 项目重构为整洁架构,保持功能不变。

测试编写:为整洁架构的每一层编写相应的测试,展示不同层次的测试策略。

适配器实现:为同一个用例实现三种不同的仓储适配器(内存、文件、数据库)。

案例分析:研究一个知名开源项目,分析其是否符合整洁架构原则,并给出改进建议。

权衡讨论:讨论在什么情况下整洁架构可能不适合,以及有哪些替代方案。

工具实践:使用架构约束检查工具(如 ArchUnit)验证一个项目是否符合整洁架构规则。

前瞻思考:随着 Serverless 和边缘计算的发展,整洁架构的哪些原则仍然适用?哪些需要调整?写一篇短文阐述你的观点。

11/12整洁架构
0%