DDD应用架构内部分享-没错我又来了

2021/09/20 957点热度 0人点赞 0条评论

没错,又来了,一个项目的结束,就会复盘并完善下。

传统开发的弊病:

  • 通过事务脚本模式来开发需求;

  • 开发人员热衷于技术并通过技术手段解决问题,而不是深入思考和设计业务的走向;

  • 过于重视数据库,围绕数据库和数据模型进行建模,按数据流程进行建模;

  • 按技术视角进行业务命名,导致后续迭代以及人员更替时,产品和技术无法对齐;

  • 随着业务的发展,到后续业务、技术无法沟通,各种不理解;

  • 业务希望技术出排期,技术得撸代码,耗费精力;

  • 代码开发的过程中技术和业务耦合,一个场景一个服务,代码流水线;

  • 因为技术的问题会导致业务流程的中断,导致异常问题过多;

  • 过度方案太多,没有定期消除,导致后续的代码越来越难维护;

  • 业务只加需求,不减过期的需求,导致软件越来越臃肿;

DDD解决了什么?

  • 通用语言,让开发和业务在语言上统一;

  • 战略设计(业务和开发都能看到的软件模型)

  • 边界划分(通过限界上下文来划分,在不同的情景下域的作用是不同)

  • 领域划分(通过域来确定业务领域,包含核心域、子域、支撑域)

  • 分层架构(整洁架构)

  • 战术设计

  • 聚合(值对象,实体,聚合根)

  • 领域事件

最终要达到的目的:

  • 业务和技术能够通过领域模型建立通用语言(业务和技术的沟通无障碍);

  • 业务边界清晰(微服务的拆分);

  • 业务模型独立且模型具备可测试性;

  • 技术实现独立,可以随着业务的发展不断的更迭;

名词解析

事务脚本-面向过程

  • 事务:执行的业务

  • 脚本 一组系统执行的操作,和用户的逻辑关联;

根据接口请求,将业务逻辑通过面向过程组织为解决方案的过程;特点:

  • 简单 (仅依赖面向对象语言的少量功能)

  • 快速 开发快、上线快

  • 扩展性查

  • 改动以后测试性差

  • 业务复杂后,代码容易变成“大泥球”,系统腐化速度和复杂性呈指数级上升

领域模型

通过面向对象的设计,将业务实现模型化(将类的状态和行为分离);

特点:

  • 还原现实世界(复杂)

  • 责任清晰

  • 边界清晰

  • 完全面向对象,状态和行为分离;

  • 扩展性强

  • 测试性强

  • 简单的业务用领域模型反而复杂了;

如何理解领域?

  • 领域逻辑是显性的专业知识,符合事实逻辑(生活中的事务,必须满足一致性),可以很容易的推导出来;

  • 领域逻辑是提纯、通用的规则,不管我们的业务流程怎么变,我们的领域逻辑是不变的,所以领域一定是正确的,一定是合规的(合理性不由领域控制);

  • 领域逻辑是稳定的(不易变,易变的规则都应该放到应用层)

就像我们做的会员系统,用户在购买会员的时候

  • 参数校验 application层

  • 防重校验 application层

  • 有支付中的会员订单不能购买(随着业务的变动,易变)application层

  • 会员不能重复购买(随着业务的变动,易变)application层

  • 库存扣减与初始化订单 (不变)领域层

  • 支付(支付领域)领域层

  • 会员初始化 (不变)领域层

DDD

是对面向对象设计的改进,开发复杂业务逻辑的一种方式,但不是银弹。

特点:

  • 通过领域划分有助于把应用程序分解为服务;

  • 每个服务都有自己的领域模型;

  • 限界上下文清晰

实体

具有持久化ID的对象,能唯一标识一个条记录的;

特点:

  • 标识(identity)唯一不可变,且能区分出具体的事物

  • 连续性(continuity),贯穿整个生命周期

  • 只有根实体对外暴露(但不排除一些为了冗余而暴露别的信息)

在我们的系统里,用户实体的标识:包含id和租户,只有这两个组合我们才能唯一识别出这个用户

public class CustomerId {

    private Long id;
    private Integer tenantId;

    public CustomerId(Long id, Integer tenantId) {
        this.id = id;
        this.tenantId = tenantId;
    }

    public boolean isRealName(){
        if (Objects.isNull(this.id) || this.id == 0L){
            return false;
        }
        return true;
    }
}

值对象

⽤于描述状态的属性,特征,只关心对象是什么,不关注唯一性。

脱离了主体对象就没有任何意义,比如会员的有效期;

public class VipPeriod {

    /**
     * 当期会员开始时间
     */
    private Date currentStartTime;
    /**
     * 当期会员过期时间
     */
    private Date currentExpiredTime;
    /**
     * 会员有效连续性的开始时间
     */
    private Date validStartTime;
    /**
     * 最终过期时间
     */
    private Date lastExpiredTime;

    public Boolean isVip(){
        //是否会员判断逻辑
    }

    public VipStatus vipStatus(){
        //获取状态
    }
    //续费会员计算
    public void renew(VipType vipType){
        //根据不同的会员类型,做不同的计算
        addMoth();
    }
    //续费月会员逻辑
    private void addMoth(){

    }
    //续费季会员逻辑
    private void addQuarter(){

    }
    //续费季会员逻辑
    private void addQuarter(){

    }
}

聚合根

把一组有相同生命周期、在业务上不可分离的实体和值对象放在一起;

  • 定义了对象之间清晰的关系和边界,实现领域模型的内聚;

  • 必须将聚合根作为一个修改数据的单元(我们是在对应的领域服务中处理)

  • 一个聚合必须有一个聚合根,根是聚合中的一个实体;

  • 对一个聚合中实体的访问或操作,必须通过这个聚合的聚合根开始;(我们的聚合根只有访问,和聚合根的公共方法)

  • 在对聚合进行查询或操作时,整个聚合是作为一个整体;


public class VipEntity {
    /**
     * 主键id
     */
    private VipId vipId;

    /**
     * 用户id
     */
    private CustomerId customerId;

    /**
     *会员类型 
     */
    private VipType vipType;

    /**
     *会员有效期(开始时间,失效时间)
     */
    private VipPeriod vipPeriod;
}

领域事件

由特定领域,因触发一个动作而触发的发生在过去的行为事件

特点:

  • 动作(一个行为的发生产生的)

  • 已发生

工厂

负责聚合根、实体的创建,以及各层之间数据的转化(有些可能内聚到了实体中,但不能污染领域模型);如:

  • adapter 到领域层的数据转化

  • 基础设施层到领域的转换

建模

什么是建模?

建模分为数据建模和业务建模。业务关注的是我的流程怎么往下走;技术关注的是我该用什么样的技术手段去保证业务的完整性,以什么样的数据模型去承接业务;最终业务和技术虽然实现了功能,但业务与技术并没有在理念上对齐。

领域建模是为了解决上面的问题。

建模的本质是归类抽象。 目的:

  • 减轻认知的负担,最终将业务的心智模型提取出来,显性化,业务和技术达成一致;

  • 避免重复的思考与工作(内聚)

  • 提升人的效率(关注更高层次的内容,而不是陷入大泥球中)

架构

DDD分层架构

DDD分层架构中有很重要的依赖原则:每层只能与位于下方的层发生耦合,类似于网络的7层或TCP/IP的4层模型架构,每一层各司其职,并且只关心向下一层的实现,而不会出现各层耦合。

图片

洋葱架构

同心圆代表软件的不同部分,从里向外依次是领域模型,领域服务,应用服务和外层的基础设施和用户终端。洋葱架构根据依赖原则,定义了各层的依赖关系,越往里依赖程度越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的情况,

图片

六边形架构

将应用分为内六边形和外六边形两层,内六边形实现应用的核心业务逻辑。外六边形完成外部应用,基础资源等的交互和访问,对于与不同的外部系统交互,由外六边形的适配器负责协议转换,保证内六边形业务逻辑的干净。

图片

四种领域模型

  • 失血模型:领域对象只包含setter和getter方法,业务逻辑放到service

  • 贫血模型:包含属性的getter/stter和所有业务逻辑;

  • 充血模型:包含属性的getter/stter和所有业务逻辑,再加上与数据库的操作。这是DDD提倡的模型

  • 胀⾎模型,删除Service层,所有逻辑都放到模型中,模型直接对接web层

通过什么手段来落地?

  • 事件风暴

  • 用例+用户故事

我们是怎么做的?

  • 越底层越稳定,越上层越易变 (高内聚,低耦合)

  • domoin层捕捉业务规则,应用层捕捉应用逻辑;

  • adapter完成验签,解密,适配各端的到应用服务的逻辑(如注册发短信,h5,安卓,ios都不太一样)

  • 应用层将是应用逻辑的组装,包含了技术以及领域服务的调用,以及易变的逻辑;

  • 使用半贫血模型;

  • 聚合根里包getter和setter,包含部分公用的业务逻辑(主导查询)但不包含所有的业务逻辑;

  • 一个聚合根可能支撑多个查询,每个查询可能只包含聚合根的部分内容

  • 领域服务包含操作型的业务逻辑(主导操作)

  • domain包含领域模型、领域服务,用于捕捉领域逻辑,暴露出的服务用于操作领域模型

  • domain作为底层,依赖倒置基础设施层;

  • domain层通过gateway,做了一层防腐,gateway就是一组接口;

  • 复杂业务使用领域服务构建,简单业务直接应用层调用基础设施层;

图片

分享后的大家问的几个问题

  • 事务如何添加?

  • 如果整个领域服务是一个事务,则在application层进行加事务,在application里需要注意,不要破坏领域;

  • 如果只是其中领域服务的一个小方法,可以在gateway的实现里实现;

  • 性能问题如何解决?性能属于技术问题,和业务无关;

  • 比如常见的CQRS,将写和读分离,写异步

  • 先写redis再写数据库

  • 外部调用短连接变长链接;

  • 事实数据做宽表;

  • 事件驱动

  • 采用哪种架构(六边形、洋葱架构)

  • 最核心的整洁架构+DDD的分层架构,并没有特别严格;

  • 最核心的是业务逻辑和与模型;

  • 随着业务的发展与技术发展不断的适配;

  • context的作用以及放的位置?

  • context应该只作用域application层

  • context对应的是当前application的上下文

  • 在application里会根据不同的领域模型拆解到对应的聚合根上,或领域实体上;

  • 一个context可能对应多个接口,但是一定对应一一种场景

  • 聚合根、实体、值对象的关系(拆解每个人一个理解,还没想好怎么标准化操作)

  • 聚合根是根据业务走,用例图能很好的表达,我们操作的领域

  • 值对象,一组字段组合起来能标识出一块内容的,比如会有的有效期,几个字段组合起来就能体现会员是否有效,有效期脱离了会员又没有意义;

  • 聚合根不能引用聚合根

  • 实体或值对象能出现在多个聚合根里

参考:

https://blog.csdn.net/significantfrank/article/details/110934799

https://www.cnblogs.com/jiyukai/p/14830869.html

https://zhuanlan.zhihu.com/p/115685384

https://www.zhihu.com/question/25089273

yxkong

这个人很懒,什么都没留下