业务逻辑全塞在 Service 层,项目上线半年后为什么连加个字段都不敢?
你在面试时肯定听过“高内聚低耦合”。但看看实际的项目代码,大部分情况还是从 Controller 接收 DTO,传给 Service,Service 里写几百行甚至几千行的if-else和业务逻辑,最后调用 Dao 存进数据库。
这套 CRUD 模式在项目初期开发极快,这也是它能流行这么多年的原因。但等项目上线半年后,业务开始加需求了。比如原来只卖普通商品,现在要加虚拟商品、组合商品、秒杀商品。原来只用校验库存,现在还要算会员折扣、积分抵扣、黑名单拦截。
这个时候,你的OrderService.createOrder()方法可能已经膨胀到了 800 行。当你接到一个“商品下架时,判断如果是组合商品则同时下架子商品”的需求时,你打开 Service 层,看着里面错综复杂的校验和状态流转,心里只剩下一个想法:加个字段或者改个if,会不会把别的逻辑搞崩?
这就是典型的发展痛点:代码腐化。不敢重构的原因是没有边界。
这种把所有状态判断、计算逻辑都堆在 Service 层,而实体类(Entity)只包含get和set方法的写法,被称为“贫血模型”(Anemic Domain Model)。数据和行为是彻底分离的。
领域驱动设计(DDD,Domain-Driven Design)给出了一种解法。
核心变化是把业务规则收拢回对象本身,也就是所谓的“充血模型”。
来看一个直观的例子。
在传统的贫血模型中,我们修改密码通常是这样写的:
publicvoidchangePassword(LonguserId,StringnewPassword){Useruser=userMapper.selectById(userId);if(user.getStatus()==UserStatus.LOCKED){thrownewBizException("账号已锁定");}// 很多很多其他校验...user.setPassword(passwordEncoder.encode(newPassword));userMapper.update(user);}这段代码看起来没问题,但User的状态规则散落在各个 Service 里。如果哪天加了一个新状态“待激活”,你需要去所有用到 User 的 Service 里把相关判断都加一遍。漏掉一个就是线上 Bug。
换成 DDD 的充血模型思路,业务逻辑是属于User领域的,它应该长这样:
// User 实体内部publicvoidchangePassword(StringnewPassword,PasswordEncoderencoder){if(this.status==UserStatus.LOCKED){thrownewBizException("账号已锁定,无法修改密码");}this.password=encoder.encode(newPassword);}然后在应用服务层(Application Service)里,代码变成了纯粹的流程编排:
publicvoidchangePassword(LonguserId,StringnewPassword){Useruser=userRepository.findById(userId);user.changePassword(newPassword,passwordEncoder);userRepository.save(user);}注意到了吗?核心业务规则不再由 Service 掌管,而是实体类自己维护自己的不变量(Invariants)。不管有多少个入口需要修改密码,校验逻辑永远在实体内部,不会散落。
这只是 DDD 的冰山一角。更宏观层面上,DDD 强调“限界上下文”(Bounded Context)。在大型系统中,用户的概念在登录模块、订单模块、物流模块里是完全不同的。如果不做上下文隔离,大家都去修改那张包含了上百个字段的巨大t_user表,项目最终必然变成一团乱麻。
划清边界,让领域对象自己管理自己的状态,是 DDD 应对软件复杂度的有效手段。
但千万别为了 DDD 而 DDD。
如果你只是在写一个简单的后台管理系统,每天的需求就是加减几个表单字段,用 MVC 加上贫血模型绝对是开发效率最高的。DDD 引入了聚合根、值对象、领域服务等大量新概念,它的存在是为了应对复杂的业务变化,而不是为了让简单的增删改查变得高端。
评估你的项目,如果是强业务规则、多状态流转、高复杂度的系统,下一次写代码前,可以试着先把逻辑放进对象里,而不是 Service 里。