简介
如果我们不做工程,只是简单的写一个程序,我们都可以很熟练的使用面向对象,但就是因为工程的复杂性,导致我们没有办法随心所欲去用面向对象里的各种优秀设计。
理念
How To Implement Domain-Driven Design (DDD) in Golang Domain-Driven Design is a way of structuring and modeling the software after the Domain it belongs to. What this means is that a domain first has to be considered for the software that is written. The domain is the topic or problem that the software intends to work on. The software should be written to reflect the domain. The architecture in the code should also reflect on the domain.
迄今为止最完整的DDD实践一些套话
- 边界清晰的设计方法:通过领域划分,识别哪些需求应该在哪些领域,不断拉齐团队对需求的认知,分而治之,控制规模。
- 统一语言:团队在有边界的上下文中有意识地形成对事物进行统一的描述,形成统一的概念(模型)。
- 业务领域的知识沉淀:通过反复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使知识(模型)可以很好地传递和维护。
- 面向业务建模:领域模型与数据模型分离,业务复杂度和技术复杂度分离。
设计
从概念开始理解DDD
聚合根、领域对象、领域服务、领域事件、仓储、贫血充血模型、界限上下文、通用语言
从分层开始理解DDD
一文详谈领域驱动设计实践我们工作的代码库中最多类名后缀就是Service了,我们也应该被各种Service的调用层级、循环依赖教训过很多遍了,出现这种问题实际上还是我们对代码缺少设计,一股脑的把业务逻辑、系统逻辑、应用逻辑、基础设施等等随意组装到一起使用,本来每一个部分的复杂度就已经非常高了,我们还要将这些复杂度揉到一起。领域驱动设计给我们提供了一种分层治理的思路,将系统内的服务类分为几个大类:应用层服务、领域层服务、基础设施层服务。
- 应用层服务用于处理输入输出、与领域模型和领域服务之间的调度、连接基础设施层服务。
- 在将领域模型与工程结合的过程中,应用服务(ApplicationService)扮演了十分重要的角色,它对入口、领域模型、外部依赖、基础设施等部分进行编排和调度,最终使领域模型能够在实际应用中正常工作。
- 当领域模型中某个动作或者操作不能看作某个领域对象自身的职责时,可以将其托管到一个单独的服务类中,这种服务类,我们把它叫做领域服务。对于领域服务的使用,经常很难去定义哪些行为或逻辑是应该托管到服务类中还是由领域对象自己来负责。全部托管到领域服务中,领域对象则会变成贫血模型,如果不托管,又容易因职责过多而导致领域对象过于膨胀。对于这个问题我们也没有太好的解决办法,软件工程的问题永远都是在Balance的过程中,当代码复杂度可控的范围内,我们尽量减少对领域服务的使用,如果领域对象开始出现膨胀的现象,那就将其托管到领域服务中。对于领域服务,一定要把守住一条底线,领域服务一定不要有状态,也就是我们所说的“纯函数”,这样做能够让领域服务保持单纯,仅关注于领域对象之间的关系和其状态的变化,而不会引入领域逻辑以外的复杂性。
https://github.com/KendoCross/kendoDDD 概念太多了,换一个角度,从MVC/MVP/MVVM的分层架构入手,类比理解DDD经典的四层。然后融合自己已有的编码习惯和认知,按照各层的主要功能定位,可以写的代码范围约束,慢慢再结合理解DDD的概念完善代码编写。
分层,分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。
- 严格分层架构,某层只能与直接位于其下方的层发生耦合;自然是最理想化的,但这样肯定会导致大量繁琐的适配代码出现,故在严格与松散之间,追寻和把握恰达好处的均衡。
- 松散分层架构,则允许任意上方层与任意下方层发生耦合。
各层理解
- 用户接口层/Presentation。负责向用户显示信息和解释用户指令
- 出入口主要的功用逻辑也尽量的简单,主要承接不同“表现”形式采集到的指令/出入参等,并进行转发给应用层。和不同的Web/RPC框架有一定的耦合,不同的框架代码不全一样。
- 该层的核心价值在于多样化,而不在于功能有多强大,不涉及到具体的业务逻辑。
- 应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。只能做简单的数据转换 & 触发,转换成能处理的内部指令,交给 entity 执行
- 应用层要尽量简单,不包含业务规则,只为下一层中的领域对象协调任务,分配工作。应用层是很薄的一层,只作为计算机领域到业务领域的过渡层。
- 这一层直接消费领域层,并且开始记录一些系统型功能,比如运行日志、事件溯源。 这一层的也应该尽可能的业务无关,以公用代码逻辑为主。
- 通过直接持有领域层的聚合根,infra层等直接进行业务表达。并将不常变化的domain model,转换为可能经常变化的view model。
- 领域层。负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节由基础设施层提供,但反应业务情况的状态是由本层控制并使用的。 在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。
- 可以细拆分为聚合根、实体,领域服务等一大堆其他概念。
- 聚合根,负责整个聚合业务的所有功能就行了。比如项目中的fileAggregate,该类直接负责与平台系统管理员相关的所有操作业务,对内依赖调用领域服务、其他实体,或封装一些不对外的方法、函数等,完成所有所需的功能,由聚合根对外统一提供方法。聚合根和其附属模型间有个共生死的约定(附属不可独自苟存)
- 实体有唯一的标识,有生命周期且具有延续性。例如一个交易订单,从创建订单我们会给他一个订单编号并且是唯一的这就是实体唯一标识。同时订单实体会从创建,支付,发货等过程最终走到终态这就是实体的生命周期。订单实体在这个过程中属性发生了变化,但订单还是那个订单,不会因为属性的变化而变化,这就是实体的延续性。
- 实体的代码形态:我们要保证实体代码形态与业务形态的一致性。那么实体的代码应该也有属性和行为,也就是我们说的充血模型,但实际情况下我们使用的是贫血模型。贫血模型缺点是业务逻辑分散,更像数据库模型,充血模型能够反映业务,但过重依赖数据库操作,而且复杂场景下需要编排领域服务,会导致事务过长,影响性能。所以我们使用充血模型,但行为里面只涉及业务逻辑的内存操作。
- 值对象:在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象。值对象没有唯一标识,没有生命周期,不可修改,当值对象发生改变时只能替换(例如String的实现)。值对象是描述实体的特征,作为实体的属性,数据库里一般是一个字段。
- 聚合。The reason for an aggregate is that the business logic will be applied on the aggregate, instead of each Entity holding the logic. An aggregate does not allow direct access to underlying entities( all fields in the aggregate struct begins with lower case letters). aggregates should only have one entity act as a root entity, this means that the root entity ID is the unique identifier of aggregate.
- 领域层不依赖基础层的实现,Entity 永远不能依赖非entity 包下的逻辑,在充血模型下,实际上 repository 是一部分 entity 的。Repository接口在领域层定义好,由infra层依赖领域层实现这个接口。数据库操作都是基于聚合根操作,保证聚合根里面的实体强一致性。PS: 一个domain一个repository接口
- 可以细拆分为聚合根、实体,领域服务等一大堆其他概念。
- 基础设施层。为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制等。
- 这一层也是讲究和业务逻辑无关,只重点提供通用的功能。主要需要编码的部分是仓储功能,单纯的(增删改查)数据库持久化等功能,变更的概率不大。
- 根据领域模型设计数据模型,而不是反过来。 一般原则,领域模型中的实体映射为数据库中的表;领域模型中的属性,映射成表中的字段,同时还要根据需求补充更多的字段。模型中的一个一对多关联,可以映射成一个外键字段,以及一个外键约束。但一般不会真的建立外键约束,而外键的逻辑关系还是存在的。可以用虚线箭头表示这种逻辑上的外键关系,称为虚拟外键。对于多对多关联,必须增加一个关联表,其中包括了两个实体表各自的主键。另外,关联上的多重性决定了外键字段的非空约束。
极客时间《DDD实战课》
实体 | 值对象 | |
---|---|---|
业务形态 | 是多个属性、操作或行为的载体,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合 | 值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。 |
代码形态 | 实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。 | 值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。 |
运行形态 | 实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。 | 值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。 |
数据库形态 | 与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。在领域模型映射到数据模型时,一般是一对一,也有一对多的情况。 | 数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。 |
为什么要在限界上下文和实体之间增加聚合和聚合根这两个概念?实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。举个例子。社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同一致的工作,朝着一个最大的目标前进,发挥出更大的力量。领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。
聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,很可能会导致实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
- 它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
- 它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
- 在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
领域事件是解决子域之间耦合的重要手段,当一个领域事件发生时,它会触发一些操作,这些操作可能会更改系统的状态,也可能会导致其他领域事件的发生。
领域建模的体系化思维与6种方法论怎么判断模型好不好?当接到一个新的需求时,如果发现模型的关系不匹配了,那说明之前的模型设计的肯定存在问题,好的模型实体和实体之间的关系一定是稳定的,只会新增不会对原有关系进行改变。PS:变相支持了开闭原则。
领域模型为什么要一直持续迭代?
- 意识问题。在用户、产品人员、运营人员眼中,沟通的语音是”流程”而不是”模型”。开发人员在与他们的沟通过程中,慢慢就形成了以”流程”为主导,而不是以”模型”为主导的思维方式。这使得整个开发过程是”流程驱动”,而不是”领域驱动”。大家在讨论业务与系统解决方案的时候,大部分时间都花在了业务流程、业务规则上,而不是深刻挖掘流程背后的不变因素。
- 现实世界的复杂性。业务也就是我们的现实世界,灰度的、模棱两可的东西,比计算机的世界多得多,变化也多得多。很难确定有哪些东西是不怎么变的,什么东西是容易变的,而这恰恰是做建模的前提条件。
- 迭代速度。在稳固的模型,也不可能一成不变,毕竟现实世界一直在变。当现实世界变化到模型不能支撑的时候,要能马上修改模型才行。但实际情况是,因为开发效率的原因,工期赶不上,然后就会在旧的模型上进行打补丁,补丁一个接着一个打,最后整个系统臃肿不堪,开发效率进一步降低,如此恶性循环。
- 火候的掌握。领域模型是要对现实世界建模,既要去寻找不变性,又要为可能变化的地方留出扩展性。什么地方是不变的,要作为基础;什么地方是易变的,要留出扩展性,这其中并没有一个标准原则。另外,各家公司的业务规模、速度不一样,团队实施能力也不一样。所以在实践中,要么会”缺乏设计”,要么会”过度设计”。对火候的掌握,需要有悟性。只有反复思考,反复推翻自己之前的想法,再重建新的想法,才能在实践中不断找到领域模型、业务发展速度、技术团队能力之间的”最佳平衡点”。
CQRS
命令查询职责分离
- 命令(Command):不返回任何结果(void),但会改变对象的状态。实践时还是让命令返回了一些主键之类的。
- 命令抽象出来之后,可以串一下表现层/应用层对该命令的消费。
- 表现层:将表现层接受来的请求主体 转换为Command ,并且进行参数校验,触发Command执行。
- 应用层:命令的Handler,一般都有相关领域上下文的聚合根来承担。基本逻辑 ``` commandHanlder.Func{
- 实例化领域实体 // 查询我们需要处理的实体数据,然后创建对应的领域对象,构造我们所定义的聚合。
- 调用实体行为 // 调用行为会修改实体的属性是内存上的
- 保存实体变更 // 把变更保存到基础设施层,例如 MySQL,PG等 } ```
- 每一个确定的命令操作,不论成功还是失败,只要执行之后就产生相应的事件(Event)。
- 事件的Handler,某个命令处理完毕之后,也即某个事件发生了,有可能需要短信通知、邮件通知等等。事件订阅者继续后续的逻辑。
- 命令抽象出来之后,可以串一下表现层/应用层对该命令的消费。
- 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。查询可以从应用层进行分离,直接操作infra层获取业务不是特别复杂的查询,这与没有引入CQRS的代码可以保持一致。PS: domain只是服务command,且domain 的持久化相对简单,一般都是根据id 进行crud。
ddd 缺点
为什么领域驱动实践了20年,效果并不好在领域驱动理论中,业务学习和领域建模的优先级最高是理所当然。
可惜,这两个阶段书中没有提供什么可供实践的操作方法论。
- 学习多少对领域建模是足够的?
- 学习哪些内容?什么学习方法是无效的?
- 领域模型一定存在么?
- 如果存在,如何构建?
- 如果构建完成,他具备什么特征?
这些都是问题。不解决根本问题,其他都是舍本追末。cqrs,eda,战略设计,战术设计……都是缘木求鱼。不单是空谈——这些特定场景的特定解决方案,在没有明确领域适用性的情况下——反而平台引入了不必要的技术复杂度。这不是领域驱动,这反而是违反领域驱动精神的。扛着红旗反红旗。
解决方案
- 如何快速学习一个陌生的领域,给出它的模型?咨询公司很多咨询师,从了解一个行业,都侃侃而谈把行业专家侃晕到五体投地,往往不过几个月。——这并不仅仅是因为他们掌握了某种话术。而是咨询公司形成了一套快速归纳一个行业业务模型的高效方法论。以流程为中心,关注客户,关注价值链。所以,和普通人学习业务更关注业务细节不同。他们学习业务,更关注业务流程背后的利益相关方,市场格局——等本质规律。然后,站在这样的高度,抓住了根本,反而可以忽略一切细节繁琐,直透本质。进行“流程再造”,提升业务效率,提升客户价值,提升整个业务流程的合理性,灵活性,通用性,标准型……对于软件架构师而言,掌握这套方法。就意味如同咨询师一样,并不需要学习漫无边际业务知识、和细节。而仅仅通过梳理一两个业务流程。就获得整体业务关键的、不变的相关方;通过3,4个业务流程,归纳出行业一般的,通用的步骤和能力……根据本质和一般再组合出任意需要的业务。
- 如何确保模型对领域是完备的?同归本质和归纳得出的模型。不是完备的。通过事件风暴,能够确保模型对当前和未来短期的需求是完备的。但是不能确保任意完备。所以DDD理论最后章节是柔性设计/重构。DDD方法论实际让认为“绝对的领域模型并不存在”而“只存在相对的领域模型”。
- 领域模型应该是什么样子的?架构的最大目标是控制复杂度。而在架构设计中存在两种不同的复杂度对象——必然复杂度和偶然复杂度。必然复杂度是这个问题领域存在客观规律。其复杂度是最小的,不可减少的。偶然复杂度是软件工程在解决这个问题中引入的额外复杂度,可能源自设计的不足,或者技术手段的不合理,组织结构的不合理,工程开发方法不合理…………导致付出的额外复杂度。偶然复杂度是会随着我们解决问题方法的不同而或有增减,但不可能为零。无论任何设计方案,任何技术手段,任何工程方法,都只会增加。而不会降低,可能是设计的不足,或者过度,或者设计本身就不符合问题的客观规律。技术选型错误,产生了额外的工程复杂度和学习复杂度,或者稳定性,性能等方面产生了太多问题……
- 让额外复杂度最低的方法就是在核心的设计阶段根本不考虑工程层面的问题。不考虑就不会有复杂度。偶然复杂度就为0。然后,再根据领域模型,结合业务场景(事件风暴),取舍出最必要的业务扩展,技术需求……这便是工程上理论上能达到的最小的复杂度。所以,在设计阶段,不必考虑任何技术实现层面的问题,分工协作层面的问题,工程落地方法上的问题。而仅仅是基于问题和需求本身,做分析和权衡。否则便是错误,让各种复杂度交织在一起,互相影响,一定是最差的决策。