DDD领域驱动设计---入门书籍:第二部分:模型驱动设计的构造块

第二部分:模型驱动设计的构造块

开发一个好的领域模型是一门艺术。而模型中各个元素的实际设计和实现则相对系统化。将领域设计与软件系统中的其他关注点分离会使设计与模型之间的关系非常清晰。根据不同的特征来定义模型元素则会使元素的意义更加鲜明。对每个元素使用已验证的模式有助于创建出更易于实现的模型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QQeEMUp-1617203210259)(G:\研究方向\DDD领域驱动设计\photo\DDD入门书籍\image-20210329000808384.png)]


第四章 分离领域

我们需要将领域对象与系统中的其他功能分离,这样就能够避免将领域概念和其他只与软件技术相关的概念搞混了,也不会在纷繁芜杂的系统中完全迷失了领域。

要想创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分都得到单独的关注。在分离的同时,也需要维持系统内部复杂的交互关系。

给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码
放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识。

只要连接方式能够维持领域层的独立性,保证在设计领域对象时不需要同时考虑可能与其交互的用户界面,那么这些连接方式就都是可用的。

最好的架构框架既能解决复杂技术问题,也能让领域开发人员集中精力去表达模型,而不考虑其他问题。精细的框架也可能会束缚住程序开发人员。

框架的目的:建立一种可以表达领域模型的实现并且用它来解决重要问题。

**“领域层”**则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。

将领域实现独立出来是领域驱动设计的前提。

SMART UI智能用户界面

在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。

  • 优点
    • 效率高,能在短时间内实现简单的应用程序。
    • 能力较差的开发人员可以几乎不经过培训就采用它。
    • 甚至可以克服需求分析上的不足,只要把原型发布给用户,然后根据用户反馈快速修改软件产品即可。
    • 程序之间彼此独立,这样,可以相对准确地安排小模块交付的日期。额外扩展简单的功能也很容易。
    • 可以很顺利地使用关系数据库,能够提供数据级的整合。
    • 可以使用第四代语言工具。
    • 移交应用程序后,维护程序员可以迅速重写他们不明白的代码段,因为修改代码只会影响到代码所在的用户界面。
  • 缺点
    • 不通过数据库很难集成应用模块。
    • 没有对行为的重用,也没有对业务问题的抽象。每当操作用到业务规则时,都必须重复这些规则。
    • 快速的原型建立和迭代很快会达到其极限,因为抽象的缺乏限制了重构的选择。
    • 复杂的功能很快会让你无所适从,所以程序的扩展只能是增加简单的应用模块,没有很好的办法来实现更丰富的功能。

如果一个架构能够把那些与领域相关的代码隔离出来,得到一个内聚的领域设计,同时又使领域与系统其他部分保持松散耦合,那么这种架构也许可以支持领域驱动设计。


第五章 软件中所表示的模型

3种模型元素模式:ENTITY、VALUE OBJECT和SERVICE。

关联

控制关联:

  • 规定一个遍历方向
  • 添加一个限定符,以便有效的减少多重关联
  • 消除不必要的关联

坚持将关联限定为领域所倾向的方向,不仅可以提高这些关联的表达力并简化其实现,而且还可以突出剩下的双向关联的重要性。

模式:ENTITY/REFERENCE OBJECT

ENTITY(实体)有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标识来决定,而不依赖于其所具有的属性。

ENTITY可以是任何事物,只要满足两个条件即可,一是它在整个生命周期中具有连续性,二是它的区别并不是由那些对用户非常重要的属性决定的。

ENTITY最基本的职责是确保连续性,以便使其行为更清楚且可预测。保持实体的简练是实现这一责任的关键。不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住ENTITY对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。

模式:VALUE OBJECT

跟踪ENTITY的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。

用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。VALUE OBJECT被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。

VALUE OBJECT可以是其他对象的集合。

VALUE OBJECT经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。VALUE OBJECT可以用作ENTITY(以及其他VALUE)的属性。

当我们只关心一个模型元素的属性时,应把它归类为VALUE OBJECT。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。VALUE OBJECT应该是不可变的。不要为它分配任何标识,而且不要把它设计成像ENTITY那么复杂。

共享VALUE OBJECT

  • 节省数据库空间或减少对象数量是一个关键要求时;
  • 通信开销很低时(如在中央服务器中);
  • 共享的对象被严格限定为不可变时。

保持VALUE OBJECT不变可以极大地简化实现,并确保共享和引用传递的安全性。而且这样做也符合值的意义。如果属性的值发生改变,我们应该使用一个不同的VALUE OBJECT,而不是修改现有的VALUE OBJECT。尽管如此,在有些情况下出于性能考虑,仍需要让VALUE OBJECT是可变的。这包括以下因素:

  • 如果VALUE频繁改变;

  • 如果创建或删除对象的开销很大;

  • 如果替换(而不是修改)将打乱集群(像前面示例中讨论的那样);

  • 如果VALUE的共享不多,或者共享不会提高集群性能,或其他某种技术原因。

    再次强调:如果一个VALUE的实现是可变的,那么就不能共享它。

模式:SERVICE

在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是SERVICE(服务)。

一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为ENTITY或VALUE OBJECT的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。

操作名称应来自于UBIQUITOUS LANGUAGE,如果UBIQUITOUS LANGUAGE 中没有这个名称,则应该将其引入到UBIQUITOUS LANGUAGE中。参数和结果应该是领域对象。

好的SERVICE有以下3个特征。
(1) 与领域概念相关的操作不是ENTITY或VALUE OBJECT的一个自然组成部分。
(2) 接口是根据领域模型的其他元素定义的。
(3) 操作是无状态的。

SERVICE还有其他有用的功能,它可以控制领域层中的接口的粒度,并且避免客户端与ENTITY和VALUE OBJECT耦合。

在大型系统中,中等粒度的、无状态的SERVICE更容易被复用,因为它们在简单的接口背后封装了重要的功能。此外,细粒度的对象可能导致分布式系统的消息传递的效率低下。

模式:MODULE/PACKAGE

虽然使用模块有一些技术上的原因,但主要原因却是认知超载。MODULE为人们提供了两种观察模型的方式,一是可以在MODULE中查看细节,而不会被整个模型淹没,二是观察MODULE之间的关系,而不考虑其内部细节

一个人一次考虑的事情是有限的(因此才要低耦合)。不连贯的思想和“一锅粥”似的思想同样难于理解(因此才要高内聚)。

MODULE的选择应该取决于被划分到模块中的对象的意义。

选择能够描述系统的MODULE,并使之包含一个内聚的概念集合。这通常会实现MODULE之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为MODULE基础的概念(这个概念先前可能被忽视了),基于这个概念组织的MODULE可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。

MODULE的名称应该是UBIQUITOUS LANGUAGE中的术语。MODULE及其名称应反映出领域的深层知识。

如果一个类确实依赖于另一个包中的某个类,而且本地MODULE对该MODULE并没有概念上的依赖关系,那么或许应该移动一个类,或者考虑重新组织MODULE。

实现中的对象、指针和检索机制必须直接、清楚地映射到模型元素。如果没有做到这一点,就要重写代码,或者回头修改模型,或者同时修改代码和模型。

对象模型可以解决很多实际的软件问题,但也有一些领域不适合用封装了行为的各种对象来建模。例如,涉及大量数学问题的领域或者受全局逻辑推理控制的领域就不适合使用面向对象的范式。

当领域的主要部分明显属于不同的范式时,明智的做法是用适合各个部分的范式对其建模,并使用混合工具集来进行实现。

当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下4条经验规则。

  • 不要和实现范式对抗。我们总是可以用别的方式来考虑领域。找到适合于范式的模型概念。
  • 把通用语言作为依靠的基础。即使工具之间没有严格联系时,语言使用上的高度一致性也能防止各个设计部分分裂。
  • 不要一味依赖UML。有时固定使用某种工具(如UML绘图工具)将导致人们通过歪曲模型来使它更容易画出来。例如,UML确实有一些特性很适合表达约束,但它并不是在所有情况下都适用。有时使用其他风格的图形(可能适用于其他范式)或者简单的语言描述比牵强附会地适应某种对象视图更好。
  • 保持怀疑态度。工具是否真正有用武之地?不能因为存在一些规则,就必须使用规则引擎。规则也可以表示为对象,虽然可能不是特别优雅。多个范式会使问题变得非常复杂。

第六章 领域对象的生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eOpdVdH8-1617203210261)(G:\研究方向\DDD领域驱动设计\photo\DDD入门书籍\image-20210331161839678.png)]

(1) 在整个生命周期中维护完整性。
(2) 防止模型陷入管理生命周期复杂性造成的困境当中。

模式:AGGREGATE

在多个客户对相同对象进行并发访问的系统中,这个问题更加突出。当很多用户对系统中的对象进行查询和更新时,必须防止他们同时修改互相依赖的对象。

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8V3DZc7-1617203210262)(G:\研究方向\DDD领域驱动设计\photo\DDD入门书籍\image-20210331164445295.png)]

固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及AGGREGATE成员之间的内部关系。而任何跨越AGGREGATE的规则将不要求每时每刻都保持最新状态。

为了实现这个概念上的AGGREGATE,需要对所有事务应用一组规则。

  • 根ENTITY具有全局标识,它最终负责检查固定规则。
  • 根ENTITY具有全局标识。边界内的ENTITY具有本地标识,这些标识只在AGGREGATE内部才是唯一的。
  • AGGREGATE外部的对象不能引用除根ENTITY之外的任何内部对象。根ENTITY可以把对内部ENTITY的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个VALUE OBJECT的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个VALUE,不再与AGGREGATE有任何关联。
  • 作为上一条规则的推论,只有AGGREGATE的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。、
  • AGGREGATE内部的对象可以保持对其他AGGREGATE根的引用。
  • 删除操作必须一次删除AGGREGATE边界之内的所有对象。(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收。)
  • 当提交对AGGREGATE边界内部的任何对象的修改时,整个AGGREGATE的所有固定规则都必须被满足。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrSkHciz-1617203210264)(G:\研究方向\DDD领域驱动设计\photo\DDD入门书籍\image-20210331165052487.png)]

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

模式:FACTORY

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

对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

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

好工厂的两个基本需求:

  1. 每个创建方法都是原子的,而且要保证被创建对象或AGGREGATE的所有固定规则。
  2. FACTORY应该被抽象为所需的类型,而不是所要创建的具体类

FACTORY的作用是隐藏创建对象的细节,而且我们把FACTORY用在那些需要隐藏细节的地方。

整个AGGREGATE通常由一个独立的FACTORY来创建,FACTORY负责把对根的引用传递出去,并确保创建出的AGGREGATE满足固定规则。

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

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

接口的设计

  • 每个操作都必须是原子的。
  • Factory将与其参数发生耦合。

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

由于VALUE OBJECT是不可变的,因此,FACTORY所生成的对象就是最终形式。因此FACTORY操作必须得到被创建对象的完整描述。

ENTITY FACTORY则只需具有构造有效AGGREGATE所需的那些属性。对于固定规则不关心的细节,可以之后再添加。

用于重建对象的ENTITY FACTORY不分配新的跟踪ID。

当固定规则未被满足时,重建对象的FACTORY采用不同的方式进行处理。必须通过某种策略来修复这种不一致的情况,这使得重建对象比创建新对象更困难。

FACTORY封装了对象创建和重建时的生命周期转换。

模式:REPOSITORY

获得对某个对象的引用:

一种方法是创建对象,因为创建操作将返回对新对象的引用。

第二种方法是遍历关联。我们以一个已知对象作为起点,并向它请求一个关联的对象。

客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过AGGREGATE的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而ENTITY和VALUE OBJECT则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

REPOSITORY将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(collection),只是具有更复杂的查询功能。在添加或删除相应类型的对象时,REPOSITORY的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对AGGREGATE根的整个生命周期的全程访问。

REPOSITORY有很多优点,包括:

  1. 它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期;
  2. 它们使应用程序和领域设计与持久化技术(多种数据库策略甚至是多个数据源)解耦;
  3. 它们体现了有关对象访问的设计决策;
  4. 可以很容易将它们替换为“哑实现”(dummy implementation),以便在测试中使用(通常使用内存中的集合)。

将存储、检索和查询机制封装起来是REPOSITORY实现的最基本的特性。

Repository和Factory之间:

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

从领域驱动设计的角度来看,FACTORY和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样。对象可能必须被重建(的确,可能会创建一个新实例),但它是同一个概念对象,仍旧处于生命周期的中间。


第七章 使用语言:一个扩展的示例

举例如何利用上述概念来进行实操。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值