从四层到三层:一个实际重构案例
核心问题
Growth AIOS 的 Web.Server 模块原本采用四层架构(Controller → ApplicationService → Command → DomainService),为什么花了两个月重构为三层(Controller → DomainService)?
使用篇:重构前的四层架构
Web.Server 是 Growth AIOS 的 Web 服务端模块,负责提供 REST API。最初设计时沿用了标准的 .NET 四层架构:
原始四层架构:
Controller(接收请求、参数校验)
→ ApplicationService(编排业务逻辑)
→ Command(封装请求参数)
→ RequestConverter(请求参数转换)
→ DomainService(领域逻辑)
→ Repository(数据访问)每一层都有自己的一套对象:
| 层次 | 对象 | 职责 |
|---|---|---|
| Controller 层 | VO + VOConverter | 视图对象展示、类型转换 |
| Application 层 | ApplicationService + Command | 应用服务编排、命令封装 |
| Domain 层 | DomainService + Entity | 领域逻辑、实体定义 |
| Repository 层 | Repository + ORM 映射 | 数据访问 |
一个典型的请求链路涉及 7-9 个文件。
实践篇:重构的触发
在实际开发中,团队逐渐发现中间三层的边界越来越模糊:
问题一:ApplicationService 里面没有什么"应用逻辑"
大多数 ApplicationService 只是调用了 DomainService:
csharp
// 重构前的 ApplicationService——几乎只是转发
public class BotApplicationService
{
public async Task<BotVO> GetModelListAsync(BotGetModelListCommand cmd)
{
var request = _converter.ToRequest(cmd);
var result = await _domainService.GetModelListAsync(request);
return _converter.ToVO(result);
}
}这段代码没有"应用逻辑"——它只是在调用 DomainService 之前做参数转换,在返回结果之前做 VO 转换。
问题二:Command 对象和 Request 对象高度重复
Command 的作用是"封装请求参数",但它的字段和 Controller 接收的 Request 几乎一模一样:
csharp
// Controller 接收的 Request
public class BotGetModelListRequest {
public string BotId { get; set; }
public string ModelType { get; set; }
}
// Application 层的 Command——几乎一样的字段
public class BotGetModelListCommand {
public string BotId { get; set; }
public string ModelType { get; set; }
}问题三:RequestConverter 没有真正的转换逻辑
大多数 RequestConverter 只是在做字段级别的类型映射:
csharp
// RequestConverter——没有真正的逻辑
public class BotRequestConverter
{
public BotGetModelListCommand ToCommand(BotGetModelListRequest request)
{
return new BotGetModelListCommand
{
BotId = request.BotId,
ModelType = request.ModelType
};
}
}问题四:改一个字段要同步修改多处
一个字段的改动涉及:
- Request(Controller 层)
- Command(Application 层)
- Entity(Domain 层)
- VOConverter、RequestConverter 中的映射逻辑
4 层 × 至少 2 处 = 8 处修改。
实践篇:重构方案
核心思路
斩断中间层,让 Controller 直接调用 DomainService。
重构前(四层):
Controller → RequestConverter → ApplicationService → Command → DomainService
重构后(三层):
Controller → VOConverter → DomainService第一次重构:11 个模块 55 个端点
提交 a3c5f2f0,涉及 103 个文件:
删除:
- ApplicationService:9 个
- Command:43 个
- RequestConverter:11 个
- 旧 VOConverter:部分
新增/修改:
- Controller:直接调用 DomainService
- VOConverter:移到统一位置
代码变化:-5054 行 / +1538 行 = 净减 3516 行第二次重构:Plugin + Workflow 模块
提交 1e18a49b,涉及 57 个文件:
删除:
- ApplicationService:2 个
- RequestConverter:2 个
- Command:43 个
- 旧 VOConverter:2 个
代码变化:-4614 行 / +1002 行 = 净减 3612 行两次重构合计
- 删除文件:49 个(ApplicationService 11 + Command 86 + RequestConverter 13 + 旧 VOConverter 4)
- 净减代码:7128 行
- 受影响模块:13 个
- 受影响端点:约 80 个

实践篇:重构前后的对比
AI 理解成本对比
| 维度 | 重构前(四层) | 重构后(三层) |
|---|---|---|
| 一个请求的文件数 | 7-9 个 | 3-4 个 |
| 需要理解的中间对象 | VO + Command + Entity | VO + Entity |
| 需要跟踪的转换逻辑 | VO⇄Request⇄Command⇄Entity | VO⇄Entity |
| AI 理解一个接口 | 需跨 7 个文件拼接 | 看 Controller + DomainService 即可 |
| 改一个字段涉及文件数 | 4-6 个 | 2-3 个 |
代码量对比
重构前:每个模块约 500-800 行(Controller + ApplicationService + Command + RequestConverter + VOConverter + DomainService)
重构后:每个模块约 200-400 行(Controller + VOConverter + DomainService)
核心逻辑占比:从 20-30% 提升到 40-50%知识篇:重构时机判断
并不是所有系统都需要从四层砍到三层。判断标准是:
如果一层只做"转发"而不做"转换",那它大概率是多余的。
- ApplicationService 如果只负责调用 DomainService,没有真正的"编排逻辑"→ 可去掉
- Command 如果和 Request 字段几乎一样 → 可合并
- RequestConverter 如果只做字段映射,没有业务逻辑 → 可去掉
什么情况下应该保留中间层:
- 跨多个 DomainService 的事务协调——ApplicationService 有真正的编排职责
- Command 有复杂的参数构造逻辑——不仅仅是字段映射
- 领域模型的聚合根需要保护——通过 ApplicationService 控制访问入口
这不是说三层优于四层。而是说:每一层的价值都应该被严格审视。
本章小结
| 阶段 | 文件数 | 代码行 | 核心逻辑占比 |
|---|---|---|---|
| 重构前 | 7-9 | ~1000 | 20-30% |
| 重构后 | 3-4 | ~400 | 40-50% |
| 变化 | -50% | -60% | +100% |
Web.Server 的重构证明了一件事:很多中间层是在"为未来做准备",而不是在解决当下的问题。 AI 时代的架构原则应该是"当前需要什么就做什么",而不是"未来可能需要什么就先做"。因为 AI 的编码效率足够高,未来真的需要时再添加也不迟。
关联:下一章从后端架构转向前端——为什么 React 是 AI 时代的前端王者。