type
status
date
slug
summary
tags
category
icon
password
catalog
sort
在软件开发的世界里,代码就如同城市的建筑,需要精心规划布局,才能高效运转。许多项目初期,开发者一心扑在功能实现上,代码一股脑堆砌,很快就陷入 “混沌”。想象一个没有分区规划的城市,商业区、住宅区、工业区混杂,交通拥堵,水电供应混乱。项目代码也是如此,业务逻辑、数据操作、用户交互代码交织,牵一发而动全身,修改一处功能,可能导致看似不相关的地方出错,排查问题像大海捞针,耗费大量时间精力。
这类问题的根源在于代码职责不清晰。每个功能模块都承担过多任务,既处理复杂业务规则,又直接操作数据库读写,还负责与用户交互展示,就像一个人既要当厨师、服务员,又要做收银员,最终哪个角色都做不好。这种混沌状态下,代码难以理解、维护和扩展,成为项目持续发展的 “绊脚石”。
一、为啥要搞分层?聊聊那些被"混沌代码"坑过的痛
当项目里的代码像乱炖一样堆在一起——一个类里既处理HTTP请求,又算业务逻辑,还直接写SQL操作数据库——你是不是经历过这些绝望时刻?
- 改一行代码,到处出bug:想调整下数据库查询方式,结果前端页面突然报错了(因为代码耦合太紧,牵一发动全身);
- 找个功能像"大海捞针":要改"订单价格计算",翻了十几个类才发现逻辑藏在Controller里,还和表单校验混在一起;
- 测试跑不起来:写个单元测试,得先启动数据库、模拟前端请求,跑一次要等5分钟;
- 新人上手哭唧唧:刚入职的同学看代码像看天书,光理清楚"数据从哪来、到哪去"就花了一周;
- 重复代码堆成山:同样的"手机号格式校验",在Controller、Service、DAO里各写了一遍,改的时候漏了一处就出问题。
这些坑的根源,其实是代码职责没分清。而分层架构,就是给代码"划地盘"——让每个模块只干自己该干的事,就像餐厅里服务员只管点菜、厨师只管做菜、采购只管进货,各司其职才高效。

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

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
里又有校验又有计算),拆成OrderValidator
、OrderCalculator
这样的小类。
举个例子:
3.3 DAO层(地基担当:和数据库打交道)
定位:项目的"仓库管理员",专门负责数据的存、取、改、删。
该干的活:
- 写CRUD:用MyBatis/JPA/SQL查数据库、存数据(比如
save()
、findById()
);
- 处理数据映射:把数据库的表结构转成Java对象(Entity),比如数据库的
user_name
字段对应Entity的userName
属性;
- 优化查询:复杂SQL放这层调,比如分页查询、多表联查。
不该干的活:
- 别碰业务逻辑(比如"这个用户能不能删",让Service判断);
- 别返回前端直接用的数据(只返回Entity,转格式让Service处理)。
举个例子:

四、关键辅助包:这些"工具人"包,让分层更顺畅
除了核心三层,还有些"辅助角色"能帮我们规范代码结构,避免大家乱放文件。
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里写业务逻辑)。记住这几条"边界红线":

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 接口隔离:模块间只认"接口",不认"实现"
比如"订单模块"需要调用"支付模块"的功能,应该:
- 支付模块定义一个接口(
PaymentService
);
- 订单模块只依赖这个接口,不管它是用支付宝还是微信实现的;
- 支付模块自己写实现类(
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们,交给一个"中介"来协调。就像小区里的业主们不直接互相打交道,有事找物业(中介者),效率更高。
实现步骤:
- 定义一个中介者接口(比如
OrderMediator
),里面包含模块内需要的协调方法(如calculateTotalPrice(OrderDTO order)
);
- 让模块内的Service(
OrderService
、OrderItemService
)都依赖这个中介者,而不是互相依赖;
- 中介者的实现类(
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
就能轻松实现:实现步骤:
- 定义事件(比如
OrderCreatedEvent
),包含订单创建的关键信息(订单ID、金额、用户ID等);
- 订单模块在订单创建后,发布这个事件(
applicationEventPublisher.publishEvent(event)
);
- 其他模块(支付、库存、物流)写监听器,监听这个事件并处理自己的逻辑。
代码示例:
效果:
- 订单模块和其他模块彻底解耦:支付模块改接口?订单模块完全不用管;
- 想加新功能(比如订单创建后自动投保),只需加个
InsuranceService
的监听器,原代码一行不动;
- 测试订单模块时,不用启动支付、库存服务,因为事件发布后没人监听也不影响订单创建;
- 异步处理:如果扣库存、创建物流单比较慢,监听器里加
@Async
注解就能异步执行,不阻塞订单创建(用户下单后不用等物流单创建完才看到成功)。
解耦神器2:门面模式(Facade Pattern)
如果跨模块调用需要"同步响应"(比如订单创建前必须先查用户是否为会员,需要用户模块返回结果),直接调用用户模块的
UserService
可能依赖太多细节(比如用户模块有getUser()、getUserLevel()、checkVipExpire()
等方法)。这时候可以用门面模式:用户模块提供一个统一的"门面接口",订单模块只依赖这个门面,不管内部实现。
代码示例:
效果:
- 用户模块内部重构(比如把
VipService
拆成VipQueryService
和VipUpdateService
),只要门面接口isValidVip()
没变,订单模块完全不用改;
- 权限控制:门面可以做访问限制,比如只允许订单模块调用
isValidVip()
,不允许直接调用userService.getById()
(保护用户隐私字段);
- 简化调用:订单模块不用知道"查会员要调两个Service",门面已经封装好了,减少出错概率。
7.4 案例:看看大厂是怎么玩的?
案例1:阿里电商的订单与支付解耦
阿里的订单系统创建订单后,并不会直接调用支付系统,而是通过"消息队列"(比如RocketMQ)发送一条"订单待支付"消息。支付系统监听消息后,创建支付单;支付成功后,支付系统再发一条"支付成功"消息,订单系统监听后更新订单状态。
这种基于消息队列的事件驱动,让订单和支付系统可以独立部署、独立升级,哪怕支付系统暂时宕机,订单系统也能正常创建订单(消息存在队列里,支付系统恢复后再处理)。
案例2:美团外卖的订单与配送协同
美团外卖下单后,订单系统发布"新订单创建"事件,配送系统、商家系统、营销系统同时监听:
- 配送系统:生成配送任务,分配骑手;
- 商家系统:打印订单小票,开始备餐;
- 营销系统:给用户送一张"复购券"。 每个系统只处理自己的逻辑,订单系统完全不用关心"骑手怎么分配"——这就是观察者模式的典型应用,支撑了每天数千万订单的高效流转。
八、再补点"进阶料":让模块设计更上一层楼
除了前面说的核心三层和解耦技巧,还有些优秀实践能让项目更抗打:
8.1 引入"领域层(Domain)":复杂业务的"定盘星"
当业务越来越复杂(比如电商的促销规则:满减、叠券、会员折扣、限时特价可能同时生效),Service层会变得臃肿。这时候可以在Service和DAO之间加一层领域层,放核心业务对象(比如
Order
、Product
)和领域服务(OrderDomainService
),让领域层专注于"业务规则本身",Service层负责协调和事务控制。比如计算订单价格:
- 领域层的
OrderPricingDomainService
只干一件事:用复杂的规则算出最终价格(不依赖DAO,纯内存计算);
- Service层的
OrderService
调用领域层算价格,再调用DAO存数据——分工更细,逻辑更清。
8.2 用"模块API"约束跨模块调用
大型项目(比如有10+业务模块),可以给每个模块定义"API包":
- 模块内部的类(
OrderServiceImpl
、InventoryDAO
)放在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 解决方案:事务补偿+本地消息表(可靠事件)
可靠事件的核心思路是:先确保事件一定能发出去,再确保失败了能补救。就像寄快递,先填好快递单(本地消息表),确认快递员取走了(事件发送成功),才放心;如果对方没收到,还能查快递单重新发。
实现步骤(以订单扣库存为例):
- 订单库建一张
local_message
表,存要发送的事件(订单ID、事件类型、状态:待发送/已发送/失败);
- 订单创建时,在同一个事务里:保存订单 + 往
local_message
表插一条"订单创建"事件(状态待发送);
- 用一个定时任务(比如
MessageSenderJob
)扫描local_message
表,把"待发送"的事件发到消息队列(比如RocketMQ);
- 库存模块消费事件扣库存,成功后回调订单系统标记"事件已处理";
- 如果库存扣减失败(比如库存不足),订单系统收到失败通知,执行补偿逻辑(比如把订单状态改成"创建失败",并通知用户)。
代码关键片段:
效果:
- 确保"订单创建"和"扣库存"要么都成功,要么订单回滚/补偿(不会出现订单存在但库存没扣的情况);
- 即使消息队列宕机,本地消息表会存着事件,恢复后定时任务重新发送,不会丢消息;
- 适合分布式系统:订单和库存可能在不同数据库,用这种方式实现最终一致性,比分布式事务(如2PC)更轻量、更可靠。
9.3 案例:京东的"下单-库存"一致性方案
京东早期也遇到过"下单成功但库存没扣"的超卖问题,后来采用类似"本地消息表+定时补偿"的方案:
- 订单创建时,先预占库存(库存表加一行"预占记录",状态"锁定");
- 订单支付成功后,发布"支付成功"事件,库存模块将"预占"改成"已扣减";
- 如果15分钟内未支付,定时任务自动释放预占库存(补偿逻辑);
- 全程用消息表记录每个步骤,确保任何环节失败都能追溯和补救。 这套方案支撑了京东618每秒几十万订单的峰值,没再出现大规模超卖。
九、反模式避坑:这些"看似聪明"的设计,其实在埋雷
分层和模块设计中,有些做法乍一看"省事",实则会让项目慢慢烂掉,必须警惕:
9.1 反模式1:"万能Service"——什么都往里塞
症状:一个
CommonService
包含了"校验手机号、计算金额、发送短信、查用户信息"等N个不相关的方法,全项目都依赖它。为什么坑:
- 改任何一个方法(比如改手机号校验规则),都得重新测试所有依赖它的模块;
- 这个类会越来越大(可能超过5000行),没人敢删里面的方法(怕删了哪个模块崩了);
- 新人想加个"校验邮箱"的方法,也只能往
CommonService
里塞,恶性循环。
解药:按功能拆成专用工具类/Service(
PhoneValidator
、SmsService
、PriceCalculator
),每个类只干一件事。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里可能有
password
、lastLoginIp
等字段,前端不该看到;
- 数据库字段变动直接影响前端:比如把
user_name
改成nickname
,Entity字段名跟着改,前端接口突然报错;
- 无法灵活调整响应格式:前端想要"用户状态中文描述"("正常"而不是1),Entity里没有,得在Controller里硬加逻辑。
解药:严格按"Entity→DTO→VO"转换,用工具类(如MapStruct)自动生成转换代码,既安全又灵活。
9.4 反模式4:"全局常量类"——所有常量堆在一起
症状:搞一个
Constants
类,里面堆了订单、用户、支付等所有模块的常量:为什么坑:
- 改订单超时时间,得动这个全局类,可能影响其他模块(比如有人误删用户相关常量);
- 找常量像找垃圾:想查"支付类型有哪些",得在几百行里翻;
- 模块依赖混乱:用户模块依赖这个类,其实只用到其中1个常量,却把订单、支付的常量也引进来了。
解药:按模块拆分常量类(
OrderConstants
、UserConstants
、PayConstants
),每个类只放自己模块的常量。十、团队落地:怎么让大家都按"规矩"写代码?
分层设计的最大挑战不是"设计出来",而是"团队所有人都遵守"。分享几个落地技巧:
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步法":
- 画依赖图:用工具扫描所有模块的依赖关系,标红"循环依赖"和"不合理依赖"(比如DAO依赖Service);
- 定红线:发布《模块设计规范》,明确"Controller不能调DAO"、"跨模块必须用API包"等红线,CI/CD流水线自动检查,不合规的代码不让合并;
- 树标杆:选一个核心模块(如用户模块)按规范重构,作为示例,组织团队学习。 半年后,代码变更引发的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%的调试和重构时间,这才是对团队和项目最负责的做法。
- 作者:Honesty
- 链接:https://blog.hehouhui.cn/archives/22b0c7d0-9e17-80e7-af57-e95e6fe8fa70
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章