[译文]Domain Driven Design Reference(三)—— 模型驱动设计的构建模块
本书是Eric Evans对他自己写的《领域驱动设计-软件核心复杂性应对之道》的一本字典式的参考书,可用于快速查找《领域驱动设计》中的诸多概念及其简明解释。
其它本系列其它文章地址:
[译文]Domain Driven Design Reference(一)—— 前言
[译文]Domain Driven Design Reference(二)—— 让模型起作用
[译文]Domain Driven Design Reference(三)—— 模型驱动设计的构建模块
Ⅱ.模型驱动设计的构建模块
这些模式根据领域驱动设计,广泛地推行了面向对象设计的最佳实践。他们指导决策来提炼模型,并使模型和实现保持一致,每一个都增强了其他的有效性。仔细制定模型元素的细节为开发人员提供了一个稳定的平台,从中可以探索模型并使其与实现保持紧密联系。
分层架构
在面向对象的程序中,用户界面,数据库和其他支持代码通常会直接写入业务对象。额外的业务逻辑被嵌入在UI部件和数据库脚本的行为中。发生这种情况是因为在短期内,这样做是最简单的方法。
当与领域相关的代码通过如此大量的其他代码被扩散时,变得非常难以理解和推理。UI的表面变化实际上可以改变业务逻辑。要更改业务规则,可能需要仔细跟踪UI代码,数据库代码或其他程序元素。实现一致的、模型驱动的对象变得不切实际。自动化测试变得难以进行。由于每个活动都涉及到所有的技术和逻辑,程序必须保持非常简单,否则就无法理解。
因此:
隔离领域模型和业务逻辑的表达形式,并消除对基础架构,用户界面甚至非业务逻辑的应用程序逻辑的依赖。将一个复杂的程序分成多个层。在每个层次内开发一个内聚的设计,并且仅依赖于下面的层。遵循标准的建筑模式,为上面的分层提供松散的耦合。将所有与领域模型相关的代码集中在一个层中,并将其与用户界面,应用程序和基础设施的代码隔离。领域对象没有显示自己,存储自己,管理应用程序任务等等的职责,可以集中在表达领域模型上。这使得一个模型能够发展到足够丰富,足够清晰,能够捕获必要的业务知识并将其付诸实践。
这里的关键目标是隔离。 诸如“六边形架构”之类的相关模式可以起到允许我们的领域模型表现避免依赖和引用其他系统问题,甚至更好的效果。
实体
许多对象代表了一个连续的具有身份标识的主线,贯穿整个生命周期,尽管其属性可能会改变。一些对象不是主要由它们的属性定义的。它们代表了贯穿时间并经常跨越不同展现形式的主线的身份标识。有时这样的对象必须与另一个对象匹配,即使属性不同。错误的身份可能导致数据损坏。
因此:
当一个对象被它的身份而不是它的属性所区分时,把它作为它在模型中定义的要点。保持简单的类定义,并关注生命周期的连续性和身份标识。
定义一个区分每个对象的方法,而不管它的形式或历史。 对通过属性调用匹配对象的需求保持警惕。定义一个保证为每个对象产生唯一结果的操作,可能通过附加一个保证唯一的符号。这种标识手段可能来自外部,也可能是由系统创建的任意标识符,但必须符合模型中的身份标识区别。
模型必须定义什么是同样的事情。
(又称参考对象)
值对象
有些对象描述或计算事物的一些特征。
许多对象没有概念上的身份标识。
跟踪实体的身份标识至关重要。但将身份标识附加到其他对象可能会伤害系统性能,增加分析工作,并使所有对象看起来都一模一样。软件设计是一个复杂的持续战斗。我们必须作出区分,以便只有在必要时才进行特殊处理。
然而,如果我们把这种类型的对象看作是缺少身份的话,那么我们并没有在我们的工具箱或词汇中添加太多东西。实际上,这些对象具有自己的特点,对模型本身也有意义。 这些是描述事物的对象。
因此:
当您只关心模型元素的属性和逻辑时,将其归类为值对象。使其表达它传达的属性的含义并赋予它相关的功能。将值对象视为不可变的。使所有操作是不依赖任何可变状态的无副作用函数。不要给值对象任何身份标识,并避免保留实体所必需的设计复杂性。
领域事件
领域专家关心的事情发生了。一个实体负责跟踪其状态和规定其生命周期的规则。但是,如果你需要知道状态变化的实际原因,这通常是不明确的,并且可能很难解释系统如何实现它。审计线索可以允许跟踪,但通常不适合用于程序本身的逻辑。实体的变化历史可以允许访问先前的状态,但忽略这些变化的含义,以便对信息的任何操作都是程序性的,并且经常被抛出领域层。
分布式系统中出现了一系列独特但又相关的问题。分布式系统的状态在任何时候都不能保持完全一致。我们始终保持聚合内部一致,而异步的进行其他更改。当更改在网络的节点间传播时,可能很难解决无序或来自不同来源的多个更新。
因此:
将关于领域中活动的模型信息视为一系列离散事件。将每个事件表示为一个领域对象。这些不同于系统事件,它们反映了软件本身的活动,虽然通常系统事件与领域事件相关联或者作为领域事件的响应的一部分,或者作为将领域事件的信息携带到系统中的一种方式。
领域事件是领域模型的一个完整的部分,是领域中发生的事情的表示形式。忽略不相关的领域活动,同时明确领域专家想要跟踪或者被通知的事件,或者与其他模型对象中的状态改变相关联的事件。
在分布式系统中,实体的状态可以从特定节点的当前已知的领域事件中推断出来,从而在没有关于整个系统的完整信息的情况下得到相关的模型。
领域事件通常是不可变的,因为它们是过去的某种事物的记录。除了对事件的描述之外,领域事件通常包含事件发生时间的时间戳以及事件涉及的实体的身份标识。此外,领域事件通常具有单独的时间戳,指示事件何时进入系统以及使其进入系统的人的身份标识。如果有用,领域事件的身份标识可以基于这些属性的一些集合。所以,例如,如果同一个事件的两个实例到达一个节点,则它们可以被识别为相同的。
服务
有时候,这不是一回事。领域的一些概念由模型作为对象是不自然的。强制所需的领域功能成为实体或者值对象的职责,要么篡改基于模型的对象的定义,要么添加无意义的虚拟对象。
因此:
当领域中的重要流程或转换不是实体或值对象的自然职责时,添加一个操作到模型中作为一个单独的接口同时声明为一个服务。定义一个服务契约,一组关于与服务交互的声明。用一个特定限界上下文的通用语言来陈述这些声明。给服务一个名字,这也成为通用语言的一部分。
模块
每个人都使用模块,但很少将它们视为模型的完整部分。代码被分解成各种类别,从技术架构的各个方面到开发人员的工作任务。即使是做了很多重构的开发人员也倾向于使用项目早期构思的模块。
耦合和凝聚力的解释倾向于使它们听起来像是技术指标,根据关联和相互作用的分布进行机械的判断。然而,这不仅仅是将代码划分为模块,还包括概念。一个人一次可以思考多少事情是有限的(因此耦合度低),不连贯的想法片段很难被理解为一个无差别的想法(因此具有很高的内聚性)。
因此:
选择能够讲述系统故事的模块,并包含一系列内聚的概念。让模块名称成为通用语言的一部分。模块是模型的一部分,它们的名称应反映对领域的洞察。
这通常会导致模块之间的低耦合,但是如果它不寻找一种方法来改变模型来分解概念,或者是一个被忽视的概念,它可能是一个能够以有意义的方式将元素组合在一起的模块的基础。在可以被独立地理解和推理的概念上寻求低耦合。根据高层领域概念对模型进行细化直到它被划分,并将相应的代码解耦。
聚合
要保证复杂关联模型中对象变化的一致性是很困难的。他们能够被是概念上的构成部分的其它对象的变化所掩盖。在多个服务器之间分发对象或设计异步事务时会出现类似的问题。
因此:
将实体和值对象集中到聚合中并在周围定义边界。选择一个实体作为每个聚合的根,并允许外部对象仅保留对根的引用(对内部成员的引用仅在一个操作中返回出去才能使用)。定义聚合的属性和不变量作为一个整体,并将这个约束的责任赋予根【这里指的是聚合根】或某种指定的框架机制。
使用相同的聚合边界来管理事务和分配。
在一个聚合边界内,同步地应用一致性规则。 跨越边界,异步地处理更新。
在一台服务器上共同维护一个聚合。允许不同的聚合在节点间分配。
如果这些设计决策没有受到聚合边界的良好指导,请重新考虑模型。是领域的场景正在暗示着一个重要的新见解吗?这种改变通常会提高模型的表现力和灵活性,并解决事务和分配问题。
仓储
查询通用语言表达的聚合。
可遍历的关联的扩散只用于找到弄乱模型的东西。在成熟模型中,查询经常表达领域概念。然而查询可能会导致问题。
应用大多数数据库访问基础架构的纯粹技术复杂性迅速吞噬了客户端代码,导致开发人员陷入了领域层,使得模型无关紧要。
查询框架可能会封装大部分的技术复杂性,使开发人员能够以更自动化或声明的方式从数据库中提取所需的确切数据,但这只能解决一部分问题。
不受约束的查询可能会从对象中拉出特定的字段,违反封装,或从聚合内部实例化几个特定的对象,让聚合根变得充满变数并使这些对象无法执行领域模型的规则。领域逻辑移入查询和应用程序层代码,实体和值对象变成仅仅为数据容器。
因此:
对于需要全局访问的每种聚合类型,创建一个服务,它可以提供所有聚合根类型的对象的在一个内存集合中的错觉。通过一个大家都知道的全局接口设置访问。提供添加和删除对象的方法,这将封装实际数据往数据存储中的插入或删除。提供基于对领域专家有意义的标准来选择对象的方法。返回完全实例化的对象或属性值符合条件的对象集合,从而封装实际的存储和查询技术,或者返回给予以惰性的方式完全实例化的聚合的幻觉的代理。仅为实际需要直接访问的聚合根提供仓储。保持应用程序逻辑专注于模型,委托所有的对象存储和访问给仓储。
工厂
当创建一个完整的,内部一致的聚合或者一个大值对象变得复杂或者显示太多的内部结构时,工厂提供封装。一个对象的创建本身可以是一个主要的操作,但是复杂的组装操作不适合由创建的对象来承担。将这些职责结合起来可能会产生难以理解并且难看的设计。让客户端直接组装会混乱客户端的设计,破坏组装对象或集合的封装,并且过度地将客户端耦合到所创建对象的实现中【举个例子,这里的客户端可以理解成应用层或者UI层】。
因此:
将创建复杂对象和聚合实例的责任转移到单独的对象上,这个对象本身可能在域模型中没有职责,但仍然是领域设计的一部分。提供一个封装所有复杂程序集的接口,并且不要求客户端引用实例化对象的具体类。将创建一个完整的聚合作为一部分,强制实施它的不变性。创建一个复杂的值对象,可能是在将元素与构建器组合后。
作者:Zachary
出处:https://zacharyfan.com/archives/290.html
▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。
如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。