Skip to content

从四层到三层:一个实际重构案例

核心问题

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 + EntityVO + Entity
需要跟踪的转换逻辑VO⇄Request⇄Command⇄EntityVO⇄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~100020-30%
重构后3-4~40040-50%
变化-50%-60%+100%

Web.Server 的重构证明了一件事:很多中间层是在"为未来做准备",而不是在解决当下的问题。 AI 时代的架构原则应该是"当前需要什么就做什么",而不是"未来可能需要什么就先做"。因为 AI 的编码效率足够高,未来真的需要时再添加也不迟。


关联:下一章从后端架构转向前端——为什么 React 是 AI 时代的前端王者。