IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    基于DDD的编码实践

    bty834发表于 2024-06-19 00:35:47
    love 0

    分层设计

    领域驱动设计(Domain-driven design, DDD) 作为一种复杂软件系统的应对方案,在设计和编码提供了一种新的解决方式,即领域驱动,要求程序员在设计和编码时从领域专家的角度
    出发来实现架构/代码,做到代码即业务。同时利用各种方式拆解复杂模块,常用的方式有拆分子域、构建富血对象。

    设计时,需要建立统一语言,确保领域中的业务概念处于同一个限界上下文,比如在一套电商系统中,用户买了一个东西,对应后台有一个订单,此时订单指代
    订单域的一项数据,当该订单需要发货时,在物流域中也会接受订单域输入并产生发货订单,此时,物流域的订单和订单域的订单就不处于一个限界上下文。建立统一语言有助于
    后续的产品和研发之间的高效沟通,打破代码和业务的语义鸿沟。领域模型的设计方法有 用例分析、事件风暴,领域模型需要提取出核心功能,并保证一定的扩展性,往往该过程是最重要也是最困难的。

    进入编码阶段,构建聚合、聚合根、实体、值对象。虽然领域层与业务逻辑强关联,但是为了技术实现,在设计时也会有一些妥协,如,聚合不宜设计的过大,聚合的设计需要考虑
    实体之间的一致性要求,同时有一些事务、锁的使用在某些时候会侵入领域层(并非不能这样,实践中往往在实现时会借鉴DDD的思想,但不会全套照搬);除此之外,结合事件驱动的方式,
    可以让领域层代码保留一定的扩展性,实现上可以参考文章SpringEvent扩展性利器;
    领域层作为核心不应该依赖具体实现,借鉴六边形架构,领域层中定义了仓储协议(Repository接口),业务逻辑只需要从仓储接口中获取数据,至于实现领域层并不关系,而具体的实现由其他模块如infrastructure层来实现;
    同时,在实际处理输入时(http,rpc,job...)通常涉及与其他域的交互,DDD中通过构建防腐层来应对外部变化。

    最终得到的代码分层结构如下图,Maven archetype代码参见:ddd-spring-web-maven-archetype:

    编码tips

    构建富血实体

    经典的MVC架构基于贫血对象构建,贫血对象只作为data class,其业务含义丢失,通过构建富血对象将业务实体的
    逻辑内聚,不在分散在各个service中,一是业务含义清晰,二是能够单点控制。

    比如,判断ExpressAggregate物流聚合的发货状态,其含有字段如下:

    @Data
    public class ExpressAggregate {
        
        private ExpressNumber expressNumber;
        // 状态
        private ExpressStatus expressStatus;
        ...
    }
    

    基于贫血对象,判断该物流实体是否发货需要在service中调用ExpressAggregate做判断:

    ExpressAggregate expressAggregate = ...;
    if (Objects.equals(express.status,...)){
      // bisiness logic
      ...
    }

    而基于富血对象,我们可以将是否发货的逻辑内置与ExpressAggregate中:

    @Data
    public class ExpressAggregate {
    
        private ExpressNumber expressNumber;
        private ExpressStatus expressStatus;
        ...
    
        /**
         * 判断是否发货
         * @return
         */
        public boolean hasSent() {
            return Objects.equals(this.expressStatus, ExpressStatus.SENT)
                       || Objects.equals(this.expressStatus, ExpressStatus.RECEIVED)
                       || Objects.equals(this.expressStatus, ExpressStatus.RETURN);
        }
    }

    这样,调用方直接使用 expressAggregate.hasSent() 即可知道结果,避免了判断逻辑散落各处。

    值对象不可变

    使用值对象表示无唯一标识(id)含义的实体,其各项属性相等即视为同一个值对象,因此值对象不可变。在实现层面,
    值对象不应有setter:

    // 无setter
    @Getter
    @AllArgsConstructor(staticName = "of")
    public class ExpressNumber {
        private String expressNumber;
    }
    
    // usage
    ExpressNumber expressNum = ExpressNumber.of("abc123");

    相比于直接使用String expressNumber , 在业务代码中使用ExpressNumber具有更强的业务含义,且作为方法入参时不易与其他String类型参数弄混。

    层间对象转换

    不同层的对象不应混用,层间调用应使用转换器转换,转换工作由谁来做?谁有转换需求谁来做。

    CQRS

    CQRS(Command Query Responsibility Segregation) 将输入分为 Command 和 Query,
    Command作为变更系统状态的输入由领域层(聚合根)处理,而Query可不走领域层。


    图片来源:Axon Framework : Architecture Overview



沪ICP备19023445号-2号
友情链接