当前位置: 首页 > 科技观察

DDDTactics:DomainModel

时间:2023-03-13 00:50:10 科技观察

Domain-DrivenDesignDDD提供了一个关于战术建模(以下简称建模,除非另有说明)的元模型系统(如下图),通过它我们将策略对建模过程中识别出的问题子域进行抽象,抽象用于指导***的实现。(DDD构建的元模型元素脑图)我们这里说的战术阶段其实就是这么一个抽象的过程。由于元模型的存在,这个抽象过程实际上在一定程度上被建模了。这样做的好处是不仅技术人员可以参与建模,业务人员经过一定的培训也是完全可以理解的。在带领多个团队练习建模的过程中,我也要求业务人员参与战术设计。由于介绍DDD元模型的书籍已经很多,这里不再赘述,而是说说大家在这个抽象过程中经常遇到的一些困惑。这些比较普遍的问题可能需要在未来DDD元模型的演进中解决,但是我们还是要注意业务问题和架构设计的多样性,不要过度规范,以免为时已晚。业务对象的抽象通过对业务问题的子域划分,我们发现了一些关键的业务对象。开始抽象之前的必要步骤是“讲故事”!讲什么故事?关于此子域解决的业务问题或提供的业务能力的故事。既然是故事,就必须有明确的业务场景和业务对象之间的交互。这件事看起来是那么的自然和简单,但是在一个团队中,能够站出来汇报清楚的人却不多。看到这里的读者不妨停下来试一试,你能不能在两三分钟内用一个场景描述一下你在做的生意?这样做的明显目的是让我们更全面地思考我们想要提炼和抽象的东西。业务对象是什么。只有当我们能够把业务场景“说”清楚的时候,我们才应该开始抽象这一步。对于一个业务对象,我们常见的抽象可以是“实体”(Entity)和“值对象”(ValueObject)。这两个抽象方法在定义上的区别是实体需要被赋予唯一的标识,而值对象则不需要(可以通过属性集来标识)。当然,另一个经常被引用的区别是实体应该有一个连续的生命周期。例如,如果我们将一个订单抽象成一个订单跟踪字段中的实体,那么每个订单都应该有一个唯一的标识号,订单也应该有一个从订单创建到完成发货的生命周期。很显然,如果不加其他约束,值对象的抽象是没有意义的,直接用实体不行吗?但是如果我们考虑到实体的管理成本,比如在生命周期中需要保证实体状态的一致性,那么我们就会发现值对象变得非常简单可爱。当一个对象在我们的(抽象)世界中无法改变时,一切都会变得简单。对象创建后,只能被引用。当没有引用时,我们可以交给垃圾回收自动处理。随着高并发和分布式系统的流行,其实我们对业务对象进行抽象的第一步就是能不能使用值对象。如果你实现的技术架构采用函数式范式语言(类似于Closure),那么首先考虑值对象抽象可能是一种建模原则。在对象抽象初步完成后,我们必须重复前面的故事来检查我们的建模。在经历了这个抽象过程之后,参与讨论的每个人应该会发现自己更清楚业务的需求和需要提供的能力。聚合封装DDD元模型中的一个核心概念称为“聚合”。这个来自建筑的术语非常形象。在建筑学中,我们把它翻译成“骨料”,它是混凝土形成的重要元素,也是混凝土之所以如此坚固的基础。(Anaggregateinconcrete)同样,在DDD建模中,聚合也是我们构建领域模型的基础,每一个聚合都是一个高内聚的组合。聚合本身就完成了对我们核心业务规则的封装,减少了我们实现出错的可能性。以上面的订单跟踪字段为例,假设我们允许一个订单下有多个子订单,每个子订单也可以独立发货。在这种情况下,我们抽象出“子订单”这个实体。显然,订单和子订单之间的业务逻辑是一致的。没有订单时,不应创建子订单。更新子订单时,应同时“通知”其所属的订单。这时候就需要使用聚合订单和子订单的包。采用聚合抽象的结果是访问每个子订单需要从相关的订单条目访问(即订单是聚合根)。在访问时,我们以这个聚合为基本单位,包括订单和订单下的所有子订单。显然,这样做的好处是在订单跟踪的领域模型中,订单是作为一个聚合存在的。我们只需要一次性梳理出订单和子订单之间的逻辑关系,以后就不用再去考虑每个引用中的业务规则了。.(OrderAggregationintheOrderTrackingDomain)在建模过程中,很多团队并没有努力去思考聚合的存在。封装,作为技术实现领域的一个基本原则,在建模时却很少被关注。开篇提到在战术建模过程中强调业务领域人员的参与,也是为了解决这个问题。聚合的标识其实就是对业务规则的封装。在不了解业务规则的情况下,我们无法做出是否封装的判断。总之,识别聚合是识别潜在核心业务规则的过程,定义聚合是基于大家的共识对核心业务规则的封装。领域服务的定义在最初的元模型定义中,领域服务让很多人感到困惑。一个经典的例子就是账户管理领域对“转账”业务行为的抽象。由于转账本身至少作用于两个账户,因此将转账视为一个账户显然是不合适的。那么,如果我们将转移名义化为一个实体呢?感觉很别扭,毕竟转账是附在账户上的。这个时候DDD提出了元模型中服务(Service)的抽象,把传输抽象成一个服务,感觉就顺畅多了。同理,在我们上面的订单跟踪字段中,如果在跟踪过程中需要短信通知,更好的建模方式是抽象出一个“通知”服务来完成。我经常使用静态方法来帮助技术人员理解服务的抽象(尽管服务不一定是用静态方法实现的)。服务本身就像一个静态方法,有一定的逻辑但不持有任何信息。从整个领域来看,同一个服务没有不同的“版本”。经常困扰大家的一个问题就是Service这个词的局限性。在一些分层架构设计中,会出现领域服务(DomainService)和应用服务(ApplicaitonService)。大多数时候,应用服务处于领域服务的上层,直接对外提供接口。如果有这样的分层,那么领域服务就不应该直接对外,而应该通过应用来服务。比如之前的订单消息通知是领域服务,当订单状态发生变化时创建一条通知消息,最新的通知以短信的形式发送给设定的人群,所以应该有对应的应用服务包含具体的业务场景处理逻辑。后面可能还会有邮件通知的应用服务,同样调用这个通知域服务,但是通过邮件通道完成最终的业务场景。由于微服务架构的流行,各个子域的粒度已经相当细了,很多时候并没有域服务和应用服务这样的区分。当然,从简单的角度来看,这是一件好事。在整个建模过程中,服务的抽象往往是最不确定的,也是最值得反复考虑的。Repositories的使用Repositories是一个容易被误解的抽象概念,很多人会直接将其与具体的数据存储联系起来。在我刚开始采用DDD建模的时候,我经常刻意避免这种抽象,以免让大家感到困惑。这个抽象的概念其实可以追溯到MartinFowler的ObjectQuery模式。另一个相关的概念是DAO(DataAccessObject),用来简化要存储的数据与对应的业务对象之间的映射关系。不同之处在于Repositories用于粗粒度的抽象。在DDD方法中,我们可以认为映射对象就是我们的聚合。也可以在实现时为每个实体创建对应的DAO(比如使用Hibernate等ORM框架),但显然这不是我们在建模过程中需要关注的。那么为什么需要Repositories的抽象呢?让我们回到订单跟踪的例子。通知订单状态变化的服务在发出通知前需要定位到订单的信息(可能包括订单的相关涉众和子订单的信息)。).通知作为一项服务,不应包含特定的订单信息。这个时候我们就需要利用Repositories的抽象来构建一个订单的聚合查询,也就是有一个订单repo,具体的查询逻辑应该在这个repo里面。当需要存储和查询值对象时,这种抽象也是必要的。假设我们分析订单查询的字段。在这个字段中,订单记录显然是不允许修改的。自然的抽象方法就是值对象。同时,查询服务持有特定的查询逻辑(如按时间或按用户)是合理的。外部应用直接调用查询服务(接口)并提供指定的参数,所以我们需要一个订单记录的repo来存放与存储相关的查询逻辑。当然,这并不是说一个query就一定要有一个repo对应。如果查询的逻辑很简单,直接让服务实现数据存储或许是可以的。请记住,我们抽象的目标是使建模更容易,并且抽象过程应该是灵活的。限界上下文的含义经过10多年的演进,我们对如何支持组织的规模已经达成了一些基本共识。我们知道微服务架构(Microservices)可以帮助我们组织成百上千的工程师,小团队的自组织是至关重要的。我们也逐渐在技术和业务团队之间如何清晰沟通“架构”这个难题上找到了DDD。那么DDD和微服务架构有什么关系呢?很多人会提到限界上下文(BoundedContext)。我曾经写过一篇关于这个主题的文章(DDD&Microservices)。限界上下文封装了一个相对独立的子域的域模型和服务。有界上下文映射描述了各个子域之间的集成调用关系。从某种意义上说,这个定义与我们的微服务部门不谋而合:一个自治的、独立的部署单元,面向提供业务能力。所以虽然我们不能绝对的按照限界上下文来划分服务,但是限界上下文或者DDD,绝对是我们设计微服务架构的重要方法之一。如果我们回到DDD的战略设计上,就会发现在问题域中,DDD已经通过子域的划分对业务能力进行了分解,而限界上下文在解决方案域中完成了进一步的分解。当然,我们不能完全认为子问题域和限界上下文之间存在严格的一对一关系,但在大多数情况下,一个子问题域会被设计为一个或多个限界上下文。子域和限界上下文在某种意义上相互印证,重点在于区分问题域和解决方案域。这是实施DDD最难的部分,也是判断一个架构师能力进步的分水岭。战术建模小结DDD的建模要素比较简单,本文描述的元模型应该可以满足大部分场景下的建模。毛主席曾有一句名言,“战略上要蔑视敌人,战术上要注意敌人”。在架构设计上,我们没有敌人,业务需求是我们的朋友。因此,在领域驱动架构设计方面,我们需要的是“战略上重友,战术上简化建模”。希望这句话能帮助正在实践DDD的团队重新思考他们对战略问题领域的投入和重视,不要挥舞战术模型的大锤去寻找实际上不存在的钉子。【本文为专栏作者“ThoughtWorks”原创稿件,微信公众号:Thinkworker,转载请联系原作者】点此查看该作者更多好文