对领域驱动设计DDD理解

写在前面:9月份研究生开学,实习也该告一段落了。

也许最好的学习就是将已会的知识再系统化吧。

开发不应关注点一直都是在单个功能的实现上,也要考虑整个系统的可维护性和可拓展性。话虽如此,但是每次写代码后我自己也没眼去看。


文章目录

  • DP - Domain Primitive
  • Entity
  • Repository
  • Domain Service
  • ACL - 防腐层
  • 面接抽象接口编程
  • 单元测试
  • UL - 统一语言
  • 模型价值
  • 聚合根
  • BC - 限界上下文
  • 实战经验


DP - Domain Primitive

传统POJO只存在getter、setter方法,但是DP却包含了更多的逻辑结构,可能有初始化、校验、属性处理等,也就是说DP不仅拥有属性,也拥有属性相关的职责。但是边界如何界定、强度如何把握、内聚的边界就需要有很丰富的经验了。
DP的三原则:让隐性的概念显性化、让隐性的上下文显性化、封装多对象行为。1、让隐性的概念显性化,赋予属性行为(如get操作)显性化隐藏属性。2、让隐性的上下文显性化,隐性的上下文作为类属性表示。3、封装多对象行为,一个DP可以封装多个对象行为。

重构例子有DP代替DTO具体实现

Entity

实体类描述在这个系统实体应该包含的信息,尽量多与DP组合将更多的校验和隐性属性内聚起来,又常见组合Repository层,也就是说定义完了Entity终究是要在数据库Repository层落地实现。Repository是数据访问的抽象层。

Entity和DP本质差异是在语义上是否拥有数据状态。比如User是Entity,是有状态的。DP是组成Entity的基础类型。

什么叫状态?总结是“该对象是否存在生命周期”,“程序是否需要追踪该对象的变化事件”,Eric Evans举的例子是体育馆场里的座位,观众买了票,每张票上对应着座位号,这种场景下座位就是有状态的,是Entity,我们的程序需要关注该对象的变化事件,如关注座位的预订状态,价格,位置等属性,另一种场景,观众买了票就可以随便坐,这时候座位就是无状态的,就是DP,只需要关注座位的总数即可。

在我理解上Entity和DP可以看作是其他架构中说的Model层,也就是模型层,我们对领域建模的模,也就是Entity和DP。

Repository

在抽象层,我们只定义动作,如save、find等,再实现Impl实现类。在Impl实现类中是什么ORM看技术选型。

Domain Service

设置Service层主要用于封装多Entity或者跨业务域的逻辑。根据业务描述编排Entity和Service

ACL - 防腐层

防止外部系统的腐烂影响我们自己的系统。

主要进行的是两个系统之间的model(模型)或者协议的转换,ACL尽量把系统提供者的模型转换为系统使用者的模型(而不引入中间第三者模型)

简单的例子就是其他系统的RPC服务返回结果封装为自己的DP

面接抽象接口编程

减少外部耦合,一切不属于当前领域的设施和服务都可以被认为是外部依赖,比如数据库、schema、RPC服务、ORM、中间件等,有一个特征是这些依赖是可以被替换的,比如Mysql替换成postgresql,schema的改动表中添加几个字段。外部依赖产生变动,也要使得自己系统产生变动控制在最小。

抽象接口本质是一种中间协议,依赖方和被依赖方只要对该协议负责,接口将软件分层隔离,在这种隔离下,任何一层的变动都将被控制在当前范围内。

举个例子,如将具体营运商的RPC Service服务,我们可以创建抽象RPC服务类implements具体营运商的RPC Service服务。实现对象通过依赖倒置注入。

单元测试

单元测试是开发中容易忽略的点,高耦合测试用例是乘法级,分层解耦后测试是加法级数量。一定要保证覆盖率。
常见的技术选型是Mockito。Ctrl+shift+T,创建新的测试类,类加上@SpringBootTest注解。使用依赖注入我们需要的Service,或者使用@MockBean注入(@BeforeEach方法中使用Mockito.when返回对象),实现方法里配合Assert.断言。

单元测试比较重要的点就是一定要写预期和结果,还有覆盖率

UL - 统一语言

术语统一,特别是在强业务相关中的代码里,各种变量什么意思,会不会产生歧义,这些问题都需要定义和统一,在半路接手项目的时候,特别是强业务相关的,看项目的时候先看外部接口,不要一上来就深入到代码实现里面去,这部分是业务相关,需要找专家咨询,或者吃饭的时候约前辈聊聊,不断对业务领域知识进行消化。

模型价值

业务逐渐复杂之时,需要在关键节点进行重构和改革,引入良好的模型,否则技术债会抑制业务的成长,此时模型价值变高。业务处于不同时期时对建模的要求也不一样。

当然了如果仅仅只是开发简单的一个或者几个页面,那么无需过多投入,此时模型价值低,使用下水道(脏乱差)写法也无可厚非。

为了建设有价值的模型,我需要在UL基础上,消化知识,并向模型中提炼知识。

我的理解这里模型可以看作是有状态的Entity和无状态DP了,也是需多设计架构(如php的TP5)总称的Model层。

聚合根

在一个复杂的系统中,对一个对象进行修改,可能会涉及到大量的其他关联对象的状态,对象中的关系有点像数据结构中的图,用聚合Aggregrate描述存在引用关系的对象集合。

聚合是对存在引用关系的一组对象的封装,目的是屏蔽掉内部对象之间的复杂的关联关系,只对外暴露统一接口(根对象),关于聚合,我们需要关注它的两个属性,根对象和边界。

根对象是Entity,需要ID和状态区分其他聚合,聚合内部通过执行一系列逻辑来保持各个对象状态的一致。

标识根对象推荐使用实现作为标识父类接口的DP,不推荐使用String类型。

BC - 限界上下文

限界上下文就是用来细分领域,从而定义通用语言所在的边界。

本质是为了解决复杂系统的领域分治问题,领域分治最清晰的方式就是通过拆分成微服务,但拆分服务本身也存在边界问题,而且也会增加通信的复杂度。DDD中通过BC思想解决领域分治问题

1、领域隔离,如支付和退款服务,应划分开来
2、模型隔离,一个领域可能多长时间的迭代下催生出多套模型,甚至共用一部分逻辑和代码,但目标是总老模型彻底迁移到新模型,那么这些模型应使用BC尽可能的隔离起来,模型之间交叉的地方通过实现转换器来适配,这样在一套模型中修改代码是相对安全的。

同一领域下模型隔离方式:
1、在不同包路径
2、在不同module
3、在不同容器上下文

实战经验

初步抽象需求,从业务上抽象几个领域,明确每个领域的职责,并且将领域进行子域划分和依赖关系梳理

对领域划分,在系统开发中要始终提醒自己当前做的工作对系统的改造是当前领域所关注的,这样可以避免各个领域之间职责混乱不堪的局面。

每个子域之间存在通信需求,依赖限界上下文实现,限界上下文存在哪些关系,这些关系被称为context map。与外部领域进行通信应该通过防腐层

一个领域中存在很多术语,团队内外根据业务发展需要所创造出来的术语需要统一,一旦出现歧义将会导致沟通困难。

针对每个子域,设计模块结构组织(按公司的来,DDD没有明确方式组织代码模块),设计建模,有DP,Entity,聚合。聚合和聚合根是难点,需要掌握。

编码上,灵活运用设计模式很重要,要充分考虑以后拓展情况的出现。

你可能感兴趣的:(java)