三层架构落地实践:表现层、业务逻辑层与数据访问层边界治理

发布时间:2026/10/30 9:10:48
三层架构落地实践:表现层、业务逻辑层与数据访问层边界治理 三层结构这四个字几乎是每个做后端的人入行第一周就会听到的词但真正能把表现层、业务逻辑层、数据访问层这三层的边界说清楚、并且在项目里长期守住的人并不多。我见过太多项目一开始规规矩矩分了三个包半年之后 Controller 里塞满了 SQL 拼接Service 变成了一个两千行的巨型类Dao 层只剩下一个通用 Mapper 在裸奔。三层结构本身不难难的是它在真实业务压力下会持续被侵蚀而大多数人没有意识到侵蚀是从哪一天开始的。这篇东西想聊的就是三层结构到底在解决什么问题每一层该守什么、不该碰什么落地的时候有哪些参数和细节需要算清楚以及我自己踩过的那些坑。不管你是刚接触分层设计的新人还是已经带过几个项目、想重新梳理一遍架构思路的老手应该都能从里面找到点能直接抄走的东西。1. 三层结构到底在解决什么问题1.1 从一个越写越乱的接口说起我印象最深的一次是接手某后台系统的订单查询接口。打开文件一个方法从头拉到尾要滚五六屏先解析请求参数顺手做了十几个 if 判空然后直接拼了一段带 like 的 SQL再调两次远程接口补充用户信息和商品信息中间还穿插了三处写缓存的逻辑最后把结果拼成一个 Map 返回。这个方法当时的行数是八百多行改一个字段的展示格式我得先在这八百行里定位到底哪一段在处理这个字段。这种代码的问题不在于它跑不起来它跑得很好线上也没什么大故障。问题在于它把三种完全不同频率的变化绑死在了一起。页面要改字段展示是每天都会发生的事业务规则要调整比如满减门槛变了是一个月几次的事数据库要加索引或者换存储是一年一两次的事。这三件事的变化频率差了整整两个数量级却被塞在同一个方法里结果就是最频繁的那种变化被迫承担了最高昂的修改成本和最高的回归风险。三层结构要解决的核心问题就是这个把变化频率不同的东西分开放让改动的影响范围收敛在一个可控的盒子里。表现层面对的是界面和协议的变化业务逻辑层面对的是规则的变化数据访问层面对的是存储的变化。这个划分不是教科书为了考试方便硬分出来的它是从大量项目纠错过程中沉淀下来的经验切面。1.2 分层的本质把变化关进各自的笼子我习惯用餐厅来类比。前厅负责迎客、点单、上菜、结账客人看到的全是前厅后厨负责把食材做成菜前厅的人不需要知道灶台几口、火候怎么控仓库负责备货和保鲜后厨也不需要关心供应商从哪儿进的货。三层结构就是这么一个分工表现层是前厅业务逻辑层是后厨数据访问层是仓库。这个类比里最关键的一点是前厅的人不能直接进仓库自己拿菜。一旦前厅可以绕过厨房去仓库翻东西仓库的摆放规则就会被各种人按自己的习惯改乱最后谁都找不到东西。对应到代码里就是表现层绝对不能直接持有数据库连接、不能直接写 SQL、不能直接操作持久化对象。这条线一旦破了分层就只剩下目录结构上的意义。反过来仓库也不该关心这道菜是配米饭还是配面条。数据访问层只负责“把东西拿出来”和“把东西放回去”它不应该出现“如果用户是 VIP 就打八折”这种判断。我审过一些代码Dao 层的 SQL 里带着case when user_level 3 then price * 0.8写的人觉得很省事一次查询搞定但这种写法把业务规则焊死在了 SQL 里规则一变就要改 SQL、改索引、走一次数据库变更流程成本高得离谱。还有一层容易忽略的东西层与层之间的数据形态也应该不一样。表现层拿到的应该是为界面量身定制的结构业务层处理的是领域概念数据访问层面对的是表记录。这三者的形态如果完全一致说明中间少了一层转换短期看着省事长期就是耦合。后面第 2.3 节会专门讲这个转换到底值不值。1.3 三层结构的边界划分与常见误读关于三层结构我见过最多的三个误读值得单独拎出来说清楚。第一个误读是把逻辑分层和物理部署混为一谈。三层结构说的是代码组织上的逻辑分层它跟你部署几台机器没有任何必然关系。你可以把三层全部打在一个进程里跑也可以把表现层单独拆出去做成网关。老早以前的三层架构图习惯画三台服务器导致很多人以为不分三台机器就不叫三层这个理解是错的。逻辑分层解决的是代码可维护性物理拆分解决的是伸缩性和故障隔离这是两个正交的问题。第二个误读是把分层等同于建三个包。很多项目里controller、service、dao三个包建得很齐整但 controller 里注入了 daoservice 里返回的是数据库实体对象这种情况下包名只是装饰。判断分层有没有真落地看的是依赖方向表现层依赖业务层业务层依赖数据访问层反过来绝对不行。数据访问层如果 import 了表现层的类那就是依赖倒挂说明某一层在偷偷往外伸手。第三个误读是层数越多越好。有些项目在业务逻辑层里再拆出“服务层”“领域层”“门面层”一层套一层一个查询请求要穿过六个类才到数据库。这种过度设计在小项目里是纯负担改一个字段要动六个文件调试的时候断点打得手酸。我的观点很直接三层是及格线不是天花板但加层之前必须有明确的理由比如确实存在多个表现层共享同一套业务逻辑或者业务规则复杂到需要独立的领域模型。没有理由的加层本质上是在用结构复杂度掩盖设计懒惰。2. 表现层别把业务逻辑藏在接口入口2.1 表现层真正该干的四件事表现层的位置很特殊它是所有请求的入口也是所有返回的出口。正因为它在入口写代码的人很容易顺手在这里做任何事——反正参数都在这儿顺手判个重、顺手算个价、顺手写个日志。我总结了表现层真正该负责的四件事超出这个范围的动作都值得警惕。第一是协议适配。HTTP 请求进来要转成内部调用内部的异常要转成合适的响应码和错误结构。这活儿只有表现层能干因为它最清楚对面是网页、是小程序还是别的系统在调。第二是参数校验的“形状检查”——必填项有没有传、类型对不对、长度超没超。注意这里说的是形状不是业务规则两者的界线在第 2.2 节展开。第三是身份与权限的粗粒度拦截比如这个接口需不需要登录、当前调用方有没有这个接口的访问权限。第四是响应组装把业务层返回的结果包装成前端约定的结构。这四件事有个共同点它们都跟“外部世界怎么跟我说话”有关跟“业务规则是什么”无关。只要你发现自己在表现层里写了“如果订单金额大于 500 就免运费”这类判断就该把它往下挪了。注意表现层方法里出现数据库访问、事务注解、或者超过三个以上业务分支判断基本都是分层要失守的信号。2.2 参数校验放在哪一层一个争论多年的问题参数校验放哪儿这个问题我跟不少人争过。常见的做法有两种全放表现层或者表现层做基础校验、业务层做规则校验。我倾向后者理由是把校验分成两类看。一类是“格式校验”字段非空、字符串长度、数字范围、时间格式。这类校验跟业务规则无关纯粹是防御脏数据放在表现层最合适因为它离入口最近能在最便宜的位置把无效请求挡掉省掉后面一整条调用链的开销。这类校验用框架自带的注解就能搞定代码量极小。另一类是“业务校验”账户余额够不够、这个订单当前状态能不能取消、同一用户今天是不是已经领过券。这类校验必须放在业务逻辑层因为它依赖领域状态而且很可能需要查询数据库。如果你把它放在表现层就会出现两个后果一是别的地方调用同一个业务能力时这段校验被绕过了二是校验逻辑和业务规则分居两地改规则的时候容易漏改一处。举个具体的例子。某项目里有个“发起退款”的接口表现层写了“订单状态必须是已支付”的校验。后来业务上增加了一个批量退款的入口走的是另一条路径直接调业务层结果这批退款把未支付的订单也退了一遍。根因就是把业务校验放在了表现层。修的办法很简单把状态判断挪进业务层的方法里让所有入口共享同一份规则。顺带说一句校验的返回格式。表现层做格式校验的时候失败返回应该是结构化的字段级错误而不是一句笼统的“参数错误”。前端要靠这个结构定位到具体哪个输入框运维要靠它在日志里快速判断是调用方的问题还是服务端的问题。这个细节很小但能省掉大量扯皮。2.3 DTO 与 VO 的转换成本与收益实体对象能不能直接返给前端技术上完全可以很多项目就是这么干的。但我建议不要理由不是洁癖是三个很现实的问题。第一个问题是字段泄露。数据库实体往往包含一些不该暴露的字段比如内部状态码、逻辑删除标记、创建人 ID、某些敏感的业务字段。直接返回实体等于把整张表的形状暴露给了外部一旦有人扫接口这些字段全都能被看到。你可能会说可以在序列化的时候加忽略注解但注解漏加的代价是安全事故而多写一个转换类的代价只是几十行代码。第二个问题是耦合。实体跟着表结构走前端跟着接口走两者节奏完全不同。表加了一个字段如果直接返回实体前端会莫名其妙收到一个新字段某些严格的客户端解析逻辑可能直接报错。中间隔一层 VO表结构怎么变都不会影响已经发布的接口契约。第三个问题是聚合。真实页面需要的数据往往不是单表能拼出来的比如商品详情页要商品基础信息 库存 三个推荐位 用户是否收藏。这种结构必然要一个专门的组装对象实体根本装不下。代价当然也有多写一个类多一次字段赋值多一处可能漏改的地方。我的经验是把转换逻辑集中在一个转换器类里用方法引用的方式批量赋值别在每个方法里手写十几行 set。字段多了之后手写赋值一定会漏而漏赋值引发的问题往往很隐蔽——某个字段莫名返回 null排查半天才发现是转换时忘了拷。方案开发速度字段安全接口稳定性适用场景实体直接返回快差差内部临时接口、原型验证手写 VO 转换中好好中小项目主流做法映射工具自动转换快中中字段多、结构稳定的项目查询层直接投影快好好读多写少的查询场景最后一行值得展开说一句。对于纯查询场景其实没必要先查出实体再转 VO可以让数据访问层直接把结果投影成目标结构少一次对象创建和字段拷贝。这种写法在报表类接口上收益很明显尤其是返回列表的时候能省掉大量中间对象。3. 业务逻辑层三层结构里最容易腐化的一层3.1 事务边界应该画在哪里事务边界画在哪儿这是分层设计里最有技术含量的一个决定。原则其实一句话事务边界应该等于一个完整的业务用例边界既不能太细也不能太粗。画太细的典型表现是在数据访问层的每个方法上加事务。这样一次业务操作里的多次写库各自独立提交中间任何一步失败前面的写入已经落库数据就不一致了。比如下单要写订单、扣库存、写流水三张表如果三个操作各自一个事务扣库存失败的时候订单已经存在了就出现了有订单没扣库存的脏数据后面得靠人工对账去捞。画太粗的典型表现是在表现层的方法上加事务把参数解析、远程调用、结果组装统统包进去。这样事务的持有时长会被远程调用的网络耗时撑大数据库连接被长时间占用并发一上来连接池瞬间打满。我见过一个接口因为这个写法平均响应时间从 80 毫秒涨到 1.2 秒原因就是事务里夹了一次第三方接口调用对方偶尔抖动两秒连接就一直挂着不释放。比较稳妥的做法是把事务注解加在业务层的用例方法上方法内部只做本地数据库操作任何远程调用、消息发送、文件读写都挪到事务提交之后。这类“提交后再做”的动作要单独处理因为事务提交本身可能失败得考虑补偿。还有一个参数经常被忽略事务超时时间。默认值往往很长某些框架默认是 30 秒甚至不限。我的建议是显式设置一般按这个用例的 P99 耗时乘以 2 给个上限比如正常 200 毫秒的接口设 3 秒足够。超时时间设短一点的好处是出现慢 SQL 或者锁等待的时候事务能快速回滚释放资源而不是拖垮整个连接池。3.2 应用服务与领域服务的分工项目稍微复杂一点业务层内部还会再分。我习惯分成应用服务和领域服务两类这个划分不是为了好看是为了解决“业务逻辑复用”和“业务流程编排”这两个不同诉求的冲突。应用服务面向用例它知道一次请求完整的流程先校验、再查数据、调领域服务算结果、然后落库、最后发事件。它不包含具体的计算规则只负责编排顺序和事务控制。领域服务面向规则它提供的是可复用的业务能力比如“计算这个订单的应付金额”“判断这个用户能不能参与这个活动”它不关心调用方是谁也不该自己开事务。这么分的好处很直接。当第二个入口需要复用同一套规则时它调领域服务就行不用把应用服务的流程抄一遍。而流程本身往往每个入口都不一样抄流程的成本远低于抄规则——规则错了是业务事故流程错了只是体验问题。反过来说如果项目就是十几个接口的简单后台硬拆应用服务和领域服务就是浪费。判断标准是同一段业务规则有没有可能被两个以上的入口用到。有就抽领域服务没有先放在应用服务里等第二次用到再抽。过早抽象和永不抽象一样有害前者增加理解成本后者增加修改成本。3.3 贫血模型与充血模型的取舍这个话题争议了很多年我的看法比较务实不要站队按场景选。贫血模型指的是实体只有字段和读写方法所有业务规则都写在服务层。它的优点是直观新人上手快规则集中在服务里容易查找配合数据访问层写起来很顺。缺点是实体退化成了数据袋子业务规则散落在多个服务方法里容易出现同一规则在三个地方各写一遍的情况。充血模型指的是把跟自身状态强相关的规则放到实体上比如订单实体自己提供“能否取消”的判断。它的优点是规则跟数据在一起天然防重复缺点是如果实体被序列化框架、ORM 框架深度使用往里加行为容易踩坑而且团队里如果有人习惯性地在实体里塞远程调用那就彻底乱套了。我自己的实践是一条中间路线跟单个实体自身状态强相关的判断放实体里跨多个实体的流程放服务层。比如“这个订单能不能取消”只依赖订单自己的状态字段放实体上合适“取消订单要退库存、退优惠券、写流水”涉及三个聚合放服务层编排。这条线划下来实体上有行为但不臃肿服务层管流程但不管细节。提示不管你选哪种模型都要在团队里统一。最怕的是一个项目里两种风格混用一半规则在实体里、一半在服务里找规则的时候两边都得翻。4. 数据访问层不只是增删改查4.1 仓储模式的落地方式与代价数据访问层的抽象方式主流有两种一种是直接暴露一个通用 Mapper业务层想查什么就组合条件另一种是仓储模式为每个聚合根定义一个接口接口里只暴露业务语义的方法比如“按用户查未支付订单”。通用 Mapper 的好处是省事一个接口继承下来基本的增删改查全有了写起来飞快。问题是业务层会直接面对查询条件对象等于把“怎么查”的知识泄露到了上层。我见过业务层的方法里拼了七八个查询条件其中三个是历史遗留的无效条件没人敢删因为不知道删了会影响什么。仓储模式的好处是查询语义被封装了业务层说“我要未支付订单”不需要知道底层是按状态字段过滤还是查了一张单独的待支付表。将来加索引、改分表、换存储只要接口不变业务层完全无感。代价是接口会越写越多每个查询场景都要加一个方法接口膨胀得很快。我的折中做法是写操作和核心查询走仓储接口报表类、列表类的灵活查询允许直接走查询层。理由是写操作和核心查询是业务的骨架必须稳定报表查询本来就多变硬封装反而累赘。这条线上我吃过一次亏早期追求纯粹把所有报表查询都封进了仓储接口结果三个月后接口里堆了四十多个方法一半只用过一次维护成本高得离谱。4.2 分页、批量、N1 这些细节怎么处理数据访问层的问题八成出在细节上而不是在抽象上。这几个细节值得单独说。分页。最常见的坑是深分页也就是offset很大时的查询。数据库要先扫过前面几十万行再丢掉越翻越慢。实测过一个列表接口第一页 30 毫秒翻到第 500 页要 4 秒多。解决办法有两种一是限制最大可翻页数产品上做个“最多查看前 100 页”的约束二是改成基于游标的分页用上一页最后一条的排序键做条件这种方式在无限滚动的场景里体验更好也不会有深分页问题。批量。循环里单条插入是最典型的性能杀手。一千条数据一条一条插每条一次网络往返累计起来能到几十秒。改成批量插入后往往是几十毫秒的事。批量大小的选择有个经验值单批 500 到 1000 条比较稳太大容易撞上数据库的包大小限制或者锁等待太小则批量优势不明显。另外要注意批量操作失败时的处理整批回滚还是逐条降级这个要按业务容忍度决定。N1 查询。这个坑的隐蔽性最高因为单看代码完全没问题。列表查了 20 条订单然后循环里对每条订单查一次用户信息一共执行 21 次查询。数据量小的时候完全看不出来等列表变成 200 条接口就慢了十倍。识别方式很简单看日志里的 SQL 条数如果列表接口的 SQL 条数跟返回条数成正比基本就是 N1。修法是改成一次性批量查回来在内存里做映射。4.3 数据访问层的异常转换与降级数据访问层抛出来的异常通常带着大量底层细节表名、字段名、SQL 片段、驱动版本。这些信息如果直接冒到表现层返回给调用方一是泄露内部结构二是对调用方毫无意义——他看到“Duplicate entry for key idx_order_no”根本不知道该怎么办。我的做法是在数据访问层做一次异常转换把底层异常归成几类业务能理解的语义唯一键冲突、连接不可用、超时、其他未知。转换之后往上抛业务层根据语义决定怎么处理表现层再翻译成合适的响应。降级这块要单独提醒。数据访问层的降级一定要谨慎因为很多写操作是没法降级的。读操作可以考虑返回缓存或者兜底数据写操作一旦失败最稳妥的做法是把错误如实抛上去让业务层决定是重试还是落一条待处理记录。我见过一个项目在数据访问层给写操作加了“失败就静默吞掉”的降级逻辑结果线上出现了一批用户以为下单成功、实际没有订单的投诉排查了两天才定位到这段代码。5. 一次完整的落地实操从零搭一个三层骨架5.1 目录结构与依赖方向先定结构。我常用的一套目录是这样的按层分包层内再按业务模块分src/main/java/com/example/app ├── web 表现层 │ ├── controller 接口入口 │ ├── request 入参对象 │ ├── response 出参对象 │ └── advice 统一异常与响应包装 ├── biz 业务逻辑层 │ ├── service 应用服务面向用例 │ ├── domain 领域服务与实体行为 │ └── converter 对象转换 ├── dal 数据访问层 │ ├── repository 仓储接口实现 │ ├── mapper 查询映射 │ └── entity 持久化对象 └── common 公共能力工具、常量、异常定义关键是依赖方向必须单向。可以用一条很土但有效的检查手段在构建脚本里加一条规则禁止dal包下的任何类 importweb或biz包下的类违反就构建失败。这条规则刚加上的时候一定会暴露一堆历史问题但收拾完之后分层就真的是分层了而不只是文件夹名字。5.2 关键代码骨架从入口往下走一遍看每一层各自承担什么。先看表现层// 表现层只做协议适配、形状校验、权限拦截 RestController RequestMapping(/api/orders) public class OrderController { private final OrderAppService orderAppService; public OrderController(OrderAppService orderAppService) { this.orderAppService orderAppService; } PostMapping(/{orderNo}/cancel) public ApiResultCancelOrderResponse cancel(PathVariable String orderNo, RequestBody Valid CancelOrderRequest req) { CancelResult result orderAppService.cancel(orderNo, req.getReason(), req.getOperatorId()); return ApiResult.ok(CancelOrderResponse.from(result)); } }注意这个方法里没有任何 if 判断业务规则也没有事务注解全靠业务层。入参对象的校验注解管形状业务层的判断管规则。再看业务逻辑层// 业务逻辑层编排流程、控制事务、不碰具体存储细节 public class OrderAppService { private final OrderRepository orderRepository; private final StockDomainService stockDomainService; Transactional(timeout 3) public CancelResult cancel(String orderNo, String reason, Long operatorId) { Order order orderRepository.findByNo(orderNo) .orElseThrow(() - new BizException(订单不存在)); // 规则判断交给实体自己 order.assertCancellable(); order.markCancelled(reason, operatorId); orderRepository.save(order); // 库存回滚走领域服务事务提交后再执行外部动作 stockDomainService.releaseAfterCommit(order.getSkuId(), order.getQuantity()); return CancelResult.of(order); } }这段代码里有几个设计取舍值得说。事务超时显式设成 3 秒因为这个用例的正常耗时在 100 毫秒级别留几十倍余量足够又能防止慢 SQL 长期占用连接。库存回滚的方法名带AfterCommit是在提醒后来者这里不能同步执行必须等主事务提交。实体上的assertCancellable把状态判断收进了实体避免多个入口各写一遍。最后是数据访问层// 数据访问层只负责持久化不参与业务判断 public class OrderRepositoryImpl implements OrderRepository { private final OrderMapper orderMapper; Override public OptionalOrder findByNo(String orderNo) { OrderEntity entity orderMapper.selectByOrderNo(orderNo); return Optional.ofNullable(entity).map(OrderConverter::toDomain); } Override public void save(Order order) { OrderEntity entity OrderConverter.toEntity(order); int affected orderMapper.updateById(entity); if (affected 0) { throw new ConcurrentModifyException(订单已被修改请重试); } } }updateById的返回值判断是个容易被忽略的细节。返回 0 说明这条记录在读取之后被别人改了比如乐观锁版本号不匹配这种情况必须显式抛出来不能当成成功。我见过不少项目直接忽略返回值结果并发场景下出现了库存扣减丢失的问题。5.3 参数计算与配置选择这套骨架能不能稳很大程度上取决于几个参数选得对不对。我按实际算过的几个说一下过程。连接池大小。常见的经验公式是连接数 CPU核数 × 2 磁盘数。假设服务部署在 8 核机器上磁盘按 1 算得到 17取整到 20 左右。这个公式背后是一百多个并发压测得出的结论思路是连接太多反而增加上下文切换成本性能不升反降。但实际还要叠加一个约束单个服务实例的连接数乘以实例数不能超过数据库的最大连接数否则部署第三个实例的时候就会有一批连接被拒绝。假设数据库允许 500 个连接留 20% 余量给运维和批处理可用 400 个那么最多部署 20 个实例。这个账必须提前算。事务超时。我按 P99 耗时 × 20 取正常 150 毫秒的用例设 3 秒。角度是这样的超时时间太短会导致正常的网络抖动也能触发回滚太长则失去保护意义。乘 20 是个折中兼顾了偶发的 GC 停顿。批量大小。500 条一批。这个数的依据是一条记录大约 500 字节500 条就是 250 KB远低于常见数据库的包大小限制默认 4 MB 或者 16 MB同时又能把网络往返次数压到原来的五百分之一。如果单条记录特别大比如带长文本就按“单批总字节数不超过 1 MB”反算条数。分页大小上限。接口层强制限制单页最多 100 条。这个限制不是为了省带宽是为了防拖库也防前端写出“一页拿一万条然后在浏览器里自己过滤”这种代码。上限设多少要看单条数据的大小我一般按“单次响应体不超过 2 MB”反推。缓存过期时间。如果数据访问层加了本地缓存过期时间建议在 30 秒以内并且一定要有主动失效的通道。只靠过期时间失效的缓存在数据更新之后会有最长一个过期周期的不一致窗口这个窗口对某些业务是可以接受的比如商品分类对某些业务是灾难比如库存。6. 踩坑记录与问题速查6.1 典型故障排查表下面这些是我这些年真实遇到过、并且反复在新项目里看到的问题整理成速查表遇到类似现象可以先去这几条里对号入座。现象大概率原因排查方式处理办法接口偶发超时无明显规律事务里夹了远程调用或慢查询打点记录事务持有时长对比接口总耗时把远程调用移出事务超时后补偿列表页越翻越慢深分页 offset 过大看执行计划里的扫描行数限制最大页数或改游标分页列表接口随条数线性变慢N1 查询统计一次请求的 SQL 条数改批量查询后内存映射并发下数据被覆盖更新未校验影响行数检查 update 返回值是否被忽略加乐观锁版本号并判断返回值单元测试跑不通但线上正常业务层依赖了容器上下文检查测试是否启动了完整 Spring抽接口打桩别让业务层依赖框架细节改一个字段要动五六个文件分层腐化逻辑四处散落从入口往下追一遍调用链把规则收拢到业务层转换集中化事务不回滚异常被吞或抛了受检异常检查 catch 块和异常类型明确回滚条件别乱 catch表里最后一行值得多说两句。事务不回滚最常见的两个原因一个是业务代码里 try-catch 把异常吃掉了日志打了一条但没往外抛另一个是抛出的异常类型不在回滚规则里。这两种情况在测试环境往往发现不了因为不会触发异常路径等线上真出问题的时候才发现事务根本没起作用数据已经脏了。6.2 分层腐化的早期信号分层不是一次性设计完就一劳永逸的它会持续被侵蚀。我总结了几个早期信号出现任意两个就该做一次重构了。第一个信号是业务层的方法参数里开始出现请求对象。这说明表现层的对象在往上渗透业务层被迫知道了协议的形状。第二个信号是数据访问层的方法名开始带业务语义比如selectPaidOrdersWithCoupon这说明查询逻辑在往存储层沉淀将来规则一变就要改 SQL。第三个信号是同一个业务规则在两个以上的地方出现这个最危险因为迟早会出现改了一处漏了一处的情况。第四个信号是单元测试需要启动数据库才能跑。业务逻辑层的测试应该是纯内存的用打桩对象替换掉数据访问层。如果测试必须连库说明业务层和数据访问层粘住了。第五个信号是新增一个接口需要改动超过五个文件且改动内容高度重复。这些信号单独看都不致命但它们出现的频率能反映架构的健康度。我一般建议团队每个季度花半天时间做一次依赖关系扫描看看有没有跨层引用、有没有循环依赖成本很低收益很直接。6.3 我个人的几条经验最后分享几条纯个人体会都是文档里不会写、但实际工作中特别管用的。第一条分层的第一版一定要简单到有点丑。我见过太多项目在需求还没稳定的时候就设计了六层结构加一堆抽象接口结果三个月后需求一变这些抽象全成了负担。先按最朴素的三层写等到某个地方第二次出现重复的时候再抽这个节奏比一开始就追求完美结构靠谱得多。第二条转换代码要允许冗余。DTO、领域对象、持久化对象三套模型之间的转换看起来是重复劳动但这种重复是有价值的它让三层可以各自演化。我宁愿多写三百行转换代码也不愿意三套模型共用一个类然后因为一个字段的展示需求变化而引发连锁修改。第三条跨层调用的口子一旦开了就收不住。第一个绕过业务层直接调数据访问层的人往往是因为“就这一次赶时间”然后第二个人看到第一个人这么干也就跟着干了。这种事情靠自觉没用得靠构建期的规则自动拦截让违反分层的人在提交代码的时候就失败比事后 code review 说一百遍都有效。第四条别把分层当成目的。三层结构是为了让代码好改、好测、好交接如果某个场景下分层带来的复杂度超过了它解决的问题那就该简化。我做过一个只有七个接口的内部工具直接两层结构一年下来一次都没出过问题。结构服务于场景反过来就本末倒置了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询