第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)
示例:
// 银行系统的账户实体
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)
示例:
// 转账用例
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)
示例:
// 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
示例:
// 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 数据结构转换
// 外层 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 接口适配
// 内层定义的接口
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 事件解耦
// 内层发布事件
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 |
实体层测试示例:
@Test
public void testWithdrawInsufficientFunds() {
BankAccount account = new BankAccount("123", "John");
account.deposit(new BigDecimal("100"));
assertThrows(InsufficientFundsException.class, () -> {
account.withdraw(new BigDecimal("150"));
});
}用例层测试示例:
@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领域层 - 实体:
// 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; }
}// 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; }
}// 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; }
}领域层 - 仓储接口:
// 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);
}// 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);
}用例层:
// 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);
}
}// 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));
}
}// 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;
}
}适配器层 - 仓储实现:
// 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:
// 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);
}
}
}// 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("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """);
}
}适配器层 - Controller:
// 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;
}
}框架层:
// 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 实现的待办事项应用
# 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()# 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# 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)# 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]# 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 示例三:测试驱动开发中的整洁架构
// 测试:实体层
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 和边缘计算的发展,整洁架构的哪些原则仍然适用?哪些需要调整?写一篇短文阐述你的观点。