Screaming Architecture: 让你的代码架构会说话
概述
在软件开发中,我们经常遇到这样的问题:当一个新团队成员查看项目代码时,需要花费很长时间才能理解这个系统是做什么的。这就像走进一座建筑,却无法一眼看出这是医院还是商场。Screaming Architecture 的核心理念就是解决这个问题 - 让系统的目的和核心业务在代码结构中清晰可见。
本文将探讨:
- [[Screaming architecture|Screaming Architecture]] 的核心概念
- 如何实现一个"会说话"的架构
- 常见的实现陷阱和解决方案
- 在不同规模项目中的应用策略
1. 什么是 Screaming Architecture
1.1 核心概念
Screaming Architecture 这个概念最早由 Robert C. Martin (Uncle Bob) 提出。他用建筑学做了一个绝妙的类比:当你看到一所建筑的蓝图时,应该能立即知道这是一所图书馆还是一所医院。同样,当查看一个软件项目的顶层目录结构时,应该能立即理解这个系统的用途。
1.2 传统架构 vs Screaming Architecture
csharp
// 传统的分层架构
src/
├── controllers/
├── services/
├── repositories/
└── models/
// Screaming Architecture
src/
├── catalog/ # 产品目录管理
├── ordering/ # 订单处理
├── inventory/ # 库存管理
└── shipping/ # 物流配送
1.3 核心优势
- 提高代码可读性和可理解性
- 加快新团队成员的上手速度
- 便于系统演进和重构
- 强化领域驱动设计的实践
实践启示
在实际项目中,完全按照业务功能组织代码可能会遇到一些挑战。比如跨功能的共享组件该如何放置,或者是否要为了保持清晰的业务结构而牺牲一些代码复用性。这需要在实践中找到平衡点。
2. 实现 Screaming Architecture
2.1 目录结构设计
让我们通过一个电子商务系统的例子来说明如何实现 Screaming Architecture:
typescript
// e-commerce/
src/
├── product/
│ ├── domain/
│ │ ├── Product.ts # 产品领域模型
│ │ └── ProductService.ts # 产品领域服务
│ ├── application/
│ │ ├── ProductController.ts # API 控制器
│ │ └── ProductDTO.ts # 数据传输对象
│ └── infrastructure/
│ └── ProductRepository.ts # 数据访问层
│
├── order/
│ ├── domain/
│ │ ├── Order.ts
│ │ └── OrderService.ts
│ ├── application/
│ │ └── OrderController.ts
│ └── infrastructure/
│ └── OrderRepository.ts
│
└── shared/ # 共享模块
├── types/
├── utils/
└── infrastructure/
2.2 代码实现示例
下面是一个具体的实现示例:
typescript
// src/product/domain/Product.ts
export class Product {
private id: string;
private name: string;
private price: number;
private inventory: number;
constructor(id: string, name: string, price: number, inventory: number) {
this.id = id;
this.name = name;
this.price = price;
this.inventory = inventory;
}
// 领域行为
public decreaseInventory(quantity: number): void {
if (quantity > this.inventory) {
throw new Error('Insufficient inventory');
}
this.inventory -= quantity;
}
}
// src/product/application/ProductController.ts
@Controller('/products')
export class ProductController {
constructor(private productService: ProductService) {}
@Get('/:id')
async getProduct(@Param('id') id: string): Promise<ProductDTO> {
const product = await this.productService.findById(id);
return ProductMapper.toDTO(product);
}
}
2.3 关键设计决策
- 按业务功能组织代码
- 在每个业务模块内部采用分层架构
- 共享代码放在 shared 目录
- 依赖注入实现模块间的解耦
设计考量
- 模块大小的把握:太大会导致代码难以维护,太小会增加不必要的复杂性
- 跨模块调用的处理:是否允许直接调用,或强制通过接口
- 共享代码的管理:什么代码应该放在 shared 目录
3. 架构演进
3.1 从传统架构迁移
分步骤迁移策略:
- 识别核心业务模块
- 创建新的目录结构
- 渐进式地移动代码
- 重构跨模块依赖
3.2 处理遗留系统
typescript
// 过渡期的混合架构
src/
├── legacy/ # 遗留代码
│ ├── controllers/
│ └── services/
│
├── product/ # 新架构
│ ├── domain/
│ └── application/
│
└── shared/ # 共享代码
实践启示
在实际项目中,很少有机会从零开始实现完美的架构。通常需要在保持系统稳定运行的同时,渐进式地改进架构。关键是要有清晰的目标架构和循序渐进的迁移计划。
总结
Screaming Architecture 不仅仅是一种代码组织方式,更是一种设计思维。它强调:
- 业务领域应该是代码组织的主要依据
- 系统的用途应该在代码结构中清晰可见
- 架构应该服务于业务,而不是技术框架
练习题
- 给定一个传统的分层架构项目,如何将其重构为 Screaming Architecture?
- 在一个电子商务系统中,订单处理和支付处理是否应该是独立的顶层模块?为什么?
- 如何处理跨模块的业务流程?比如下单流程涉及商品、库存、订单等多个模块。
- 在微服务架构中,如何应用 Screaming Architecture 的原则?
延伸阅读
- Clean Architecture by Robert C. Martin
- Domain-Driven Design by Eric Evans
- Building Evolutionary Architectures by Neal Ford
- [[Ford et al. - 2022 - Building evolutionary architectures automated software governance.pdf]]