首页 / 资讯中心 / 文章详情

分层架构设计:从概念到实践,彻底理解依赖管理与层间边界

分层架构设计:从概念到实践,彻底理解依赖管理与层间边界 ★ FEATURED ARTICLE
分层架构这个词我在带新人的时候几乎每个月都要讲一遍。有意思的是很多人第一次听到“分层”两个字第一反应是“不就是把代码分几个文件夹吗”然后过两个月被自己写的循环依赖折磨得死去活来再回来找我聊才真正理解分层到底在解决什么问题。这篇内容我想把分层架构设计概念从头到尾拆一遍不讲教科书上那种干巴巴的定义而是讲清楚每一层为什么存在、边界怎么划、依赖方向为什么不能反、以及我在实际项目里踩过的那些坑。不管你是刚接触系统设计的新手还是已经写过几年代码但总觉得架构“差点意思”的开发者应该都能从里面找到能直接用的东西。1. 分层架构到底在解决什么问题1.1 从一次典型的“代码烂掉”过程说起我见过太多项目是这么烂掉的最开始只有一个业务类里面既有数据库查询又有业务计算还顺手把返回结果拼成了前端要的格式。第一周跑得很好第二周加了个新需求在原来的方法里加了个 if第三周要换数据库发现业务逻辑和 SQL 缠在一起动一处牵全身。这时候有人提议“我们分层吧”于是把代码拆成三个文件但拆完之后发现业务层里还是在直接 new 数据库连接控制层里还是在做业务判断——形式上分了层实质上还是坨在一起。这个过程的本质问题是变化的方向没有被隔离。数据库会换、界面会改、业务规则会调整这三类变化的频率和原因完全不同。分层架构的核心价值就是把这些不同原因引起的变化隔离开让改数据库的时候不用碰业务代码改界面的时候不用碰数据访问代码。用一句话概括分层不是为了好看是为了让每一类变化只影响一个地方。1.2 分层的本质是“依赖方向的管理”很多人把分层理解成“横向切块”其实更准确的理解是“纵向管理依赖”。经典的三层架构里表现层依赖业务层业务层依赖数据访问层依赖方向是单向向下的。这个单向性才是分层的灵魂。一旦出现数据访问层反过来调用业务层或者业务层直接依赖表现层的某个类分层就名存实亡了。我用一个生活化的类比来解释分层就像一家餐厅。前厅服务员表现层负责接待客人、记录点单后厨业务层负责按订单做菜仓库数据访问层负责提供食材。服务员不会自己跑进仓库拿食材后厨也不会跑到前厅去招呼客人。每一层只跟相邻的下一层打交道这就是依赖方向的约束。如果哪天服务员直接冲进仓库翻东西整个餐厅的秩序就乱了——对应到代码里就是耦合。1.3 分层带来的三个实际收益第一个收益是可替换性。数据访问层封装好之后从关系型数据库换到别的存储方案理论上只需要重写数据访问层的实现业务层代码一行不动。我做过一个项目早期用本地文件存储后来换成关系型数据库因为数据访问层接口定义得干净替换工作只花了两天。第二个收益是可测试性。业务层的逻辑不依赖真实的数据库和界面测试的时候可以用模拟实现替换掉数据访问层单元测试跑得飞快。这一点在实际项目里价值极大尤其是业务规则复杂、分支多的场景。第三个收益是团队协作的边界清晰。不同的人可以负责不同的层接口约定好之后并行开发互不干扰。新人上手也快因为每一层的职责是明确的不用一上来就理解整个系统。注意分层不是越多越好。我见过有人把一个简单的增删改查项目分成六层每层就几行转发代码维护成本反而更高。层的数量应该由系统的复杂度和变化点决定简单系统三层足够复杂系统再考虑更细的划分。2. 经典分层模型的拆解与选型考量2.1 三层架构最常用也最容易被误解三层架构指的是表现层、业务逻辑层、数据访问层。这个模型流传最广但也最容易被机械套用。我先把三层的职责说清楚层级核心职责不该做的事表现层接收输入、参数校验、组织输出格式写业务规则、直接访问数据库业务逻辑层业务规则计算、流程编排、事务边界拼装界面格式、写 SQL数据访问层数据持久化、查询封装、存储适配做业务判断、处理界面逻辑这张表看起来简单但实际项目里最常见的错误就是职责越界。比如参数校验很多人放在业务层理由是“业务层更安全”。我的做法是格式校验比如字段非空、长度限制放在表现层业务规则校验比如余额是否充足、状态是否允许流转放在业务层。这个划分的依据是——格式校验跟界面形态相关业务校验跟业务规则相关各自归各自管。2.2 四层架构领域层独立出来的意义当业务逻辑复杂到一定程度三层架构里的业务层会变得臃肿既有业务流程编排又有核心领域规则。这时候把领域逻辑单独抽出来成为领域层形成表现层、应用层、领域层、基础设施层的四层结构是更合理的选择。应用层负责流程编排和事务控制领域层负责核心业务规则基础设施层负责技术实现数据库、消息、缓存等。这个划分的关键在于领域层不依赖任何技术细节它只表达业务概念。这样领域逻辑可以独立测试、独立演进技术选型的变化不会污染业务核心。我个人的经验判断标准是如果业务层里超过一半的代码是在做流程调度而不是规则计算说明流程和规则混在一起了可以考虑拆分。但要注意四层架构对团队的设计能力要求更高如果团队对领域建模不熟悉强行拆反而会拆出一堆贫血对象得不偿失。2.3 分层与分模块的区别这里要澄清一个高频混淆点分层是纵向的分模块是横向的。分层按技术职责切分模块按业务能力切。一个电商系统纵向分表现层、业务层、数据层横向分订单模块、库存模块、支付模块。两者是正交的可以同时存在。实际项目里我通常的做法是先按业务模块划分大边界每个模块内部再分层。这样既保证了业务边界的清晰又保证了技术职责的隔离。如果只分层不分模块所有业务逻辑挤在一个业务层里照样会乱如果只分模块不分层每个模块内部技术细节和业务逻辑混在一起同样难维护。2.4 为什么依赖方向不能反转依赖方向必须单向向下这个规则背后的逻辑是越往下层越稳定、越通用越往上越易变、越具体。数据访问层封装的是存储细节相对稳定表现层跟着界面需求变变化频繁。如果让稳定的层依赖易变的层稳定的层就会被拖着一起变隔离的意义就没了。依赖倒置是这条规则的一个补充手段。业务层需要调用数据访问能力但不想直接依赖具体实现于是定义一个接口放在业务层数据访问层去实现这个接口。这样依赖方向在源码层面是“数据访问层依赖业务层的接口”但运行时调用方向还是业务层调数据访问层。这个技巧在需要替换实现或者做单元测试时非常有用本质上是把“依赖具体”变成了“依赖抽象”。3. 分层落地的核心实操要点3.1 层间接口怎么设计才算合格接口设计的第一个原则是面向业务语义不暴露技术细节。数据访问层的接口应该叫“按用户标识查询账户”而不是“执行 select 语句”。接口参数和返回值应该是业务对象而不是数据库行对象或者结果集。这样做的好处是上层完全感知不到底层用的是关系库还是别的存储。第二个原则是接口粒度适中。太细的接口会导致上层调用次数过多太粗的接口会导致下层实现臃肿。我的经验是一个接口方法对应一个完整的业务查询或操作意图而不是对应一张表的某个字段操作。比如“查询用户及其关联的订单列表”可以是一个接口方法但“查询用户名字”和“查询用户年龄”拆成两个方法就过细了。第三个原则是异常处理要统一。下层抛出的技术异常比如连接超时不应该原样透传到上层应该在层边界转换成业务语义的异常。这样表现层不需要知道底层用的是什么技术只需要处理业务层面的错误情况。3.2 数据传输对象的使用边界跨层传递数据时我强烈建议使用专门的数据传输对象而不是直接把数据库实体或者领域对象传来传去。原因有三第一数据库实体会随着表结构变化直接传上去会导致上层跟着改第二领域对象可能包含业务方法传到表现层容易被误用第三不同层需要的数据字段往往不同用一个对象硬传会造成字段冗余和安全隐患。实际做法是数据访问层返回数据库实体业务层把它转换成领域对象做计算需要往表现层传的时候再转换成传输对象。转换代码看起来是“重复劳动”但它是层与层之间的缓冲带隔离了各层数据模型的变化。我踩过的坑是早期图省事直接传实体后来加了个敏感字段结果不小心透到了接口返回里排查了半天。3.3 事务边界应该放在哪一层事务边界放在业务层这是基本原则。表现层不应该管事务因为它不知道一个业务操作涉及几次数据写入数据访问层也不应该管事务因为它只负责单次数据操作不知道业务上的原子性要求。具体实现上事务的开启和提交应该包裹在业务层的一个完整业务方法上。比如“转账”这个业务操作涉及扣款和入账两次数据写入事务边界就应该在业务层的转账方法上。这里要注意的是事务范围不要过大一个事务里不要包含远程调用或者耗时操作否则会长时间占用连接资源。3.4 分层目录结构的组织方式目录结构是分层落地的物理体现我推荐按层建顶层目录层内再按业务模块组织。比如src/ presentation/ order/ user/ application/ order/ user/ domain/ order/ user/ infrastructure/ persistence/ messaging/这种结构的好处是层的边界一目了然新人进来先看顶层目录就知道系统分了几层。层内按模块组织保证了同一业务的相关代码聚在一起。我不推荐按模块建顶层目录再在模块内分层因为那样层的全局约束不容易被看到容易出现某个模块偷偷跨层调用的情况。提示目录结构定下来之后最好用构建工具或者静态检查工具加一条规则禁止下层引用上层的包。靠人自觉是靠不住的工具约束才是长久之计。4. 分层实践中的常见问题与排查实录4.1 循环依赖是怎么产生的怎么破循环依赖是分层实践里最常见的问题典型表现是业务层引用了表现层的某个工具类同时表现层又引用业务层。产生的原因通常是“图方便”——某个格式化方法在表现层写好了业务层也想用就直接引用了。破解思路分三步第一步找出循环依赖的具体类用依赖分析工具扫一遍就能定位第二步判断这个被双方共用的东西到底属于哪一层如果它是纯技术工具比如日期格式化应该下沉到公共基础设施里第三步如果是业务概念被错误放置把它移到正确的层然后调整引用。预防手段比补救更重要。我在项目里会约定一条硬规则任何跨层引用必须经过接口不允许直接引用实现类。这条规则配合静态检查能挡掉大部分循环依赖。4.2 业务层越来越臃肿怎么办业务层臃肿的典型症状是一个类几千行方法几十个改一个需求要在里面找半天。根本原因是把流程编排、规则计算、数据转换全塞在了一起。我的处理办法是先做职责识别把方法按“流程调度”“规则判断”“数据组装”分类流程调度类的抽到应用层规则判断类的聚合成领域对象数据组装类的移到转换器里。这个过程不需要一次性重构完可以按需求迭代逐步迁移。关键是先建立分类意识新写的代码不要再往老类里堆。另一个实用技巧是给业务层设一个“行数警戒线”比如单个类超过五百行就强制 review看看是不是职责过载。这个数字不是绝对的但它能起到提醒作用。4.3 层间数据不一致的排查思路跨层传递数据时偶尔会遇到“上层拿到的数据和下层查出来的不一致”这种诡异问题。排查顺序我一般是这样的排查步骤检查内容常见原因第一步下层返回值是否正确查询条件拼错、缓存未更新第二步转换过程是否丢字段转换器漏映射、默认值覆盖第三步上层是否做了二次加工格式化逻辑改了原对象第四步是否存在并发修改共享对象被多线程改写我遇到最多的是第三步——上层为了展示方便直接在传输对象上改了字段而这个对象又被别处引用导致数据被污染。解决办法是传输对象尽量设计成不可变对象或者在上层使用前做一次防御性拷贝。4.4 分层过度导致开发效率下降这是另一个极端。有些团队把分层当成教条每个层都要求严格的接口、转换、异常包装结果一个简单的查询需求要写五六个类。我的判断标准是如果一层里超过七成的方法是纯转发这层就是多余的。分层的目的是隔离变化如果一个层没有隔离任何实际变化它只是增加了调用链长度。实际项目里我会根据变化频率动态调整变化频繁的边界保留分层几乎不变的部分允许合并。架构是服务于业务的不是反过来。4.5 单元测试怎么配合分层写分层做得好测试就好写这是分层的一个隐性红利。我的测试策略是按层分配数据访问层用真实数据库的轻量实例做集成测试验证查询和映射正确。业务层用模拟实现替换数据访问层专注测业务规则和分支覆盖。表现层测参数校验和输出格式业务逻辑用模拟的业务层替代。这样每一层的测试都聚焦在自己的职责上跑得快、定位准。如果发现某一层特别难测往往说明这层的职责没划清楚或者依赖没管好这时候应该回头调整设计而不是硬写测试。5. 分层架构的演进与取舍经验5.1 什么时候该引入分层什么时候不该不是所有项目一上来就需要严格分层。我的经验是原型阶段和一次性脚本不分层正式产品且预期会持续迭代的才分层。分层的成本在于额外的类和转换代码收益在于长期的可维护性。如果项目生命周期短、变化少分层的收益覆盖不了成本。判断信号可以看两条一是需求变更的频率如果每周都有新需求进来分层值得做二是团队规模超过三个人协作分层带来的边界清晰度就很有价值。反过来一个人维护的小工具硬套四层架构纯属自找麻烦。5.2 分层架构和微服务的关系经常有人问都上微服务了还需要分层吗答案是更需要。微服务解决的是进程间的业务边界问题分层解决的是进程内的职责隔离问题两者不冲突。一个微服务内部如果代码不分层照样会烂只是烂的范围被限制在一个服务里而已。实际做法是微服务划分确定业务边界每个服务内部再按分层组织代码。服务之间的调用通过接口契约服务内部的调用通过层间接口。这样宏观和微观两个层面都有清晰的边界。5.3 我踩过的三个真实坑第一个坑是过早抽象。项目初期就设计了复杂的层间接口和转换体系结果业务方向调整一半的层根本用不上删起来还牵连一片。教训是分层从最简的三层开始等变化点真正出现再细化。第二个坑是层间共享工具类。为了方便建了个公共工具包结果各层都往里塞东西最后这个包变成了大杂烩依赖关系彻底乱掉。后来改成公共包只放纯技术工具字符串处理、日期计算业务相关的工具一律归到对应层。第三个坑是忽视构建约束。早期靠代码评审来保证不跨层调用评审一忙就漏等发现时已经积了一堆违规引用。后来在构建流程里加了依赖检查违规直接构建失败问题才根治。5.4 给不同阶段团队的建议刚起步的小团队我建议先把三层架构的职责边界讲清楚用目录结构把层体现出来接口先简单定义重点是养成“不跨层直接调用”的习惯。这个阶段不要追求完美的抽象能跑通、边界清楚就行。有一定规模、业务开始复杂的团队可以考虑把领域逻辑独立出来引入应用层做流程编排。这个阶段的关键是培养领域建模的意识让业务规则有独立的表达位置而不是散落在各个服务方法里。成熟团队可以进一步在层内做模块化配合依赖检查和自动化测试把分层约束固化到工程流程里。这个阶段分层已经不是靠人记规则而是靠工具保障。最后分享一个我在实际项目里一直用的自检方法随便挑一个需求变更问自己“这个改动会波及几层”。如果答案是超过两层说明层间耦合还是太重值得回头看看接口设计和职责划分。分层做得好不好不看图画得多漂亮看的是改一个需求时你能不能快速说出“只需要动这一层”。这个判断标准比任何架构评审都实在。
阅读完成 · 觉得有帮助?
咨询建站