《实现领域驱动设计》

DDD入门

1.1 DDD是什么?

DDD是一种软件开发方法

  • DDD将领域专家和开发人员聚集到一起,开发的软件能够反映出领域专家的思维模型。目标是:交付最具业务价值的软件。
  • DDD关注业务战略:指引我们如何实现面向服务架构(service-oriented architecture)或者业务驱动(business-driven architecture)架构。
  • 使用战术设计建模工具:这些战术设计工具使开发人员能够按照领域专家的思维模型开发软件。保证成功地交付真正的业务价值。

1.2 为什么我们需要DDD?

  • 使领域专家和开发者在一起工作,这样开发的软件能够准确地传达业务规则。
  • "准确传达业务规则"意思是说:此时的软件就像是领域专家是编码人员时所开发出来的一样。
  • 可以帮助业务人员自我提高。在DDD中,每个人都在学习,同时每个人又是知识的贡献者。
  • 关键在于对于知识的集中,确保软件知识并不只是掌握在少数人手中。
  • 在领域专家、开发者和软件本身之间不存在"翻译"
  • 设计就是代码,代码就是设计
  • DDD提供了战略设计和战术设计两种方式。
    战略设计:帮助我们理解哪些投入是最重要的;
    战术设计:帮助我们创建DDD模型中各个部件。

1.3 如何DDD?

通用语言、限界上下文(Bounded Context)同时构成了DDD的两大支柱,并且它们是相辅相成的。

通用语言
通用语言是团队共享的语言。领域专家和开发者使用相同的通用语言进行交流。事实上,团队中每个人都使用相同的通用语言。不管你再团队中的角色如何,只要你是团队的一员,都将使用通用语言。团队成员通过讨论、参考资料、引用标准、查阅字典等对通用语言进行改进。有时我们发现,有些我们曾经认为能很好表达业务的词汇不再适用了,而另外的一些词汇具有更好的效果。

1.4 使用DDD的业务价值

不管使用什么技术,我们的目的都是提供业务价值。

我们可以将DDD的业务价值大致总结为以下几点:

1.你获得了一个非常有用的领域模型
2.你的业务得到了更准确的定义和理解
3.领域专家可以为软件设计做出贡献
4.更好的用户体验
5.清晰的模型边界
6.更好的企业架构
7.敏捷、选代式和持续建模
8.使用战略和战术新工具

1.5 实施DDD所面临的挑战

  • 为创建通用语言腾出时间和精力
    使用DDD最大的挑战之一便是:我们需要花费大量的时间和精力来思考业务领域,研究概念和术语,并且和领域专家交流,以发现、捕捉和改进通用语言。如果你想完全采用DDD来最大化业务价值,你需要做出很多努力,并且花费很多时间。
  • 持续地将领域专家引入项目
    要将领域专家引入你的项目恐怕也不是一件易事。但是不管有多么困难,这是你必须做的。如果你连一个领域专家都找不到,那么你根本无法对一个领域有深入的理解。
  • 改变开发者对领域的思考方式
    多数开发者在采用DDD时都需要转变自己思考问题的方式。作为开发者,我们都是技术思想者,技术实现对于我们来说并不是什么难事。我并不是说技术地思考不好,只是说有时少从技术层面去思考会更好。这么多年来,我们都习惯了单从技术层面完成软件开发,那么现在,是时候考虑一种新的思考方式了。为你的业务领域开发一门通用语言便是一个好的出发点。

2 领域、子域、界限上下文

由于“领域模型”包含了“领域”这个词,我们可能会认为应该为整个业务系统创建一个单一的、内聚的、全功能式的模型。然而,这并不是我们使用DDD的目标。正好相反,在DDD中,一个领域被分为若干子域,领域模型在限界上下文中完成开发。事实上,在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。
工作中的子域和限界上下文
在这里插入图片描述

将关注点放在核心域上
在这里插入图片描述
图2.2上半部分的领域边界,你会看到一个叫核心域的子域。
对于核心域,它是整个业务领域的一部分,也是业务成功的主要促成因素。从战略层面上讲,企业应该在核心域上胜人一筹。我们应该给予核心域最高的优先级、最资深的领域专家和最优秀的开发团队。在实施DDD的过程中,你将主要关注于核心域。

图2.2中还展示了另外两种子域:支撑子域和通用子域
有时,我们会创建或者购买某个限界上下文来支撑我们的业务。如果这样的限界上下文对应着业务的某些重要方面,但却不是核心,那么它便是一个支撑子域
创建支撑子域的原因在于它们专注于业务的某个方面,否则,如果一个子域被用于整个业务系统,那么这个子域便是通用子域
我们并不能说支撑子域和通用子域是不重要的,它们是重要的,只是我们对它们的要求并不像核心域那么高。

2.1 战略设计为什么重要

2.2 现实世界中领域和子域

领域中还同时存在问题空间(problem space)和解决方案空间(solution space)在问题空间中,我们思考的是业务所面临的挑战,而在解决方案空间中,我们思考如何实现软件以解决这些业务挑战。
以下是如何将这两者应用到我们已经学过的知识中:

  • 问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。
    对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。问题空间中的子域通常随着项目的不同而不同,他们各自关注于当前的业务问题,这使得子域对于问题空间的评估非常有用。子域允许我们快速地浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。
  • 解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。
    这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。

2.3 理解限界上下文

在很多情况下在不同模型中存在名字相同或相近的对象,但是它们的意思却不同。当模型被一个显式的边界所包围时,其中每个概念的含义便是确定的了。因此,限界上下文主要是一个语义上的边界,我们应该通过这一点来衡量对一个限界上下文的使用正确与否。

在这里插入图片描述

不同限界上下文中的Account对象具有完全不同的含义
银行上下文:账户表示一个客户在银行的存款支票账户和储蓄账户
状态,并记录每次交易信息。
文学上下文:账户表示用文字记录的在一段时间之内发生的一系列事件。

在通常情况下,我们所面对的都是一些区别甚小的概念定义。原因在于:在一个上下文中,团队通常根据通用语言来命名某个概念。我们并不会随意地命名一个概念以刻意地保持与其他上下文的不同。

比如两个银行上下文,一个用于支票账户另一个用于储蓄账户。在支票上下文(Checking
Context)中我们不必使用支票账户(CheckingAccount);在储蓄上下文(Saving Context)中我们也不必使用储蓄账户(SavingAccount)。两个概念都可以使用账户(Account)来表示因为限界上下文已经对此做了区分。当然,我们并没有规定不能使用更具体的名字这只是团队自己的选择而已。

当需要集成时,我们必须在不同的限界上下文之间进行概念映射。
限界上下文不仅仅只包含模型
一个限界上下文并不是只包含领域模型。诚然,模型是限界上下文的主要“公民”。但是,限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务”。有时,限界上下文所包含的内容可能比较少,比如,一个通用子域便可以只包含领域模型。

限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。需要注意的是,对于架构中的每个组件,我们都应该将其放在适当的地方。
限界上下文的大小
限界上下文应该足够大,以能够表达它所对应的整套通用语言。

核心领域之外的概念不应该包含在限界上下文中。如果一个概念不属于你的通用语言,那么一开始你就不应该将其引入到模型中。

与技术组件保持一致
将限界上下文想成是技术组件并无大碍,只是我们需要记住:技术组件并不能定义限界上下文。
在这里插入图片描述
让我们来看看示例DDD项目中的这3个限界上下文。他们分别是协作上下文身份与访问上下文敏捷项目管理上下文

协作上下文
在这里插入图片描述
协作上下文的团队将所有与安全和权限相关的模块和类型从该上下文中移除,然后逐渐采用新的身份与访问上下文。

身份与访问上下文

现在,多数企业级应用程序都需要某种形式的安全和权限组件,这样的组件用以对用户进行认证和授权。正如我们在前面所分析的,一种幼稚的做法是将这样的组件嵌入到每一个离散的系统中,这将导致每一个系统都产生筒仓效应(siloeffect) .
在这里插入图片描述
当有我们关心的状态由于模型行为而发生改变时,系统将发布领域事件(8)。

敏捷项目管理上下文
在这里插入图片描述

3 上下文映射图

一个项目的上下文映射图(Context Map)可以用两种方式来表示比较容易的一种是画一个简单的框图来表示两个或多个限界上下文之间的映射关系。该框图表示了不同的限界上下文在解决方案空间中是如何通过集成相互关联的。另一种更详细的方式是通过限界上下文集成的源代码实现来表示。

3.1 上下文映射图为什么重要

在这里插入图片描述
你的映射图可以使你了解到映射图的内部,并且可以指明在哪些地方需要与其他团队进行交流。尽早绘制上下文映射图,这样可以迫使你仔细思考你的项目和你所依赖项目之间的关系。

绘制上下文映射图
上下文映射图并不是一种企业架构,也不是系统拓扑图。但是,它可以用于高层次的架构分析,指出诸如集成瓶颈之类的架构不足。上下文映射图展现了一种组织动态能力(organizational dynamic)它可以帮助我们识别出有碍项目进展的一些管理问题。
限界上下文之间的关系

  • 合作关系(Partnership)
  • 共享内核(SharedKernel)
    对模型和代码的共享将产生一种紧密的依赖性。
    对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式的边界,并保持共享内核的小型化。
  • 客户方-供应方开发(Customer-Supplier Development)
    当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。
  • 遵奉者(Conformist)
    在存在上游-下游关系的两个团队中如果上游团队已经没有动力提供下游团队之所需,下游团队便孤军无助了。
  • 防腐层(Anticorruption Layer)
    防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无须修改。在防腐层内部,它在你自己的模型和他方模型之间进行翻译转换。
  • 开放主机服务(Open Host Service)
    定义一种协议,让你的子系统通过该协议来访问你的服务。
  • 发布语言(Published Language)
    在两个限界上下文之间翻译模型需要一种公用的语言。
  • 另谋他路(SeparateWay)
    在确定需求时我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。
  • 大泥球(Big Ballof Mud)
    当我们检查已有系统时经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。
映射3个示例限界上下文

在这里插入图片描述
身份与访问上下文位于最上游它对协作上下文和敏捷项目管理上下文均会产生影响。同时,协作上下文又是敏捷项目管理上下文的上游,因为后者的模型依赖于前者的模型和服务。

所有下游系统的连接框都标以ACL,即防腐层。对于它们的技术实现将在集成限界上下文(13)中讲到。

协作上下文
协作上下文通过传统的类似于RPC的方式获取外部资源。协作上下文并不会永久性地记录下从身份与访问上下文中获取来的数据而是在每次需要数据时重新向远程系统发出请求。
在这里插入图片描述
敏捷项目管理上下文
为了达到比RPC更高的自治性,敏捷项目管理上下文的团队将尽量限制对RPC的使用,此时他们可以选择异步请求,或者事件处理等方式。

DDD的做法是:在本地创建一些由外部模型翻译而成的领域对象,这些对象保留着本地模型所需的最小状态集。为了初始化这些对象,我们只需要有限的RPC调用或REST请求。然而,要与远程模型保持同步,最好的方式是在远程系统中采用面向消息的通知(notification)机制。消息通知可以通过服务总线进行发布也可以采用消息队列或者REST。
在这里插入图片描述

和身份与访问上下文集成
在这里插入图片描述
敏捷项目管理上下文在本地有DiscussionService和SchedulingService,它们是领域服务,用于管理协作系统中的讨论和日历条目

4 架构

DDD的一大好处便是它并不需要使用特定的架构。由于核心域(2)位于限界上下文(2)中我们可以在整个系统中使用多种风格的架构有些架构包围着领域模型,能够全局性地影响系统,而有些架构则满足了某些特定的需求。我们的目标是选择适合于自己的架构和架构模式。

4.2 分层

分层架构模式[Buschmannetal被认为是所有架构的始祖它支持N层架构系统,因此被广泛地应用于Web、企业级应用和桌面应用。在这种架构中,我们将一个应用程序或者系统分为不同的层次。
图4.1所示为一个典型的DDD系统所采用的传统分层架构,其中核心域只位于架构中的其中一层其上为用户界面层(User Interface)和应用层(ApplicationLayer),其下是基础设施层(Infrastructure Layer)。

在这里插入图片描述
分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合。
分层架构也分为几种:
严格分层架构(Strict Layers Architecture)中某层只能与直接位于其下方的层发生耦合;
松散分层架构(Relaxed Layers Architecture)则允许任意上方层与任意下方层发生耦合。由于用户界面层和应用服务通常需要与基础设施打交道,许多系统都是基于松散分层架构的。

依赖倒置原则

高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象

在这里插入图片描述
我们应该将关注点放在领域层上,采用依赖倒置原则,使领域层和基础设施层都只依赖于由领域模型所定义的抽象接口。由于应用层是领域层的直接客户,它将依赖于领域层接口,并且间接地访问资源库和由基础设施层提供的实现类。

4.3 六边形架构 (端口与适配器)

在这里插入图片描述
图4.4中有3个客户请求均抵达相同的输人端口(适配器A、B和C),另一个客户请求使用了适配器D。可能前3个请求使用了HTTP协议(浏览器REST和SOAP等)而后一个请求使用了AMQP协议(比如RabbitMQ)。

对于图4.4中右侧的端口和适配器,我们应该如何看待呢?

我们可以将资源库的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例,或者保存新的聚合实例。正如图中的适配器E、F和G所展示的,我们可以通过不同的方式实现资源库,比如关系型数据库、基于文档的存储、分布式缓存和内存存储等。如果应用程序向外界发送领域事件消息,我们将使用适配器H进行处理。该适配器处理消息输出。而刚才提到的处理AMOP消息的适配器则是处理消息输入的,因此应该使用不同的端口。

六边形架构的一大好处在于,我们可以轻易地开发用于测试的适配器。整个应用程序和领域模型可以在没有客户和存储机制的条件下进行设计开发。

如果你采用的是严格分层架构,那么你应该考虑推平这种架构,然后开始采用端口与适配器。如果设计得当,内部六边形一一也即应用程序和领域模型一是不会泄漏到外部区域的,这样也有助于形成一种清晰的应用程序边界。在外部区域,不同的适配器可以支持自动化测试和真实的客户请求,还有存储、消息和其他输出机制等。

4.4 面向服务架构SOA

面向服务架构(Service-Oriented Architecture,SOA)对于不同的人来说具有不同的意思。
服务设计原则
描述
1.服务契约 通过契约文档,服务阐述自身的目的与功能
2.松合 服务将依赖关系最小化
3.服务抽象 服务只发布契约,而向客户隐藏内部逻辑
4.服务重用性 一种服务可以被其他服务所重用
5.服务自治性 服务自行控制环境与资源以保持独立性,这有助于保持服务的一致性和可靠性
6.服务无状态性 服务负责消费方的状态管理,这不能与服务的自治性发生冲突
7.服务可发现性 客户可以通过服务元数据来查找服务和理解服务
8.服务组合性 一种服务可以由其他的服务组合而成,而不管其他服务的大小和复杂性如何
在这里插入图片描述

4.5 REST

REST本来就应该是属于Web架构的一种架构风格。
架构风格之于架构就像设计模式之于设计一样。它将不同架构实现所共有的东西抽象出来,使得我们在谈及到架构时不至于陷人技术细节中。

4.6 命令和查询职责分离

一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者皆是。
在对象层面,这意味着:

  1. 如果一个方法修改了对象的状态,该方法便是一个命令(Command),它不应该返回数据。在Java和C#中,这样的方法应该声明为void。
  2. 如果一个方法返回了数据该方法便是一个查询(Query),此时它不应该通过直接的或间接的手段修改对象的状态。在Java和C#中,这样的方法应该以其返回的数据类型进行声明。
客户端和查询处理器

客户端(图4.6最左侧)可以是Web浏览器,也可以是定制开发的桌面应用程序。它们将使用运行在服务器端的一组查询处理器。图4.6并没有显示服务器的架构层次。不管使用什么样的架构层,查询处理器都表示一个只知道如何向数据库执行基本查询(比如SOL)的简单组件。
在这里插入图片描述

4.7 事件驱动架构

事件驱动架构(Event-Driven ArchitectureEDA)是一种用于处理事件的生成发现和处理等任务的软件架构。
在这里插入图片描述
一个系统的输出端口所发出的领域事件将被发送到另一个系统的输入端口,此后输人端口的事件订阅方将对事件进行处理。对于不同的限界上下文来说,不同的领域事件具有不同含义,也有可能没有任何含义。在一个限界上下文处理某个事件时,应用程序API将采用该事件中的属性值来执行相应的操作。应用程序API所执行的命令操作将反映到命令模型中。

有可能出现这样一种情况:在一个多任务处理过程中,某种领域事件只能表示该过程中的一部分。只有在所有的参与事件都得到处理之后,我们才能认为这个多任务处理过程完成了。但是,这个过程是如何开始的?它是如何分布在整个企业范围之内的?我们如何跟踪处理进度?这些问题我们将在“长时处理过程”一节

4.8 数据网织和基于网格的分布式计算

5实体

5.1为什么使用实体

当我们需要考虑一个对象的个性特征,或者需要区分不同的对象时,我们引入实体这个领域概念。一个实体是一个唯一的东西,并且可以在相当长的一段时间内持续地变化。我们可以对实体做多次修改,故一个实体对象可能和它先前的状态大不相同。但是,由于它们拥有相同的身份标识(identity),它们依然是同一个实体。

唯一的身份标识和可变性(mutability)特征将实体对象和值对象(Value Objects,6)区分开来。

5.2唯一标识

用户提供唯一标识

让用户手动地输入对象标识看起来是一种很直接的做法。这是一种非常简单的方法,但是这种方法也可能变得复杂。
复杂性之一便是需要用户自已生成高质量的标识。此时标识可能是唯一的,但却有可能是不正确的。

应用程序生成唯一标识

有很多可靠的方法都可以自动生成唯一标识,但是如果应用程序处于集群环境或者分布在不同的计算节点中,我们就需要额外小心了。

持久化机制生成唯一标识

将唯一标识的生成委派给持久化机制是有特别的好处的。如果我们向数据库
获取一个序列值(Sequence)或递增值,结果总是唯一的。
性能可能是这种方法的一个缺点。

另一个限界上下文提供唯一标识

如果另一个限界上下文用于给实体标识赋值,那么我们需要对每一个标识进行查找、匹配和赋值。
其中最重要的是精确匹配。此时用户需要提供一种或多种属性,比如账户、用户名和E-mail地址等,以精确定位需要匹配的结果。

在这种方式中,对象同步可能是个问题。外部对象的改变将如何影响本地对象呢?我们如何知道所关联的对象已经改变了呢?这个问题可以通过事件驱动架构(4)和领域事件(8)以解决。

标识生成时间

实体唯一标识的生成既可以发生在对象创建的时候,也可以发生在持久化对象的时候。

委派标识

Hibernate更倾向于使用数据库提供的机制,比如使用一个数值序列来生成实体标识。如果我们自己的领域需要另外一种实体标识,此时这两者将产生冲突。为了解决这个问题,我们需要使用两种标识,一种为领域所使用,一种为ORM所使用,在Hibernate中,这被称为委派标识(Surrogate Identity)。

标识稳定性

在多数情况下我们都不应该修改实体的唯一标识,这样可以在实体的整个生命周期中保持标识的稳定性。
我们可以通过一些简单的措施来确保实体标识不被修改。

5.3 发现实体及其本质特征.

  • 揭开实体及其本质特征的神秘面纱
    一个实体应该具有唯一的标识,以区别于其他实体。一个实体同时还应该支持在其生命周期中的各种修改。显然,此时的User是一个实体。
  • 挖掘实体的关键行为
  • 角色和职责
    建模的一个方面便是发现对象的角色和职责。通常来说,对角色和职责分析是可以应用在领域对象上的。这里我们特别关注的是实体的角色和职责。
  • 创建实体
    当我们新建一个实体时,我们希望通过构造函数来初始化足够多的实体状态,这一方面有助于表明该实体的身份,另一方面可以帮助客户端更容易地查找该实体。
  • 验证
  • 跟踪变化
    跟踪变化最实用的方法是领域事件和事件存储。我们为领域专家所关心的所有状态改变都创建单独的事件类型,事件的名字和属性表明发生了什么样的事件。当命令操作执行完后,系统发出这些领域事件。事件的订阅方可以接收发生在模型上的所有事件在接收到事件后,订阅方将事件保存在事件存储中。

6值对象

值类型的优点:值类型用于度量和描述事物,我们可以非常容易地对值对象进行创建、测试、使用、优化和维护。

当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。

6.1值对象的特征

当你决定一个领域概念是否是一个值对象时,你需要考虑它是否拥有以下特征:

  • 它度量或者描述了领域中的一件东西
  • 它可以作为不变量。
  • 它将不同的相关的属性组合成一个概念整体(Conceptual Whole)。
  • 当度量和描述改变时,可以用另一个值对象予以替换。
  • 它可以和其他值对象进行相等性比较。
  • 它不会对协作对象造成副作用[Evans]。

对于以上特征,我们将在下文中做详细讲解。在使用这种方法分析模型时,你会发现很多领域概念都可以设计成值对象,而不是你先前认为的实体对象。

度量或描述
当你的模型中的确存在一个值对象时,不管你是否意识到,它都不应该成为你领域中的一件东西,而只是用于度量或描述领域中某件东西的一个概念。一个人拥有年龄,这里的年龄并不是一个实在的东西,而只是作为你出生了多少年的一种度量。一个人拥有名字,同样这里的名字也不是一个实在的东西,而是描述了如何称呼这个人。
该特征和下面的“概念整体”特征是紧密联系在一起的。
不变性
一个值对象在创建之后便不能改变了。例如,在使用Java或C#编程时,我们使用构造函数来创建值对象实例,此时传人的参数包含了该值对象的所有状态所需的数据信息。所传人的参数既可以作为该值对象的直接属性,也可以用于计算出新的属性。

概念整体
一个值对象可以只处理单个属性,也可以处理一组相关联的属性。在这组相关联的属性中,每一个属性都是整体属性所不可或缺的组成部分,这和简单地将一组属性组装在对象中是不同的。如果一组属性联合起来并不能表达一个整体上的概念,那么这种联合并无多大用处。

可替换性
在你的模型中,如果一个实体所引用的值对象能够正确地表达其当前的状态,那么这种引用关系可以一直维持下去。否则,我们需要将整个值对象替换成一个新的值对象实例。

值对象相等性
在比较两个值对象例时,我们需要检查这两个值对象的相等性。在整个系统中,有可能存在很多相等的值对象实例,但它们并不表示相同的实例引用。相等性通过比较两个对象的类型和属性来决定。如果两个对象的类型和属性都相等,那么这两个对象也是相等的。进而,如果两个或多个值对象实例是相等的,我们便可以用其中一个实例来替换另一个实例。

无副作用行为
一个对象的方法可以设计成一个无副作用函数(Side-Effect-Free Function)「Evans]。这里的函数表示对某个对象的操作,它只用于产生输出,而不会修改对象的状态。由于在函数执行的过程中没有状态改变,这样的函数操作也称为无副作用函数。

6.2最小化集成

在所有的DDD项目中,通常存在多个限界上下文,这意味着我们需要找到合适的方法对这些上下文进行集成。当模型概念从上游上下文流入下游上下文中时,尽量使用值对象来表示这些概念。这样的好处是可以达到最小化集成,即可以最小化下游模型中用于管理职责的属性数目。使用不变的值对象使得我们做更少的职责假设。

重用限界上下文(2)中的一个例子:上游的身份与访问上下文会影响下游的协作上下文,如图6.1所示。在身份与访问上下文中,两个聚合分别为User和Role。在协作上下文中,我们关心的是一个User是否拥有一个特定的Role,比如Moderator。协作上下文使用它的防腐层(3)向身份与访问上下文的开放主机服务(3)提出查询。如果这个集成的查询过程表明某个User拥有Moderator角色,协作上下文便会创建一个代表性的Moderator对象。
在这里插入图片描述
在这里插入图片描述

6.3用值对象表示标准类型

在Java中可以使用枚举来表示该标准类型:

public enum GroupMemberType {
    GROUP {
        public boolean isGroup() {
            return true;
        }
    },
    USER {
        public boolean isUser () {
            return true;
        }
        public boolean isGroup () {
            return false;
        }
    }
}

6.4测试值对象

为了强调测试驱动,在实现值对象之前,让我们先来看看测试。通过模拟客户端对值对象的使用,这些测试可以驱动出对领域模型的设计。

6.6测试值对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值