领域驱动设计(Domain-Driven Design DDD)——模型驱动设计的构造块2

书接上回
领域驱动设计(Domain-Driven Design DDD)——模型驱动设计的构造块1-CSDN博客

四、领域对象的生命周期

        每个对象都有生命周期,管理这些对象面临诸多挑战,主要的挑战有以下两类。

  •         在整个生命周期中维护完整性
  •         防止模型陷入管理生命周期复杂性造成的困境中

        我们将用Aggregate、Factory和Repository三种模式来管理对象的全生命周期。

        Aggregate(聚合)来定义对象的所属关系和边界,避免错综复杂的对象关系网来实现模型的内聚。
        Factory(工厂)来创建和重建复杂对象和Aggregate,从而封装它们的内部结构。
        Repository(存储库)来固化、持久化对象,并提供查找、检索和归档等功能。

        使用Aggregate进行建模,并且在设计中结合使用Factory和Repository,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。Aggregate可以划分出一个范围(包含一个或一组模型元素),这个范围内的模型元素在生命周期各个阶段都应该去维护其固定规则。Factory和Repository在Aggregate基础上进行操作,将特定生命周期转换的复杂性封闭起来。

4.1 模式:Aggregate

        在大多数业务领域中的对象都具有十分复杂的联系,最终会形成很长、很深的对象引用路径,我们不得不在这个路径上追踪对象。某种程度上,这是现实,因为现实世界中就少有清晰的边界。但软件设计上却需要有清晰的定义。

        在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。但是,过于谨慎的锁定机制又会导致多个用户之间毫无意义地相互干扰,从而使系统不可用。

        要想找到一种兼顾各种问题的解决方案,要求我们对领域有深刻的理解。例如,要了解特定类实例间的更改频率这样的深层次因素。需要找到一个使对象间冲突较少而固定规则联系更紧密的模型。

        为了实现上述要求,首先我们要引入一个抽象的封装模型,来封装相互间影响较深、较重要的关联。Aggregate就是一组相关对象的集合,我们把它作为数据修改的单元。每个Aggregate都有一个根(root)和一个边界(Boundary)。边界定义了Aggregate内部有什么。根则是Aggregate所包含的一个特定Entity。对Aggregate而言,外部对象只可以引用根,而边界内部的对象间则可以互相引用。除根以外的其他Entity都有本地标识,但这些标识只在Aggregate内部才需要加以区别,因为外部除了根Entity外看不到其他对象。

        在Aggregate成员之间的内部关系,需有一些固定规则(Invariant)以便在数据变化时保持其一致性。而任何跨越Aggregate的规则将不要求时刻保持最新状态。但在每个事务完成时,Aggregate内部所应用的固定规则必须得到满足。现在,为了实现这个概念上的Aggregate,需要对所有事务应用一组规则。

  •         根Entity具有全局标识,它最终负责检查固定规则。
  •         根Entity具有全局标识。边界内有Entity具有本地标识,这些标识只在Aggregate内部才是唯一的。
  •         Aggregate外部的对象不能引用除根Entity之外的任何内部对象。根Entity可以把对内部Entity的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个Value Object的副本传递给另一个对象,而不必关心它发生了什么变化,因为它只是一个Value,而不再与Aggregate有任何关系。
  •         作为一个规则上的推论,只有Aggregate的根才能直接通过数据库查询获取。所有其他的对象必须通过遍历关联来发现。
  •         Aggregate内部的对象可以操持对其他Aggregate根的引用。
  •         删除操作必须一次删除Aggregate边界之内的所有对象。
  •         当提交对Aggregate边界内部的任何对象的修改时,整个Aggregate的所有固定规则都必须被满足。

        我们应该将Entity和Value Object分门别类地聚集到Aggregate中,并定义Aggregate的边界。在每个Aggregate中,选择一个Entity作为根,并通过根来控制对边界内其他对象的访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保Aggregate中的对象满足所有固定规则,也可以确保在任何状态变化时Aggregate作为一个整体满足固定规则。

        Aggregate划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。

4.2 模式:Factory

        当创建一个对象或创建整个Aggregate时,如何创建工作很复杂,或者暴露了过多的内部结构,则可以使用Factory进行封装。

        对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全删除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。

        复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。在有些情况下,对象的创建和装配对应于领域中的重要事件,如“开立银行账户”。但一般情况下,对象的创建和装配在领域中并没有什么意义,它们只不过是实现的一种需要。为了解决这一问题,我们必须在领域设计中增加一种新构造,它不是Entity、Value Object,也不是Service。这与前一章的论述相违背。因此我们正在向设计中添加一些新元素,但它们不对应于模型中的任何事物,而确实又承担领域层的部分职责。

        每种面向对象的语言都提供一种创建对象的机制,但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制。这就是Factory,它是一种负责创建其他对象的程序元素。

        正如对象的接口封装了对象的实现一样,Factory封装了创建复杂对象或Aggregate所需的知识。它提供了客户目标的接口,以及被创建对象的抽象视图。

        因此:
        应该将创建复杂对象的实例和Aggregate的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建Aggregate时要把它作为一个整体,并确保它满足固定规则。

        Factory有很多种设计方式。【Gamma et al. 1995】中详尽论述了几种特定目的的创建模式,包括Factory Method(工厂方法)、Abstract Factory(抽象工厂)和Builder(构造器)。

        Factory是领域设计的重要组件,正确使用Factory有助于保证Model-Driven Design沿正确的轨道前进。

        任何好的工厂都需要满足以下两个基本需求。
        (1)每个创建方法都是原子的,而且要保证被创建对象或Aggregate的所有固定规则。
        (2)Factory应该被抽象为所需的类型,而不是所要求创建的具体的类。【Gamma et al. 1995】中的高级Factory模式介绍了这一话题。

4.2.1 选择Factory及其应用位置

        一般来说,Factory的作用是隐藏创建对象的细节,而且我们把Factory用在那在需要隐藏细节的地方。这些决定通常与Aggregate有关。

        例如,如果需要向一个已存在的Aggregate添加元素,可以在Aggregate的根上创建一个Factory Method。另外,也可以在一个对象上使用Factory Method,这个对象与生成的另一个对象密切相关,但它并不拥有所生成的对象。

        Factory与被构建对象之间是紧密耦合的,因此Factory应该只是被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏而双找不到合适的地方来隐藏它们时,必须创建一个专用的Factory对象或Service。

4.2.2 有些情况下只需使用构造函数

        常见代码都是直接调用类构造函数来创建的,或者是使用编程语言的最基本的实例创建方式。Factory的引入提供了巨大的优势,而这种优势往往未得到充分利用。在有些情况下直接使用构造函数确实是最佳选择。

        在以下情况下最好使用简单的、公共的构造函数。

  •         类(class)是一种类型(type)。它不是任何相关层次结构的一部分,而且也没有通过接口实现多态性。
  •         客户关心的是实现,可能是作为选择Strategy的一种方式。
  •         客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。
  •         构造并不复杂。
  •         公共构造函数必须遵守与Factory相同的规则:它必须是原子操作,而且要满足被创建对象的所有固定规则。

        不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是Aggregate,需要使用Factory。使用Factory Method的门槛并不高。

        虽然没有使用Factory,但抽象集合类型仍然具有一定的价值,原因在于它们使用模式。集合通常在一直地方创建,而在其他地方使用。这意味着最终使用集合的客户仍可以与接口进行对话,从而不与实现发生耦合。集合类的选择通常由拥有该集合的对象来决定,或是由该对象的Factory来决定。

4.2.3 接口的设计

        当设计Factory的方法签名时,无论是独立的Factory还是Factory Method,都要记住以下两点。

  •         每个操作必须是原子的。我们必须在与Factory的一次交互中把创建对象所需的所有信息传递给Factory。同时必须确定当创建失败时将执行什么操作,比如某些固定规则没有被满足。
  •         Factory将与其参数发生耦合。如果在选择输入时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数的处理。

        使用抽象类型的参数,而不是他们的具体类。Factory与被构建对象的具体类发生耦合,而无需与具体的参数发生耦合。

4.2.4 固定规则的相关逻辑应放置在哪里

        Factory可以将固定规则的检查工作委派给被创建对象,而且这通常是最佳选择。

        在某些情况下,把固定规则的相关逻辑放到Factory中是有好处的,这样可以让被创建对象的职责更明晰。

4.2.5 Entity Factory与Value Object Factory

        Entity Factory与Value Object Factory有两个方面的不同。由于Value Object是不可变的,因此,Factory所生成的对象就是最终形式。因此Factory操作必须得到被创建对象的完整描述。而Entity Factory则只需具有构造有效Aggregate所需的那些属性。对于固定规则不关心的细节,可以之后再添加。

4.2.6 重建已存储的对象

        在某一时刻,检索操作需要将存储后或网络传输后的数据重新装配成一个可用的对象。用于重建对象的Factory与用于创建对象的Factory很类似,但也有以下两点不同。

        (1)用于重建对象的Entity Factory不分配新的跟踪ID。如果重新分配ID,将丢失与先前对象的连续性。因此,在重建对象的Factory中,标识属性必须是输入参数的一部分。

        (2)当固定规则未满足时,重建对象的Factory采用不同的方式进行处理。若重建对象失败,必须通过某种策略来修复这种失败,这使得重建对象比创建对象更困难。

4.3 模式:Repository

        Factory封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,这是对象与存储之间的互相转换。这咱转换由另一种领域设计构造来处理,它就是Repository。

        无论要用对象执行什么操作,都需要保持一个对它的引用。客户需要一种有效的方式来获取对已存在的领域对象的引用。Repository将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(Collection),只是具有更复杂的查询功能。在添加或删除相应类型的对象时,Repository的后台机制负责将对象添加到数据库中,或从数据中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对Aggregate根的整个生命周期的全程访问。

        客户使用查询方法向Repository请求对象,这些查询方法根据客户所指定的条件来挑选对象。Repository检索被请求的对象,并封装数据库查询和元数据映射机制。Repository可以根据客户所要求的各种条件来挑选对象。它们也可以返回汇总信息。

        

         Repository有诸多优点:

  •         它们为客户提供了一个简单的模型,可用来持久化对象并管理它们的生命周期;
  •         它们使应用程序和领域设计与持久化技术解耦;
  •         它们体现了有关对象访问的设计决策;
  •         可以很容易将它们替换为“哑实现”(Dummy Implementation),以便在测试中使用。

4.3.1 Repository的查询

        所有Repository都为客户提供了根据某种条件来查询对象的方法,最容易构建的是用硬编码的方式来实现一些具有特定参数的查询。

        在一些需要执行大量查询的项目上,可以构建一个支持更灵活查询的Repository框架。如图

        基于Specification(规格)的查询是将Repository通用化的好办法。客户可以使用规格来描述它需要什么,而不必关心如何获得结果。在这个过程中,可以创建一个对象来实际执行筛选操作。

        即使一个Repository的设计采取了灵活的查询方式,也应该允许添加专门的硬编码查询。

4.3.2 客户代码可以忽略Repository的实现,但开发人员不能忽略

        持久化技术的封装可以使得客户变得十分简单,并且客户与Repository的实现之间完全解耦。但像一般的封装一样,开发人员必须知道在封装背后都发生了什么。在使用Repository时,不同的使用方式或工作方式可能会对性能产生极大的影响。

        底层技术可能会限制我们的建模选择。同样,开发人员要获得Repository的使用及其查询实现之间的双向反馈。

4.3.3 Repository的实现

        根据所使用的持久化技术和基础设施的不同,Repository的实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节,这样不管数据是存储在何处,客户代码均相同。Repository将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是Repository实现的最基本特性。

Repository的实现概念在很多情况下都适用,可能需要注意以下几点。

  •         对类型进行抽象。Repository含有特定类型的所有实例,但这并不意味着每个类型都需要有一个Repository。类型可以是一个层次结构中的抽象超类。
  •         充分利用与客户解耦的优点。
  •         将事务的控制权留给客户。 

4.3.4 在框架内工作

        通常,项目团队会在基础设施层中添加框架,用来支持Repository的实现。在实现Repository这样的构造之前,需要认真思考所使用的基础设施,特别是架构框架。这些框架可能提供了一些可用来轻松创建Repository的服务,但也可能会妨碍创建Repository的工作。

        一般来讲,在使用框架时要顺其自然。当框架无法切合时,要想办法在大方向上保持领域驱动设计的基本原理,而一些不符的细节则不必过分苛求。

4.3.5 Repository与Factory的关系

        Factory负责处理对象生命周期的开始,而Repository帮助管理对象生命周期的中间和结束。

        这种职责上的明确区分有助于Factory摆脱所有持久化职责。Factory的工作是用数据来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道在创建完成这后应该把它添加到Repository中,由Repository来封装对象在数据库中的存储。

        另一种情况促使人们将Factory和Repository结合起来使用,这就是想要实现一种“查找或创建”功能,即客户描述它所需的对象,如果找不到这样的对象,则为客户新创建一个。

4.4 为关系数据库设计对象

        在以面向对象技术为主的软件系统中,最常用的非对象组件就是关系数据库。数据库不仅仅与对象进行交互,而且它还把构成对象的数据存储为持久化形式。已有相当完善的工具可用来创建和管理对象和关系表之间的映射。依靠映射工具的功能,可以实现一些聚合或对象的组合。但至关重要的是:映射要保持透明,并易于理解——能通过代码审查或阅读映射工具中的条目就能搞明白。

  •         当数据库被视作对象存储时,数据模型与对象模型的差别不应太大。
  •         对象系统外部的过程不应该访问这样的对象存储。它们可能会破坏对象必须满足的固定规则。

        大多数情况下,关系数据库是面向对象领域中的持久化存储形式,因此简单的对应关系才是最好的。表中的一行应该包含一个对象,也可能还包含Aggregate中的一些附属项。表中的外键应该转换为对另一个Entity对象的引用。

        有时为了性能不得不违背这种简单的对应关系,但不应该由此全盘放弃简单映射的原则。

        Ubiquitous Language可能有助于将对象和关系组件联系起来,使之成为单一的模型。对象中的元素的名称和关联应该严格地对应于关系表中相应的项。

        对象世界中越来越盛行的重构实际上并没有对关系数据库设计造成多大的影响。即当对象的行为快速变化或演变的时候,数据库可能并不需要修改。让模型与数据库之间操持松散的关联是很有吸引力的。

五、参考文档

DOMAIN-DRIVERN DESIGN
TACKLING COMPLEXITY IN THE HEART OF SOFTWARE

领域驱动设计
软件核心复杂性应对之道

【美】Eric Evans 著   赵俐 盛海艳 刘霞 等 译

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/589481.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

内侧APP分发平台:移动应用开发的加速器

在数字化时代,移动应用已成为企业触达用户的重要渠道。为了迅速占领市场,开发者需要一种能够快速发布和测试移动应用的解决方案。内侧APP分发平台应运而生,它通过简化应用的封装、测试和分发流程,极大地提升了移动应用的上市速度。…

【大数据面试知识点】Spark中的累加器

Spark累加器 累加器用来把Executor端变量信息聚合到Driver端,在driver程序中定义的变量,在Executor端的每个task都会得到这个变量的一份新的副本,每个task更新这些副本的值后,传回driver端进行merge。 累加器一般是放在行动算子…

12. 整数转罗马数字

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 …

第14课 多维数组

文章目录 前言一、多维数组的定义二、多维数组的初始化三、多维数组的使用(以二维数组为例)1. 矩阵转置问题 三、课后练习1. 求一个m*n矩阵中所有元素的累加和2. 查找并输出一个m*n矩阵中的最小元素以及其在矩阵中的位置3. 将m*n矩阵A复制为m*n矩阵B&…

leetcode 贪心(分发糖果、K次取反后最大化的数组和、加油站)

1005.K次取反后最大化的数组和 给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。) 以这种方式修改数组后…

Linux报错:audit: backlog limit exceeded

今天,一台虚拟机上操作昨天打开的连接一直没响应,新打开连接连接不上。SSH校验不通过。 通过IT的后台,可以看到满屏的audit: backlog limit exceeded。 问题原因:audit服务记录的审计事件超出默认(或设置)数量 ,达到或…

边缘计算网关在温室大棚智能控制系统应用,开启农业新篇章

项目需求 ●目前大棚主要通过人为手动控温度、控水、控光照、控风,希望通过物联网技术在保障产量的前提下,提高作业效率,降低大棚总和管理成本。 ●释放部分劳动力,让农户有精力管理更多大棚,进而增加农户收入。 ●…

Python进行批量字符替换的3种方法

一、问题的提出 之前,我写过一篇如何在word中计算数学算式: 如何用Python批量计算Word中的算式-CSDN博客 为了计算算式,就需要对算式进行格式化,把不规则的算式转换成规则的算式,这时就会涉及到一些字符的批量替换。…

如何在 Linux 中配置 firewalld 规则

什么是FirewallD “firewalld”是firewall daemon。它提供了一个动态管理的防火墙,带有一个非常强大的过滤系统,称为 Netfilter,由 Linux 内核提供。 FirewallD 使用zones和services的概念,而 iptables 使用chain和rules。与 ip…

【LLM-RAG】知识库问答 | 检索 | embedding

note RAG流程(写作论文中的background:公式设定、emb、召回内容、召回基准)(工作中的思路:嵌入模型、向量存储、向量存储检索器、LLM、query改写、RAG评测方法)仅为个人关于RAG的一些零碎总结,…

【网络面试(4)】协议栈和套接字及连接阶段的三次握手原理

1. 协议栈 一直对操作系统系统的内核协议栈理解的比较模糊,借着这一篇博客做一下简单梳理, 我觉得最直白的理解就是,内核协议栈就是操作系统中的一个网络控制软件,就是一段程序代码,它负责和网卡驱动程序交互&#xff…

Docker 从入门到实践:Docker介绍

前言 在当今的软件开发和部署领域,Docker已经成为了一个不可或缺的工具。Docker以其轻量级、可移植性和标准化等特点,使得应用程序的部署和管理变得前所未有的简单。无论您是一名开发者、系统管理员,还是IT架构师,理解并掌握Dock…

7.11全排列(LC46-M)

算法: 排列和组合很像,但是有顺序。 还是用回溯算法。 与组合不同之处(无startindex,有used数组): 首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合。 可以看出元素1在[1,2]中已经使…

大学物理II-作业1【题解】

1.【单选题】——考查高斯定理 下面关于高斯定理描述正确的是(D )。 A.高斯面上的电场强度是由高斯面内的电荷激发的 B.高斯面上的各点电场强度为零时,高斯面内一定没有电荷 C.通过高斯面的电通量为零时,高斯面上各点电场强度…

基于被囊群算法优化的Elman神经网络数据预测 - 附代码

基于被囊群算法优化的Elman神经网络数据预测 - 附代码 文章目录 基于被囊群算法优化的Elman神经网络数据预测 - 附代码1.Elman 神经网络结构2.Elman 神经用络学习过程3.电力负荷预测概述3.1 模型建立 4.基于被囊群优化的Elman网络5.测试结果6.参考文献7.Matlab代码 摘要&#x…

2023-12-15 LeetCode每日一题(反转二叉树的奇数层)

2023-12-15每日一题 一、题目编号 2415. 反转二叉树的奇数层二、题目链接 点击跳转到题目位置 三、题目描述 给你一棵 完美 二叉树的根节点 root ,请你反转这棵树中每个 奇数 层的节点值。 例如,假设第 3 层的节点值是 [2,1,3,4,7,11,29,18] &…

lambda表达式和包装器

正文开始前给大家推荐个网站,前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 我们在使用库里的排序算法时如果排序的是自定义类型或者库里默认的排序不能满足我们则需求&…

【力扣100】46.全排列

添加链接描述 class Solution:def permute(self, nums: List[int]) -> List[List[int]]:# 思路是使用回溯if not nums:return []def dfs(path,depth,visited,res):# 出递归的条件是当当前的深度已经和nums的长度一样了,把path加入数组,然后出递归if …

HTML与CSS

目录 1、HTML简介 2、CSS简介 2.1选择器 2.1.1标签选择器 2.1.2类选择器 2.1.3层级选择器(后代选择器) 2.1.4id选择器 2.1.5组选择器 2.1.6伪类选择器 2.2样式属性 2.2.1布局常用样式属性 2.2.2文本常用样式属性 1、HTML简介 超文本标记语言HTML是一种标记语言&…

帆软报表如何灵活控制水印的显示

在帆软报表中如果要显示水印,如果要全部都要显示,只需要到决策系统--安装设置中打开水印开关。如果想要某个报表显示水印,可以在设计器的水印设置中为该报表设置水印。 但是如果碰到这种需求,比如某些人或者某些角色需要显示水印,其他人不显示。或者是预览报表需要显示水印…