拒绝“面条代码”!中小团队后端分层架构的3次重构实战

说实话,你有没有经历过这样的场景:

凌晨两点,线上报了个紧急Bug,你打开那个传说中的 OrderService.java,看着里面洋洋洒洒 5000多行 的代码,甚至一个 createOrder 方法就占了800行。你一边骂骂咧咧一边小心翼翼地改了一个 if 判断,结果第二天上线,购物车功能崩了。

这事儿我真干过。

三年前,我接手了一个电商中台项目,当时的架构名义上是标准的 MVC,实际上是“一锅乱炖”。Controller 里写数据库查询,Service 里拼凑 HTML,DAO 层竟然还有业务逻辑。

当时项目经理问我:“为什么加个‘修改收货地址’的功能要三天?”我只能苦笑。

代码分层不是为了炫技,而是为了在需求变更时,别让整个系统像多米诺骨牌一样倒下。

在这三年的“填坑”之路上,我们团队经历了三次典型的架构重构,我也总结了一套适合中小团队落地的分层逻辑。今天就把这些血泪经验拆解开来,希望能帮你少加几天班。

第一次重构:把 Controller 当成“点菜员”,而不是“大厨”

刚开始最典型的问题是 Controller 层太肥

当时我们的 OrderController 是这么写的:接收 HTTP 请求,解析 JSON,校验参数(比如金额是否小于0),去数据库查库存,算优惠,最后落库,甚至还顺手发了个 Kafka 消息。

这带来的直接后果是:复用性几乎为零

后来我们要开发一个小程序端,发现下单逻辑完全一样,但因为之前的逻辑都写在 Web 端的 Controller 里,且深度绑定了 HttpServletRequest,根本没法复用。结果就是:复制粘贴,搞了两份一模一样的代码。

落地方法:严格剥离业务逻辑

我们定了一条死规矩:Controller 只做三件事——接收参数、调用 Service、返回结果。 它就像餐厅的点菜员,只负责把菜单递给后厨(Service),绝对不能自己下厨炒菜。

为了强制执行,我在 Code Review 时重点抓两点:

  1. Controller 中不允许出现任何 SQL 操作或复杂的 if-else 业务判断。
  2. 入参必须使用 DTO(数据传输对象),禁止把数据库实体(Entity/PO)直接作为接口入参。

思考题:你现在的项目里,有没有在 Controller 层直接操作数据库的代码?

重构后,原本 800 行的 Controller 方法变成了这样:

// 重构后的 Controller
@PostMapping("/create")
public Result<String> createOrder(@RequestBody OrderCreateDTO request) {
    // 1. 参数基础校验交给了 JSR-303 注解
    // 2. 核心逻辑下沉
    String orderId = orderService.createOrder(request);
    // 3. 统一返回格式
    return Result.success(orderId);
}

这波操作下来,我们的代码复用率提升了至少 40%,再接新渠道时,Controller 只是个薄薄的壳。

第二次重构:引入 Manager 层,解决“剪不断理还乱”的循环依赖

配图

Controller 瘦身成功了,压力全到了 Service 层。很快我们就撞上了第二个墙:Service 之间的循环依赖

场景是这样的:OrderService 创建订单时需要查用户信息,于是注入了 UserService;而 UserService 在注销用户时,需要检查是否有未完成订单,于是又注入了 OrderService

Spring 启动时直接报错,报 BeanCurrentlyInCreationException。为了图省事,当时有个兄弟直接用了 @Lazy 注解延迟加载。这虽然让项目跑起来了,但代码逻辑变得极其混乱,服务之间耦合得像一团乱麻,改一个类,三个类都要动。

落地方法:引入 Manager 通用业务层

我们在 Service 和 DAO 之间,硬切出了一个 Manager 层(也可以叫通用业务处理层)。

这一层的定位非常明确:

  • 对第三方平台的封装(比如调用支付宝、微信发消息);
  • 对 DAO 的原子封装(比如查用户并判空);
  • 服务下沉:将原本相互依赖的逻辑下沉到 Manager,打破 Service 层的闭环。

比如上面的例子,我们把“检查是否有未完成订单”这个动作下沉到了 OrderManagerUserService 只调用 OrderManager,不再直接依赖 OrderService

这次调整后,Service 层变得清爽多了,它更像是一个业务编排者,负责把各个 Manager 提供的积木搭成房子。

第三次重构:防腐层(ACL),别让外部变化搞崩你的系统

这是我踩过最痛的一个坑。

去年双十一前夕,我们的短信服务商突然升级了 SDK,接口参数变了。本来这只是个小改动,结果我一搜代码,发现全工程有 20多个地方 直接引用了那个服务商的 SDK 类。

配图

那天下午,我们三个开发改了整整4个小时,因为很多业务逻辑里直接把 SDK 的对象透传到了最核心的 Service 层。

落地方法:接口隔离与防腐层

这次教训让我明白:永远不要信任外部系统

我们立刻实施了“防腐层”策略。简单来说,就是定义属于自己的接口,不管外部怎么变,我们内部只认自己的接口。

比如发短信,我们定义了一个 SmsService 接口:

public interface SmsService {
    void send(String phone, String content);
}

具体的实现类 AliyunSmsImpl 去引用阿里云的 SDK。如果明天换成腾讯云,我只需要重写一个实现类,业务层代码一行都不用动。

这层“防腐层”就像一道防火墙,把外部的不确定性挡在了核心业务之外。后来我们换过一次支付渠道,只用半天就完成了切换和测试。

总结与行动

经过这三次折腾,我们小团队现在的架构基本稳定在: Controller (路由与转换) -> Service (业务编排) -> Manager (通用能力/三方封装) -> DAO (数据读写)

这套架构可能不是最高大上的,但对于 5-20 人的中小团队来说,它是性价比最高的:既解决了混乱,又没有过度设计。

如果你想动手改善现有的代码,我建议从这 3个具体步骤 开始:

  1. 盘点 Controller:本周抽出一小时,随机打开3个 Controller,看里面有没有写 SQL 或者复杂的业务判断?如果有,这就是你的第一个重构点。
  2. 消灭 Entity 透传:检查一下,你的前端接口是不是直接返回了数据库实体对象?尝试引入 DTO,把数据结构的所有权拿回自己手里。
  3. 封装一个第三方调用:找一个项目里用得最多的外部接口(比如对象存储或支付),给它套上一层自己的 Interface。

架构不是画在 PPT 上的图,而是每一行代码里的克制与权衡。

我想问问大家,在你们现在的项目里,遇到过最让你头疼的“面条代码”是在哪一层?欢迎在评论区聊聊,咱们一起避坑。