从领域模型出发,让Java业务代码告别过程式脚本

当Service层沦为“脚本执行器”

很多Java项目在初期进展迅速,但随着业务迭代,你会逐渐发现一个令人头疼的模式:Controller接收参数,然后一股脑地扔给一个庞大的Service。这个Service类里充斥着各种if-else判断、零散的数据校验、对多个DAO的调用、以及对外部服务的拼接。它像一个无所不能的“上帝类”,但仔细看,它只是在机械地执行一系列步骤——读取数据、计算、再保存数据。业务规则散落在各个方法里,与数据库表结构深度绑定,这就是典型的过程式脚本代码。

从领域模型出发,让Java业务代码告别过程式脚本

这种代码最致命的问题在于,它让“业务”本身消失了。你无法从一个Order对象上看到“支付”或“取消”的行为,这些逻辑都隐藏在某个Service的某个方法深处。当需求变更时,你需要在成百上千行的Service代码里小心翼翼地寻找所有相关逻辑,生怕漏掉一处。团队协作也变成噩梦,因为大家没有统一的业务语言,开发眼中的“订单状态”和产品经理口中的可能完全不是一回事。

领域驱动设计的核心价值,正是要扭转这种局面。它要求我们首先关注业务本身,构建一个反映真实业务规则的领域模型,然后让代码成为这个模型的直接表达,而不是数据库的附庸。

战略先行:用统一语言和限界上下文划清边界

在动手写代码之前,必须先进行战略设计,否则很容易把DDD做成另一种形式的“贫血模型+DAO”。战略设计的首要产出是“统一语言”。这意味着团队(产品、开发、测试)必须就核心业务概念达成一致,并且这些概念要直接体现在代码的类名、方法名、甚至数据库表名中。例如,全团队都叫“订单履约”,那么代码里就不应该出现OrderSendService或DeliveryManager这样自行发明的术语。

在统一语言的基础上,我们需要识别“限界上下文”。这是DDD中最具威力的概念之一,它定义了某个业务概念的边界和职责。一个经典的例子是电商系统中的“用户”:在“订单上下文”里,用户可能只关心用户ID和收货地址;而在“会员与账户上下文”里,用户则涉及登录密码、积分、等级等信息。这两个“用户”模型不同,职责不同,它们属于不同的限界上下文。

在Java工程中,限界上下文通常对应一个独立的模块或顶级包。这种划分强制性地隔离了代码,使得每个上下文可以独立演化。上下文之间的协作,则需要通过明确的“上下文映射”来定义,例如使用“防腐层”来隔离和转换外部模型,避免外部变化直接污染核心领域。

战术核心:让聚合根成为业务行为的家园

战术设计是将战略落地的关键,其核心是构建“聚合”。一个聚合是一组高度内聚的对象的集合(包括聚合根、实体、值对象),它被视为一个整体进行数据修改和一致性维护。而“聚合根”,则是这个整体的唯一入口和指挥官。

让代码摆脱过程式脚本的关键一步,就是将业务行为从Service迁移到聚合根内部。一个贫血的Order对象可能只有一堆getter和setter,而一个富血的Order聚合根,则应该包含像place()pay()cancel()这样的业务方法。这些方法封装了状态变更的规则和校验。

例如,一个订单的支付逻辑不应该散落在OrderService的payOrder方法里,而是应该内聚在Order聚合根中:

public class Order {
    private OrderId id;
    private OrderStatus status;
    private Money totalAmount;

    // 业务行为:支付
    public void pay(Payment payment) {
        // 业务规则校验:状态必须为待支付
        if (this.status != OrderStatus.PENDING_PAY) {
            throw new OrderPaymentException("当前订单状态不允许支付");
        }
        // 业务规则校验:支付金额必须匹配
        if (!payment.getAmount().equals(this.totalAmount)) {
            throw new OrderPaymentException("支付金额不匹配");
        }
        // 执行状态变更
        this.status = OrderStatus.PAID;
        // 可以在这里记录支付流水或触发领域事件
    }
    // 其他getter...
}

同时,要善用“值对象”来封装那些没有独立身份但具有业务含义的概念,如Money、Address。值对象应该是不可变的,并且基于其所有属性值来判断相等性,这能极大增强代码的表达能力和安全性。

明确职责:标准分层架构下的协作模式

采用DDD后,项目结构会演变为清晰的四层架构,每一层都有其严格的职责,这是杜绝代码随意堆砌的工程保障。

分层 核心职责 包含内容 禁止行为
用户接口层 处理用户请求与响应 Controller, DTO, 参数校验 包含任何业务逻辑
应用层 编排业务流程,管理事务 应用服务, 用例入口, 事件发布 实现具体业务规则
领域层 实现所有业务规则与逻辑 聚合根, 实体, 值对象, 领域服务, 仓储接口 依赖任何外部技术框架(如Spring, MyBatis)
基础设施层 提供技术实现细节 仓储实现, 数据库操作, RPC客户端, 消息发送 包含业务规则

在这个架构下,传统的“大Service”被拆解了:

  • 业务流程编排交给应用层的Application Service。它很“薄”,只负责按顺序调用领域层的多个聚合或领域服务,并管理事务边界。
  • 具体的业务规则则内聚在领域层的聚合根和领域服务中。领域服务仅处理那些涉及多个聚合的复杂规则。
  • 数据持久化通过“仓储”接口进行抽象,领域层只依赖接口,具体实现由基础设施层提供。

这样,当你阅读领域层的代码时,你看到的就是纯粹的、与技术无关的业务逻辑。技术细节的变更(如更换ORM框架)被隔离在基础设施层,不会波及核心业务代码。

实战中的典型陷阱与规避建议

从过程式脚本转向领域模型并非一蹴而就,团队在实践中常会落入一些陷阱。

陷阱一:聚合根设计过大。 试图让一个聚合根管理过多实体,导致加载和保存性能低下,事务边界过大。正确的做法是遵循“小而专”的原则,确保聚合内对象生命周期一致,且修改频率协同。

陷阱二:领域层泄露技术细节。 在领域实体中使用了JPA的@Entity注解或引用了Spring的Bean。这严重破坏了领域层的纯洁性。必须严格遵循依赖倒置,领域层只定义仓储接口,实现细节放在基础设施层。

陷阱三:将应用服务当成新的“大Service”。 避免在应用服务里写满业务逻辑判断。它的职责应该是“协调”,而不是“实现”。如果一个业务规则只涉及一个聚合,请毫不犹豫地把它放到该聚合根的方法里去。

对于大部分团队,一个渐进式的落地路径可能更可行:

  1. 从统一语言开始: 在新功能或核心模块的重构中,先和产品、测试对齐关键术语。
  2. 识别并封装核心聚合: 挑选一个业务核心域(如“订单”),将其重构为富血的聚合根,把相关的校验和行为移入。
  3. 引入仓储抽象: 为这个聚合根定义仓储接口,将原有的DAO调用替换为面向聚合的仓储方法调用。
  4. 按需引入领域事件: 当存在需要解耦的跨聚合业务协作时(如订单支付后通知库存扣减),再引入领域事件来实现最终一致性。

写在最后:思维转变重于技术实现

让Java业务代码摆脱过程式脚本,本质上是一场开发思维的转变。它要求我们从“数据库驱动”的思维,转向“业务驱动”的思维。我们不再问“这个表怎么设计”,而是问“这个业务概念是什么,它有哪些行为,有什么规则”。

这种转变带来的收益是长期的:代码的可读性、可测试性、可维护性会显著提升。当新成员加入时,他可以通过阅读领域层的代码快速理解业务核心。当需求变更时,修改点通常被限制在某个具体的聚合或限界上下文内。

当然,DDD并非银弹,对于简单的CRUD管理系统,传统的三层架构可能更高效。但对于那些业务规则复杂、迭代频繁、需要长期维护的系统,投资时间建立清晰的领域模型,是避免代码腐化为“屎山”的最有效手段。它让我们的代码不再是机械执行命令的脚本,而是真正承载业务知识、反映业务演化的活文档。

原创文章,作者:,如若转载,请注明出处:https://fczx.net/wiki/137

(0)

相关推荐