三层各自解决什么问题
Controller / Business / DAO 的分层不是为了显得架构完整,而是为了让代码有稳定的承载位置。Controller 面向 HTTP 或 RPC 协议,负责参数、鉴权、响应格式和错误映射。Business 面向业务动作,负责流程、规则、事务和领域判断。DAO 面向数据,负责查询、写入、分页、锁和 SQL 安全。
当需求变复杂时,混写代码会迅速失控。比如客户分配既要查客户状态,又要查顾问排班,还要写分配记录和发送通知。如果全放 Controller,测试和复用都会困难。
- Controller:薄,稳定,少业务判断。
- Business:表达一个可命名的业务动作。
- DAO:围绕数据对象封装读写。
- DTO / Request:承接输入结构和校验结果。
事务边界放在哪里
事务应该靠近业务流程,而不是散落在 DAO 里。DAO 只知道自己在写某张表,但不知道这次业务动作需要几张表一起成功。Business 层能看到完整流程,因此更适合打开事务、捕获异常、决定回滚和补偿。
如果某些 DAO 方法内部自行开启事务,会让上层组合时变得不可预测。小项目早期可以宽松,但一旦出现跨表、跨服务、跨事件的动作,就需要把事务边界上移。
- 单表简单写入可以封装在 DAO。
- 跨表一致性由 Business 管理事务。
- 外部通知不要放进数据库事务内部长期阻塞。
- 失败后要有明确的重试、补偿或人工处理入口。
安全和可维护性底线
后端分层还承载安全底线。原生 SQL 必须参数绑定,不能拼接用户输入;配置读取走 config,不在业务代码随手 env;表结构变化走 Migration,不能手动在线上改表后忘记同步。
这些要求看起来基础,但它们决定了系统能否多人维护。规范不是限制开发速度,而是减少后续排障成本。
- SQL 参数绑定,避免注入风险。
- 敏感配置不进入仓库。
- Migration 要有 down 或明确不可逆说明。
- 日志记录业务关键 ID,不记录敏感明文。
一个判断标准:代码在回答谁的问题
判断一段代码应该放在哪一层,可以问它在回答谁的问题。Controller 回答“HTTP 请求如何进入系统、如何返回”;Business 回答“这件业务动作如何完成”;DAO 回答“数据如何被查询和写入”。
如果 Controller 里出现大量 if、事务、循环写库、跨表查询,它已经越界。如果 DAO 里开始判断客户是否能分配、订单是否能审核,它也越界。
- 协议问题放 Controller。
- 业务流程放 Business。
- 数据读写放 DAO。
- 跨层复用能力沉淀到基类、工具或领域服务。
用业务动作命名 Business
Business 不应该只叫 CommonService 或 DataService。它最好以业务动作命名,例如 AssignCustomerBusiness、ApproveOrderBusiness、TransferCustomerBusiness。这样代码结构会自然贴近业务语言。
命名清楚之后,评审也更容易判断职责是否膨胀:一个“客户分配”业务可以包含规则校验、候选过滤、事务写入和事件记录,但不应该顺手处理订单审核。
- 一个 Business 方法最好表达一个完整业务动作。
- 复杂动作内部可以拆私有步骤,但外部接口保持语义清楚。
- 跨多个业务动作的共性逻辑再抽公共服务。
- 不要为了复用把所有流程塞进一个万能 Service。
DAO 的边界:不是贫血,也不是万能
DAO 负责数据域,不代表它只能写一行 ORM。它可以封装复杂查询、分页、条件组合、锁和批量写入。但 DAO 不应该知道完整业务流程,也不应该决定是否发送通知、是否审核通过、是否触发回捞。
数据访问层稳定之后,后续换缓存、加索引、优化 SQL,都不会影响上层业务表达。
- DAO 方法返回结构要稳定,避免把 ORM 细节泄漏给上层。
- 原生 SQL 必须参数绑定。
- 复杂查询要说明索引假设。
- 写方法要明确是否参与外部事务。
测试策略也跟分层有关
分层清楚后,测试也更容易设计。Controller 测接口契约和权限;Business 测业务规则、事务和异常路径;DAO 测查询条件和数据写入。没有分层时,测试只能从接口一路打到底,速度慢且定位困难。
对于高风险业务,建议给 Business 层建立场景测试:成功、重复提交、权限不足、数据状态不合法、并发冲突、外部依赖失败。
- Controller:请求参数、响应格式、鉴权。
- Business:规则分支、事务回滚、幂等。
- DAO:查询条件、分页、锁、索引命中。
- 端到端测试只覆盖关键链路,不替代分层测试。