type
status
date
slug
summary
tags
category
icon
password
catalog
sort
在软件开发的世界里,代码就如同城市的建筑,需要精心规划布局,才能高效运转。许多项目初期,开发者一心扑在功能实现上,代码一股脑堆砌,很快就陷入 “混沌”。想象一个没有分区规划的城市,商业区、住宅区、工业区混杂,交通拥堵,水电供应混乱。项目代码也是如此,业务逻辑、数据操作、用户交互代码交织,牵一发而动全身,修改一处功能,可能导致看似不相关的地方出错,排查问题像大海捞针,耗费大量时间精力。
这类问题的根源在于代码职责不清晰。每个功能模块都承担过多任务,既处理复杂业务规则,又直接操作数据库读写,还负责与用户交互展示,就像一个人既要当厨师、服务员,又要做收银员,最终哪个角色都做不好。这种混沌状态下,代码难以理解、维护和扩展,成为项目持续发展的 “绊脚石”。

一、为啥要搞分层?聊聊那些被"混沌代码"坑过的痛

当项目里的代码像乱炖一样堆在一起——一个类里既处理HTTP请求,又算业务逻辑,还直接写SQL操作数据库——你是不是经历过这些绝望时刻?
  • 改一行代码,到处出bug:想调整下数据库查询方式,结果前端页面突然报错了(因为代码耦合太紧,牵一发动全身);
  • 找个功能像"大海捞针":要改"订单价格计算",翻了十几个类才发现逻辑藏在Controller里,还和表单校验混在一起;
  • 测试跑不起来:写个单元测试,得先启动数据库、模拟前端请求,跑一次要等5分钟;
  • 新人上手哭唧唧:刚入职的同学看代码像看天书,光理清楚"数据从哪来、到哪去"就花了一周;
  • 重复代码堆成山:同样的"手机号格式校验",在Controller、Service、DAO里各写了一遍,改的时候漏了一处就出问题。
这些坑的根源,其实是代码职责没分清。而分层架构,就是给代码"划地盘"——让每个模块只干自己该干的事,就像餐厅里服务员只管点菜、厨师只管做菜、采购只管进货,各司其职才高效。
notion image

二、分层的意义:不止"好看",更是团队协作的"定海神针"

分层不是为了"目录工整",而是实实在在解决协作问题:
  • 新人秒懂:刚接手项目的同学,看目录结构就知道"找接口看Controller,查业务逻辑看Service",不用逐行啃代码;
  • 改代码不慌:要换数据库?只动DAO层;要改前端交互格式?只调Controller层的响应——不用担心改一处塌一片;
  • 测试变简单:测业务逻辑时,不用启动数据库和前端服务,直接调Service方法就行,效率翻几倍;
  • 分工更明确:前端同学对接Controller层接口,后端同学专注Service业务逻辑,数据库专家优化DAO层——各干各的,互不打扰;
  • 抗住"业务膨胀":后期加功能,知道该往哪插代码(比如加个会员折扣,直接在Service层加逻辑),不会越改越乱。
简单说:分层是给代码"定规矩",规矩越清,团队协作越顺,项目走得越远

三、核心三层:Controller-Service-DAO,每层该干啥、不该干啥?

想象成盖房子:Controller是"门面"(和用户打交道),Service是"承重墙"(核心骨架),DAO是"地基"(和土地打交道),三层各司其职。
notion image

3.1 Controller层(门面担当:接请求、给响应)

定位:用户(或前端)的"直接对接人",负责HTTP协议那点事。
该干的活
  • 接请求:解析URL参数、请求体(比如用@RequestBody拿前端传的JSON);
  • 做校验:查参数格式对不对(比如手机号是不是11位,用@Valid注解);
  • 调Service:把请求丢给业务层处理,自己不碰业务逻辑;
  • 给响应:把Service返回的结果包装成JSON/XML,告诉前端"成了"还是"败了"。
不该干的活
  • 别算业务逻辑(比如"订单满减多少钱"这事,丢给Service);
  • 别直接操作数据库(想查数据?让Service去叫DAO干);
  • 别搞复杂数据转换(比如把数据库实体转成前端要的格式,用专门的转换器)。
举个例子

3.2 Service层(核心担当:业务逻辑全在这)

定位:项目的"大脑",所有业务规则、流程都在这处理。
该干的活
  • 算业务:比如"订单价格=商品总价+运费-优惠券"、"库存不够时不让下单";
  • 管事务:保证操作的原子性(比如"下单"和"扣库存"要么都成,要么都失败,用@Transactional注解);
  • 协调资源:可能要叫DAO查数据库,还要调其他服务(比如支付接口、物流接口);
  • 转数据:把DAO查出来的数据库实体(Entity)转成业务层用的DTO(后面细说)。
不该干的活
  • 别处理HTTP细节(比如别管响应码是200还是400,那是Controller的事);
  • 别写SQL(查数据库?让DAO去干);
  • 别当"万能类":如果一个Service太大(比如OrderService里又有校验又有计算),拆成OrderValidatorOrderCalculator这样的小类。
举个例子

3.3 DAO层(地基担当:和数据库打交道)

定位:项目的"仓库管理员",专门负责数据的存、取、改、删。
该干的活
  • 写CRUD:用MyBatis/JPA/SQL查数据库、存数据(比如save()findById());
  • 处理数据映射:把数据库的表结构转成Java对象(Entity),比如数据库的user_name字段对应Entity的userName属性;
  • 优化查询:复杂SQL放这层调,比如分页查询、多表联查。
不该干的活
  • 别碰业务逻辑(比如"这个用户能不能删",让Service判断);
  • 别返回前端直接用的数据(只返回Entity,转格式让Service处理)。
举个例子
notion image

四、关键辅助包:这些"工具人"包,让分层更顺畅

除了核心三层,还有些"辅助角色"能帮我们规范代码结构,避免大家乱放文件。

4.1 model/entity/domain:存数据库"真身"

这里放的是和数据库表一一对应的实体类(比如UserEntity对应user表),每个字段都和表字段对应(用@Column注解标出来)。
注意:这玩意只在DAO和Service层流转,别直接返回给前端(比如Entity里有password密码字段,前端不需要看)。

4.2 dto/vo:层间"快递盒",按需打包数据

  • DTO(Data Transfer Object):Service和Controller之间传数据的"盒子"。比如Service处理完订单,把需要给Controller的字段(订单号、总价)放进OrderDTO,多余的(比如数据库里的create_time)不塞进去。
  • VO(View Object):Controller返回给前端的"最终包装"。比如前端需要"订单状态中文描述"("已支付"而不是数据库里的1),就用OrderVO来放。
好处:数据按需传递,避免敏感信息泄露,也减少不必要的字段传输。

4.3 其他"工具包":各有各的用处

  • config:放配置类,比如数据库连接池配置、线程池配置(用@Configuration注解);
  • util:放通用工具方法,比如日期格式化(DateUtils)、字符串处理(StringUtils),注意:这里只放和业务无关的工具;
  • common:放全局通用类,比如统一响应格式(ApiResponse)、错误码枚举(ErrorCode);
  • exception:放自定义异常(比如BusinessException表示"余额不足")和全局异常处理器(GlobalExceptionHandler);
  • constant:放常量,比如订单状态(ORDER_STATUS_PAID = 1),别在代码里写死数字!

五、划清业务边界:别让代码"越界",这3条规则要记牢

分层的核心是"各管一摊",但实际写代码时很容易"手滑越界"(比如在Controller里写业务逻辑)。记住这几条"边界红线":
notion image

5.1 数据流转:什么数据该在哪层出现?

用"快递路线"打比方:
  • Entity(数据库实体):只能在DAO和Service之间跑(就像仓库内部的货,不直接送到客户手上);
  • DTO(业务数据传输对象):只在Service和Controller之间传(相当于仓库打包好的货,准备送给客户);
  • VO(视图对象):只在Controller和前端之间用(最后给客户的快递包装,按客户要求来)。
错误示范:在Controller里直接接收Entity并返回——相当于把仓库里带包装的货直接丢给客户,又乱又不安全(可能泄露数据库字段名)。
正确做法:用转换器(比如OrderConverter)在层间做数据转换,比如Entity→DTO→VO,每层只处理自己该用的数据。

5.2 依赖方向:只能"上层调下层",不能反过来

记住这个顺序:Controller → Service → DAO(上层依赖下层,下层不认识上层)。
  • 允许:Controller里调用Service,Service里调用DAO;
  • 禁止:DAO里调用Service(地基不能指挥承重墙),Service里调用Controller(承重墙不能指挥门面)。
为什么? 下层是"基础服务",上层是"使用者"。如果DAO依赖Service,那改Service时,DAO也得跟着动,就乱套了。

5.3 异常处理:在哪抛、在哪接,别乱抓

  • DAO层:只抛技术异常(比如"数据库连接失败"),不处理业务问题(别在DAO里判断"余额不足");
  • Service层:把技术异常转成业务异常(比如DAO抛"查不到用户",Service转成UserNotFoundException);
  • Controller层:用全局异常处理器(GlobalExceptionHandler)统一接异常,返回友好提示(比如给前端"用户不存在,请检查ID")。
错误示范:在DAO层用try-catch吞掉异常——出了问题根本不知道在哪报错,排查起来要人命。

六、模块依赖:别搞"蜘蛛网",要做"金字塔"

大型项目除了分层,还要按业务拆模块(比如"订单模块""用户模块""支付模块")。模块之间的依赖也得有规矩:

6.1 依赖原则:"向下依赖",别搞"交叉依赖"

  • 允许:上层模块依赖下层模块(比如"订单模块"依赖"用户模块"查用户信息);
  • 禁止:A模块依赖B,B又依赖A(比如"订单模块"调用"支付模块","支付模块"又调回"订单模块"),这会形成"死循环",改一个模块两个都得动。
小技巧:如果两个模块必须互相调用,抽一个"公共模块"(比如common-pay-order),让两者都依赖它,打破直接交叉。

6.2 接口隔离:模块间只认"接口",不认"实现"

比如"订单模块"需要调用"支付模块"的功能,应该:
  1. 支付模块定义一个接口(PaymentService);
  1. 订单模块只依赖这个接口,不管它是用支付宝还是微信实现的;
  1. 支付模块自己写实现类(AlipayServiceImpl)。
这样,支付模块想换实现(比如从支付宝换成微信),订单模块完全不用改——这就是"面向接口编程"的好处。

七、Service间依赖的那些"暗坑":同层互调太随意,项目早晚会"塌房"

前面聊了三层的分工,但实际开发中最容易出乱子的,往往是Service层内部的互相依赖。就像一群厨师在厨房各做各的菜,突然张三喊李四"借点酱油",李四喊王五"帮我切个葱",最后谁的活都没干利索——Service之间的依赖如果没规矩,比三层混乱更要命。

7.1 先踩坑:这些Service依赖的"作死操作"你肯定见过

  • 循环依赖死锁:订单Service调用支付Service,支付Service又调订单Service查状态,启动项目时直接报Circular Dependency错误,排查半天发现是互相引用;
  • 改A崩B:改了用户Service的getUserInfo方法返回值,结果订单Service因为依赖这个方法,突然报空指针(没做兼容);
  • 测试一团糟:测订单Service时,因为它依赖支付、库存、物流三个Service,得把这三个服务全启动,不然单元测试跑不起来;
  • 业务缠成"毛线团":一个"下单"流程,订单Service里直接调了支付Service的pay()、库存Service的deduct()、物流Service的createExpress(),几百行代码里混着四五个模块的逻辑,想加个"跨境订单不支持某物流",得在订单Service里硬塞判断,越改越乱。
这些坑的根源,其实是Service之间"边界不清"——没搞清楚"哪些依赖是必要的"、"用什么方式依赖才安全"。接下来,咱们分两种场景拆招:同业务模块内的Service互调,和不同业务模块的Service跨域调用。

7.2 同业务模块内的Service依赖:用"中介者"打破"小圈子"

同一个业务模块(比如"订单模块")里,可能有多个Service:OrderService(主订单)、OrderItemService(订单项)、OrderDiscountService(订单折扣)。它们低头不见抬头见,很容易互相调用形成"小圈子"。

问题场景:订单项算错了,订单总金额跟着错

比如OrderService在计算总金额时,直接调用OrderItemService.calculateItemPrice(),而OrderItemService在计算单品价格时,又调用OrderService.getBasePrice()(因为单品价格依赖订单的基础折扣)。结果就是:
  • OrderService的基础折扣逻辑,OrderItemService的计算结果可能变;
  • OrderItemService的一个方法,OrderService直接报错;
  • 调试时,得在两个Service之间跳来跳去,理不清调用链。
这就是典型的双向依赖,像两个人互相拽着对方的胳膊,谁也动不了。

解耦神器:中介者模式(Mediator Pattern)

把互相拉扯的Service们,交给一个"中介"来协调。就像小区里的业主们不直接互相打交道,有事找物业(中介者),效率更高。
实现步骤
  1. 定义一个中介者接口(比如OrderMediator),里面包含模块内需要的协调方法(如calculateTotalPrice(OrderDTO order));
  1. 让模块内的Service(OrderServiceOrderItemService)都依赖这个中介者,而不是互相依赖;
  1. 中介者的实现类(OrderMediatorImpl)里,封装原本Service之间的调用逻辑,比如"先算订单项价格,再叠加订单折扣"。
代码示例
效果
  • OrderService再也不用直接调用OrderItemService了,改订单项逻辑只动中介者和OrderItemService
  • 新加入OrderCouponService(优惠券),只需在中介者里加一行调用,其他Service不用改;
  • 测试OrderService时,只需Mock中介者返回固定价格,不用启动OrderItemService,效率翻倍。

7.3 不同业务模块的Service依赖:别直接"串门",用"事件"传消息

跨模块调用更危险:比如订单模块(OrderService)直接调用支付模块(PaymentService)、库存模块(InventoryService)、物流模块(LogisticsService)。一旦支付模块改了接口,订单模块就得跟着改,牵一发而动全身。

问题场景:订单创建后,要通知N个模块,代码越写越乱

原始代码可能这样:
这样的代码,就像订单模块成了"大管家",所有模块的变动都得它点头。哪天支付模块把createPayment的参数从orderId改成orderNo,订单模块就得改代码重新测试——这就是典型的紧耦合

解耦神器1:观察者模式(Observer Pattern)/事件驱动

订单模块只需要"喊一声"(发布事件),其他模块愿意听就自己处理(订阅事件),互不干涉。就像小区物业在群里发"停水通知",业主(对应模块)自己储水,物业不用挨个敲门。
在Spring里,用ApplicationEvent@EventListener就能轻松实现:
实现步骤
  1. 定义事件(比如OrderCreatedEvent),包含订单创建的关键信息(订单ID、金额、用户ID等);
  1. 订单模块在订单创建后,发布这个事件(applicationEventPublisher.publishEvent(event));
  1. 其他模块(支付、库存、物流)写监听器,监听这个事件并处理自己的逻辑。
代码示例
效果
  • 订单模块和其他模块彻底解耦:支付模块改接口?订单模块完全不用管;
  • 想加新功能(比如订单创建后自动投保),只需加个InsuranceService的监听器,原代码一行不动;
  • 测试订单模块时,不用启动支付、库存服务,因为事件发布后没人监听也不影响订单创建;
  • 异步处理:如果扣库存、创建物流单比较慢,监听器里加@Async注解就能异步执行,不阻塞订单创建(用户下单后不用等物流单创建完才看到成功)。

解耦神器2:门面模式(Facade Pattern)

如果跨模块调用需要"同步响应"(比如订单创建前必须先查用户是否为会员,需要用户模块返回结果),直接调用用户模块的UserService可能依赖太多细节(比如用户模块有getUser()、getUserLevel()、checkVipExpire()等方法)。
这时候可以用门面模式:用户模块提供一个统一的"门面接口",订单模块只依赖这个门面,不管内部实现。
代码示例
效果
  • 用户模块内部重构(比如把VipService拆成VipQueryServiceVipUpdateService),只要门面接口isValidVip()没变,订单模块完全不用改;
  • 权限控制:门面可以做访问限制,比如只允许订单模块调用isValidVip(),不允许直接调用userService.getById()(保护用户隐私字段);
  • 简化调用:订单模块不用知道"查会员要调两个Service",门面已经封装好了,减少出错概率。

7.4 案例:看看大厂是怎么玩的?

案例1:阿里电商的订单与支付解耦
阿里的订单系统创建订单后,并不会直接调用支付系统,而是通过"消息队列"(比如RocketMQ)发送一条"订单待支付"消息。支付系统监听消息后,创建支付单;支付成功后,支付系统再发一条"支付成功"消息,订单系统监听后更新订单状态。
这种基于消息队列的事件驱动,让订单和支付系统可以独立部署、独立升级,哪怕支付系统暂时宕机,订单系统也能正常创建订单(消息存在队列里,支付系统恢复后再处理)。
案例2:美团外卖的订单与配送协同
美团外卖下单后,订单系统发布"新订单创建"事件,配送系统、商家系统、营销系统同时监听:
  • 配送系统:生成配送任务,分配骑手;
  • 商家系统:打印订单小票,开始备餐;
  • 营销系统:给用户送一张"复购券"。 每个系统只处理自己的逻辑,订单系统完全不用关心"骑手怎么分配"——这就是观察者模式的典型应用,支撑了每天数千万订单的高效流转。

八、再补点"进阶料":让模块设计更上一层楼

除了前面说的核心三层和解耦技巧,还有些优秀实践能让项目更抗打:

8.1 引入"领域层(Domain)":复杂业务的"定盘星"

当业务越来越复杂(比如电商的促销规则:满减、叠券、会员折扣、限时特价可能同时生效),Service层会变得臃肿。这时候可以在Service和DAO之间加一层领域层,放核心业务对象(比如OrderProduct)和领域服务(OrderDomainService),让领域层专注于"业务规则本身",Service层负责协调和事务控制。
比如计算订单价格:
  • 领域层的OrderPricingDomainService只干一件事:用复杂的规则算出最终价格(不依赖DAO,纯内存计算);
  • Service层的OrderService调用领域层算价格,再调用DAO存数据——分工更细,逻辑更清。

8.2 用"模块API"约束跨模块调用

大型项目(比如有10+业务模块),可以给每个模块定义"API包":
  • 模块内部的类(OrderServiceImplInventoryDAO)放在internal包,对外隐藏;
  • 对外提供的接口(OrderService接口、OrderCreatedEvent事件、UserFacadeService)放在api包,明确告诉其他模块:"只能调这些"。
比如用户模块的目录结构:
这样,其他模块想调用户模块,只能依赖api包里的类,不用担心改了内部实现影响别人。

8.3 定期"体检":用这两个工具扫描坏味道

  • SonarQube:可以检测"循环依赖"(比如A依赖B,B依赖A)、"过大的类"(一个Service超过1000行代码,该拆了);
  • ArchUnit:能在单元测试里写规则,比如"Controller层不能直接调用DAO层"、"Service层不能依赖其他模块的internal包",每次提交代码自动检查,提前堵住设计漏洞。

九、当Service依赖遇上"事务":别让数据一致性拖垮设计

前面聊的解耦技巧,大多是"非事务场景"(比如订单创建后通知其他模块)。但如果依赖关系涉及数据一致性(比如下单时必须同时扣库存、创建支付单,要么全成功,要么全失败),直接用事件驱动可能出问题(比如订单保存成功,但扣库存失败,导致超卖)。这时候,得给解耦加一层"事务保护"。

9.1 坑场景:异步事件丢了"数据一致性"

比如用观察者模式时,订单创建成功后发布事件,库存模块扣库存失败:
  • 订单状态是"待支付",但库存没扣——用户可能重复下单,导致超卖;
  • 这就是"最终一致性"的漏洞:如果中间某一步失败,各模块数据会不一致。

9.2 解决方案:事务补偿+本地消息表(可靠事件)

可靠事件的核心思路是:先确保事件一定能发出去,再确保失败了能补救。就像寄快递,先填好快递单(本地消息表),确认快递员取走了(事件发送成功),才放心;如果对方没收到,还能查快递单重新发。
实现步骤(以订单扣库存为例)
  1. 订单库建一张local_message表,存要发送的事件(订单ID、事件类型、状态:待发送/已发送/失败);
  1. 订单创建时,在同一个事务里:保存订单 + 往local_message表插一条"订单创建"事件(状态待发送);
  1. 用一个定时任务(比如MessageSenderJob)扫描local_message表,把"待发送"的事件发到消息队列(比如RocketMQ);
  1. 库存模块消费事件扣库存,成功后回调订单系统标记"事件已处理";
  1. 如果库存扣减失败(比如库存不足),订单系统收到失败通知,执行补偿逻辑(比如把订单状态改成"创建失败",并通知用户)。
代码关键片段
效果
  • 确保"订单创建"和"扣库存"要么都成功,要么订单回滚/补偿(不会出现订单存在但库存没扣的情况);
  • 即使消息队列宕机,本地消息表会存着事件,恢复后定时任务重新发送,不会丢消息;
  • 适合分布式系统:订单和库存可能在不同数据库,用这种方式实现最终一致性,比分布式事务(如2PC)更轻量、更可靠。

9.3 案例:京东的"下单-库存"一致性方案

京东早期也遇到过"下单成功但库存没扣"的超卖问题,后来采用类似"本地消息表+定时补偿"的方案:
  • 订单创建时,先预占库存(库存表加一行"预占记录",状态"锁定");
  • 订单支付成功后,发布"支付成功"事件,库存模块将"预占"改成"已扣减";
  • 如果15分钟内未支付,定时任务自动释放预占库存(补偿逻辑);
  • 全程用消息表记录每个步骤,确保任何环节失败都能追溯和补救。 这套方案支撑了京东618每秒几十万订单的峰值,没再出现大规模超卖。

九、反模式避坑:这些"看似聪明"的设计,其实在埋雷

分层和模块设计中,有些做法乍一看"省事",实则会让项目慢慢烂掉,必须警惕:

9.1 反模式1:"万能Service"——什么都往里塞

症状:一个CommonService包含了"校验手机号、计算金额、发送短信、查用户信息"等N个不相关的方法,全项目都依赖它。
为什么坑
  • 改任何一个方法(比如改手机号校验规则),都得重新测试所有依赖它的模块;
  • 这个类会越来越大(可能超过5000行),没人敢删里面的方法(怕删了哪个模块崩了);
  • 新人想加个"校验邮箱"的方法,也只能往CommonService里塞,恶性循环。
解药:按功能拆成专用工具类/Service(PhoneValidatorSmsServicePriceCalculator),每个类只干一件事。

9.2 反模式2:"跨层调用"——Controller直接调DAO

症状:为了"少写几行代码",Controller里直接@Autowired DAO,跳过Service层:
为什么坑
  • 后续想加"用户查询权限校验"(比如普通用户不能查管理员信息),得在Controller里硬塞逻辑,破坏分层;
  • Service层的事务、缓存逻辑用不上(比如Service层可能缓存用户信息,Controller直接查DB会导致缓存失效);
  • 测试时,Controller测试得连DB,没法单独测接口格式。
解药:哪怕Service层暂时只有一行代码,也要保留:

9.3 反模式3:"数据对象混用"——Entity直接返回给前端

症状:为了省掉DTO/VO转换,Service直接返回Entity,Controller直接返回Entity:
为什么坑
  • 泄露敏感信息:Entity里可能有passwordlastLoginIp等字段,前端不该看到;
  • 数据库字段变动直接影响前端:比如把user_name改成nickname,Entity字段名跟着改,前端接口突然报错;
  • 无法灵活调整响应格式:前端想要"用户状态中文描述"("正常"而不是1),Entity里没有,得在Controller里硬加逻辑。
解药:严格按"Entity→DTO→VO"转换,用工具类(如MapStruct)自动生成转换代码,既安全又灵活。

9.4 反模式4:"全局常量类"——所有常量堆在一起

症状:搞一个Constants类,里面堆了订单、用户、支付等所有模块的常量:
为什么坑
  • 改订单超时时间,得动这个全局类,可能影响其他模块(比如有人误删用户相关常量);
  • 找常量像找垃圾:想查"支付类型有哪些",得在几百行里翻;
  • 模块依赖混乱:用户模块依赖这个类,其实只用到其中1个常量,却把订单、支付的常量也引进来了。
解药:按模块拆分常量类(OrderConstantsUserConstantsPayConstants),每个类只放自己模块的常量。

十、团队落地:怎么让大家都按"规矩"写代码?

分层设计的最大挑战不是"设计出来",而是"团队所有人都遵守"。分享几个落地技巧:

10.1 先搞"模板工程",从新建项目就定规矩

搭一个项目脚手架(比如用Spring Initializr定制),把分层目录、基础类(如BaseController、BaseService)、转换工具(MapStruct配置)都建好,新模块直接基于脚手架创建:
这样,新人一建项目就看到规范的目录,想不按分层写都难。

10.2 写"分层检查清单",Code Review时照着勾

Code Review(代码审查)时,用清单卡住不合规的代码:
  • Controller层:是否有业务逻辑?是否直接调用DAO?是否返回了Entity?
  • Service层:是否处理了HTTP细节?是否有循环依赖?事务注解加对了吗?
  • DAO层:是否有业务判断?SQL是否放在了正确的地方?
  • 跨模块调用:是否用了接口/门面?是否直接依赖了其他模块的实现类? 清单不用复杂,打印出来贴在工位上,团队慢慢就形成肌肉记忆了。

10.3 定期"重构日",给代码"瘦个身"

业务快速迭代时,难免有"临时妥协"的代码(比如赶需求时在Controller里塞了业务逻辑)。每月抽1天做"重构日",团队一起:
  • 把Service里的大方法拆成小方法;
  • 把跨层调用改回标准分层;
  • 删掉重复代码,合并相似功能;
  • 用SonarQube扫描出的"坏味道"做修复。 就像定期打扫房间,代码才不会积满"灰尘"。

10.4 案例:字节跳动的"模块治理"实践

字节跳动的产品(抖音、今日头条)迭代极快,早期也出现过代码混乱的问题。后来他们搞了"模块治理3步法":
  1. 画依赖图:用工具扫描所有模块的依赖关系,标红"循环依赖"和"不合理依赖"(比如DAO依赖Service);
  1. 定红线:发布《模块设计规范》,明确"Controller不能调DAO"、"跨模块必须用API包"等红线,CI/CD流水线自动检查,不合规的代码不让合并;
  1. 树标杆:选一个核心模块(如用户模块)按规范重构,作为示例,组织团队学习。 半年后,代码变更引发的bug率下降了40%,新人上手时间从1周缩到2天。

十一、总结:好的模块设计,是"养出来"的

怎么判断分层和模块设计好不好?用这3个"试金石":

11.1 改一处,只动一个地方

比如要改"订单超时时间从24小时改成48小时":
  • 好设计:只在OrderConstants常量类里改个数字;
  • 坏设计:得在Controller、Service、DAO里改多处硬编码的"24"。
标准一个需求对应一个修改点,改得越少越靠谱。

11.2 删功能,不影响其他模块

比如要下架"积分兑换"功能:
  • 好设计:直接删掉PointExchangeService和相关Controller接口,其他模块(订单、支付)不受影响;
  • 坏设计:删了积分模块后,订单模块突然报错(因为订单里硬编码了积分计算逻辑)。
标准模块之间"可插拔",去掉一个不影响其他。

11.3 新人接手,3天能上手改功能

好的分层和模块设计,目录结构本身就是"说明书":
  • 想加接口?找Controller层对应模块;
  • 想改业务规则?找Service层;
  • 想调数据库查询?找DAO层。
新人不用通读所有代码,按目录就能定位到要改的地方——这才是"可维护"的终极体现。
项目的分层和模块设计,不是一开始就能完美的,就像种树:
  • 刚种下时(项目初期),要定好主干(核心三层),别让枝叶乱长(避免跨层调用、混用对象);
  • 长到一定程度(业务复杂后),要及时修剪(拆分大Service、解耦循环依赖);
  • 遇到风雨(高并发、需求变更),要加固根基(用事件驱动、可靠消息保证稳定性)。
记住:代码是给人看的,不是给机器看的。好的设计,是让写代码的人(包括未来的你)能轻松看懂、放心修改。当团队里没人再抱怨"这代码谁写的",而是说"这段代码改起来真顺",你的分层设计就成功了。
最后送一句:慢就是快——前期花10%的时间定分层、解耦合,后期能省90%的调试和重构时间,这才是对团队和项目最负责的做法。
Keycloak 客户端授权服务项目中 WebFlux 的全方位应用
Loading...
目录
0%