领域驱动视频(六)

时间:2022-08-31 13:02:50

6.1 简介

在这个领域驱动的设计基础模块中,您将了解领域事件和防腐层,了解领域模型如何与内部或者其他系统进行通信的两种模式。

6.2 Goal

我们将从域事件开始,这些事件可以用于分离关注点,允许应用程序的不同领域独立发展,有时还可以帮助实现可伸缩性。然后,我们来看看反腐败的层面,当两个子系统使用两种不同的模型,需要相互沟通时,就可以减少涉及到的工作量。让我们开始吧。

6.3 简介领域事件

领域事件是限界上下文的一个关键部分。它们提供了一种描述系统中发生的重要活动或状态变化的方法。然后,领域的其他部分可以以松散耦合的方式响应这些事件。这样,产生事件的对象不必担心发生事件时需要发生的行为,同样,事件处理对象也不需要知道事件来自哪里。这与存储库允许我们封装所有数据访问代码的方式类似,因此域的其余部分不需要知道它。
我们还可以使用事件在我们的领域之外进行通信,我们稍后将看到。另一个值得记住的事情是域事件被封装为对象。这可能与你习惯编码事件的方式不同,当我第一次开始了解它们的时候,我知道不同的。例如,在用户界面事件中,通常以某种形式在另一个类中写入委托,但在这里,它们是领域模型的第一类成员。
好的,尽管可以使用c#中的Event关键字之类的技术来实现领域事件,但是领域事件本身应该是成熟的类。实际上,领域驱动设计中,领域事件是属于领域层的对象。
Vaughn Vernon简单的描述了领域事件的感念,我们应该使用域事件来捕获领域发生的事件。领域事件应该是我们通用语言的一部分。客户或领域专家应该理解你在说什么,当你说,当预约被确认,就会发出一个AppointmentConfirmed 事件。

领域驱动视频(六)

你可能对于这种用户界面层的事件很熟悉,比方说VB6, .NET Winforms,和ASP.NET web forms,或者是上图这样的,大量使用事件和事件处理程序。在本例中,有一个单页和一个按钮,在后面的代码中可以看到已经编写了两个事件处理程序,一个在页面加载时处理,一个在按钮单击事件发生时处理。
事件是有用的,因为它们让我们避免了许多条件逻辑。相反,我们可以编写代码来表示某件事情已经发生,我们可以在系统中有其他代码监听这些信号,并相应地采取行动。在这种代码中,您没有单独的类来加载事件或单击事件

领域驱动视频(六)

但是在我们的领域驱动中,我们则需要创建一个类来代表一个领域事件,领域事件提供了与用户界面中的事件相同的优势。
并不是当我们的一个对象的状态发生变化时,必须包含所有可能需要发生的行为,相反,我们可以生成一个事件。然后,我们可以编写单独的代码来处理事件,保持模型的设计简单,并帮助确保每个类都只有一个责任。
从本质上讲,一个领域事件就是一个消息,它记录了过去发生的事情,我们的应用程序的其他部分或其他应用程序可能会感兴趣。在UI中,我们考虑事件处理程序的逻辑,以响应应用程序中发生的某些事件。当页面加载时,运行Page_Load中的代码。当用户单击按钮时,运行Button_Click事件处理程序。
在与你的领域专家讨论应用程序时,尤其要注意这类短语。这些通常是指对领域专家、系统或用户非常重要的情况,因此可能值得作为领域事件建模。而您也会因为将这些以往没有发现的领域概念建模为事件,而获取到利益。
请记住,领域事件表示发生了一些事情。由于我们通常不能改变历史的事件,这也意味着它们应该是不可变的。使用来自限界上下文的术语来命名事件是一个好主意,这种语言描述了发生的事情。如果它们被作为领域对象的命令的一部分被触发,请确保一定要使用命令名字。
这里有一些例子。根据应用程序的不同,当一个用户通过身份验证,当一个约会被确认,或者收到了付款时,有事件来表示可能是很重要的。在您的模型中,确保您在需要时间时候再去创建事件。你应该遵循YAGNI原则,就是 You Ain't Gonna Need It。换句话说,一般情况下不要创建领域事件,除非在事件发生时需要额外的发生一些其他行为,并且您希望将该行为(即EventHandler)与触发器(即产生事件的聚合)分离开来。
只有当行为不属于触发它的类时,你才需要这样做。在创建领域事件时,还有一些需要记住的事情。

领域驱动视频(六)

①我们已经提到了领域事件是对象,但是要更具体,每个领域事件都应该是它自己的类(即为某个事件单独创建自己的类)。这通常也是一个好主意,当事件发生时,因为经常处理事件的代码可能在事件发生后的某个时候运行。
②为他们创建一个接口是很有帮助的,它定义了域事件的常见需求,比如这个IDomainEvent,它捕获事件发生的日期和时间。
③此外,当您设计您的事件时,您需要考虑您想要捕获的事件特定的细节。如果它与一个实体相关,那么您可能希望在事件定义中包含实体的当前状态。想想你需要再次触发事件的信息。这可以为您提供对这个事件很重要的一组信息。
类似地,您可能需要知道事件中涉及到的所有聚合体的身份,即使您不包括整个聚合本身。这将允许事件处理程序从系统中提取信息,这些信息在处理事件时可能需要。理想情况下,DomainEvent对象应该是轻量级的,因此您需要确保捕获足够的信息来处理事件,但不能确保事件对象本身变得臃肿。
④由于主要事件是不可变的,它们通常是通过构造函数完全实例化的,
⑤因为它们只是注意到系统中发生了一些事情,它们通常不会有任何行为的副作用。

6.4 领域事件演示在一个简单的应用程序

我们将使用一个简单的控制台应用程序来演示领域事件在应用程序中的价值,这背后的想法是将事情尽可能地缩小到一个水平;然后,我们还将展示在我们的兽医预约调度应用程序中,领域事件如何在更真实的世界中扮演真正的角色。

领域驱动视频(六)

如果我们看这个我早已运行的这个程序,它很简单。如上图,标识的是输出。你可与从控制台看到打印的输出,来告诉我们应用启动了,然后创建AppintmentSchedulerService服务,使用该服务来安排Appointment(预约),然后我们也会不使用AppintmentService来创建一个Appointment,即上面new Appointmenmt()这段代码,然后我们也不通过服务来确认,而是通过appointment对象的方法confirm来进行确认,并最终在控制台打印输出"application done"来标识任务结束了 。如果我们看一下输出,我在这里添加了许多的ConsoleWriteLines,通过这些打印,我来告诉您现在发生了什么,下面看一个console的大图:

领域驱动视频(六)

你会在console看到打印的类似于“Appointment::create()”被调用,还有Email被发送了,你可以看到用户界面被更新了,实体被保存了,这就是AppintmentSchedulerService调用所做的,然后当我们创建Appointment时,第二次的预约没有使用AppintmentSchedulerService,你可以看到更多与上面类似的东西被打印。
好的,所以写的文字与领域事件的课程没有关系。他们就在那里,告诉我们究竟都发生了什么事。

领域驱动视频(六)

如果我们先来看AppintmentSchedulerService这个服务的话,它其实很简单,它所要做的就是创建一个预约然后使用我们的存储库保存它,在这种情况下,所有的东西都是硬编码的,但是现在如果我们看看Appointment,这就是我们真正想要关注的地方。

领域驱动视频(六)

我们来看看这个创建方法,它是一种静态的工厂方法。这类似于我们在调度应用程序中使用的静态工厂方法。你可以看到,在第29行,有一条注释说,我们要发送一封电子邮件通知他们这个预约被创建了。
想象一下,如果你愿意的话,这里可以有5-10行的实际的网络代码在那里设置一个from地址和一个to地址,subject(主题),创建一个Email以及其附件等等。为了简单起见我么你这里只是使用ConsoleWriteLines做了一个打印工作,说这封电子邮件发了。对于正在执行的逻辑,WriteLine并不重要。同样,更新用户界面也是同样的想法。想象这里有一些代码绘制一些像素或一种形式,你知道,向HTML输出发送一些东西,它的逻辑责任可能不属于我们的Appointment实体,我们会谈论这个,但我们在这里用一个注解和一个WriteLine来模拟它发生了。、
朱莉,你和我都同意我们的Appointment不适合用来发送电子邮件和更新用户界面的逻辑,对吧?正确的。这些都是网络的东西,而且appointment(预约)甚至都不需要知道基础设施中对于一个预约的新建都会相应的发什么什么事。完全正确。如果我们要重构这个而不用使用任何事件,我们可以做的是把这个发送邮件以及更新用户界面层的代码放到一个Utils方法中,让这段appointment工厂方法的代码更小,更少的责任。我们可以将代码进一步提取到另一个类中,并将该类放入我们的基础设层的施项目中,这样它就是邮件发送的地方。对吧。然后我们就可以调用它,是的,完全正确,或者通过一个接口注入,但是,如果这个Create方法(就是上图的那个静态的工厂方法)必须了解它以及调用它,那么你还是会有这个问题,就是你创建appointment仍然需要知道需要发生的一切事情,包括发送Email和返回HTML以数据—也许还需要更多的责任。完全正确。有一个原则叫做好莱坞原则,即:你不需要你调用我,我来调用你的(现在我从来没有听说过这个原则~~)。棒极了。它与依赖性倒置原则(DI)密切相关,而不是我们之前的的想法----appointment类,调用发送一封电子邮件,更新UI,而是让这些事情来调用我们的系统或者是反向依赖,而我们需要做的是触发一个事件,他会被任何对该事件感兴趣的处理器所接受并进行自己的行为处理。只要我们的部署启动,当有类似于Appointment创建的消息被发出,那么感兴趣的接收方就会对该事件作出响应式的处理。
现在让我们来看看,如果我们使用一些事件,这个程序是什么样子的。这是相同的应用程序,当它运行时,你会看到它的输出几乎相同。我稍微修改了一下这样我们就能看到不同消息来源的来源因为我想要指出其中一些来自于email的处理程序,而有的来源于UI处理,还有的是来自于数据库,当代码运行时,使这些操作所发生的地方更加清晰。

领域驱动视频(六)

你可以看到它的工作原理是一样的,但是让我们看一下Appointment 类,看看它的不同之处。如果我们查看Create事件,您将看到它没有任何调用来发送电子邮件或更新UI。相反,这里只有这么一行代码,即产生一个AppointmentCreated事件,它会实例化一个新的事件,并将我们所创建的appointment传递进事件中了,这对于这个静态工厂方法而言是很有意义的,因为该工厂方法的职责只是创建一个预约,而不应该包含其他的职责。

领域驱动视频(六)

事件的发生是很重要的,所以我们希望能通过测试发现领域事件所带来的好处。这是Udi Dahan他倡导的方法,他本人也是他主张进行领域活动,当然还有很多其他的方法,你可以做,但在这种情况下,我们可以去这单元测试很容易,简单地将这个事件注册到我们的单元测试中,调用那个创建方法,然后验证我们在这里得到的这个回调实际上发生了。如果我们运行这个测试,您将看到它通过。

领域驱动视频(六)

领域驱动视频(六)

现在我将向您展示这是如何工作的魔法,然后我们再来看一个在我们兽医调度中的例子,只是,我们定义一组接口,所以我们有一个IDomainEvent接口,它只是用来说事件发生的时间,我们有一个IHandle接口,它可以处理特定类型的事件,而且它只是有一个Handler方法。现在发送Email以及向用户界面层返回HTML的逻辑都落在了这个Handler方法上啦。

领域驱动视频(六)

例如,您可以看到NotifyUIAppointmentConfirmed 处理器这里有一个handler方法,这里有这么一行代码,只是在控制台做了个打印,但是在实际情况中我们需要在这里写的是实际的执行逻辑。

领域驱动视频(六)

让我们来看看这些事件本身。我们有一个AppointmentCreated的事件,在这种情况下,你可以看到我们在这里添加了更多的逻辑,你知道我们需要知道什么才能对一个AppointmentCreated事件做出反应?我们需要Appointment(预约),所以我们传递进来一个Appointment(预约)。此外,这个接口有dateCreated,因此我们还会传递一个Date数据。

领域驱动视频(六)

领域驱动视频(六)

进行领域事件的注册并确保它们被调用的类是DomainEvents类。再一次,这是由Udi Dahan写的,你可以看到它不是很长,大约50行代码,只有三个方法,你可以注册一个事件,你可以清除所有的回调,当你在做测试这是很有用的,然后你可以选择产生(Raise)一个事件,然后将经过并找到每个处理器(Handler),并调用它的handler方法。
你不必担心构造所有这些逻辑,我们也可以从中受益。是的。我喜欢这种方法的好处是它不会太严重地污染我们的代码。如果我们回顾一下实际的领域模型,它非常干净。对吧?我们只有一行代码来生成这些事件,标识发生的事情。
事件本身非常简单,处理程序Handler也非常简单,所以一切都很简单,它实现了单一职责的原则,这也意味着它很容易测试,也很容易理解。

领域驱动视频(六)

最后一部分,在本特例中,我们使用的是一个IoC容器,如果我们跳回到主程序类,您将看到这个InitIoC调用,所有这些都使用我们的容器,在这个例子中,它其实是一个StructureMap,这里有一行代码真正地做了这项工作,就是这一行,说, ConnectImplementationsToTypesClosing方法检索的的类型是IHandle的子类,这样它就可以知道在领域中所有的实现了IHandler接口的类,当事件触发需要被处理器处理的时候,他们会说“嘿,我处理某指定的类型的事件”,那么就可以准确的定位到处理该事件的处理器位置,然后使用该特定事件的详细信息调用它的handler方法。
不要太担心这里的控制反转,我们只是想让你们看到拼图是如何组合在一起的,但是有一门非常棒的关于控制反转的课程,是由约翰·索姆斯(John Sommes)教授的,在这里,你可以看到更多的东西。你可以看到,这开始变得有点复杂了,如果你以前从未见过这个,别过分担心,这不过仅仅是在组织领域事件上。一旦你做了几次,特别是Udi的DomainEvents类,它确实会变得容易很多,这就是为什么我们想要在这个控制台应用程序中显示这个的原因,这里的东西都是简化的。现在让我们看一下兽医预约调度应用程序,看看我们如何在其中实现领域事件。

6.5 我们应用中的领域事件

领域驱动视频(六)

现在让我们来看看我们如何在我们一直在使用的兽医调度应用程序中使用领域事件。在我们的Appointment类中,当预约的状态发生了变化,我们会产生一些相应的事件,因此,如果我们向下滚动,查看UpdateRoom方法,你会看到它产生了一个appointmentUpdatedEvent 事件,同样的,updateTime方法则产生了一个appointmentUpdatedEvent事件。最后,当我们confirm方法这里,你可以看到它生成了一个不同的事件,这个事件叫做appointmentConfirmedEvent. 
UpdateRoom 和updateTime会生成相同事件,而conform 则触发了另外的一种事件,所以针对appointmentUpdatedEvent事件相应作出响应的所有事情,对于无论是房间更新还是时间更新都是一样的逻辑。让我们看一看这个AppointmentUpdatedEvent ,这和我们在简单的控制台应用程序中看到的类似。

领域驱动视频(六)

我们传递了一个appointment对象,但在这里,我们不需要传递一个Date数据进来,我们只是说它现在正在发生,所以我们将在构造函数中设置这个值。现在让我们来看看这些事件使用的一些方法。

领域驱动视频(六)

首先,让我们再来看看Schedule 和Schedule again,是我们的聚合根,Schedule 将使用这些事件,它实际上是将自己注册为这个AppointmentUpdatedEvent的处理程序。换句话说,这行代码的意思是“嘿,如果有任何事情触发了AppointmentUpdatedEvent ,那么就交给我,好吧??”。这就是好莱坞模式的代理者---他们没有调用我,我会自己去调用那家伙。如果某事发生更新了事件,那么就和我谈谈吧,我知道应该做什么。这行代码所做的是说,每当发生AppointmentUpdatedEvent我要你调用handler方法,如果我们向下滚动到这里我们会发现底部的Handler方法简单地调用MarkConflictingAppointments ,这样做是有必要的部分原因是因为我们做出的设计决定有这样一种关系。因为Schedule 是我们的聚合根,它自然也会有appointment成员属性,因此,它可以在任何需要的时候浏览和查看预约和编辑修改预约。。

领域驱动视频(六)

然而,当Appointment中发生了某些事件,但是这个appointment是没有办法找到并调用这个Scheduler的,因为appointment是没有任何与Schedule的导航属性的,所以是无法获取到Scheduler的,他只是知道一个SchedulerId,为了让它得到一个Scheduler的引用,我们需要它获取一个存储库并将这个Scheduler从存储库中拉出来,这就不是我们想要在我们的领域模型内部进行的逻辑啦。对,通过使用DomainEvents。将这些连接到导航属性不存在的地方注册进来。你还可以做的另一件事很酷的是你也将应用中其他部分的事件注册进来,我们将在一秒钟内看到它是如何工作的。让我们继续运行这个代码,看看当我们更改预约的时间时,这些断点是如何发生的。
(省略掉讲师在用户界面上更改预约事件)。。。

领域驱动视频(六)

首先触发的断点是调用DomainEvents.Register方法这一处,见上图。因为在我们的代码中,首先要发生的事情之一是我们要创建一个Scheduler,当它重新填充这个特定页面的后端时,要获取所有这些预约。因此这里就有了register。

领域驱动视频(六)

如果我们跳过去,接下来我们将看到,我们会更新这个预约的时间,我们也知道它会生成这个AppointmentUpdatedEvent,你看,如果我们过去的这一个,跳转到下一个断点,我们会发现,我们现在正在Scheduler的Hnadler方法中,正在检查预约冲突。

领域驱动视频(六)

在这种情况下,没有冲突,一切只是顺利地通过啦。你为什么不改变一下史蒂夫呢我们要在这里触发冲突的代码。当然。所以我们想要系统做的一件有趣的事情是,每当发生冲突的时候,就通知用户,在这个案例中,冲突就是我们在两个不同的房间里同时安排了相同的动物。
如果我们在上午11点的时候在2号Room上有sample以及它的宠物Julia,我们想在同一时间把他们加到1号Room上(打字),我们应该看到这将是一场冲突,让我快速地通过这些断点。这将由你现在可以看到的红圈显示出冲突。如果我们把他们中的一个移出那个位置,比方说如果我们把2号Room的时间安排在10点30分,我们会再次击中这些事件,最终结果将是他们不再处于冲突中。另一件很酷的事情是:你可以用这些事件来展示这些红色的线还有一些其他的事情是通过在UI层上捕捉这些事件。

领域驱动视频(六)

如果我吧Sampson重新放回11点,我们会在右下方得到一个警告(上图我已经勾画出来了)因为我不在断点的中间告诉我们他被更新了每次我改变这个约会我们都会收到另一个警告。对吧?然后如果我把它拖到另一个地方,你会看到我们还有一个警告,但是红圈消失了。
发生这种事情的我们获取到了一个时间处理器,这个处理器是在我们的项目中的,但是它所处理到的是我们之前相同的事件,即:AppointmentUpdatedEvent事件。而这个处理器实现了IHandle 接口,这意味着我们可以在我们的IOR容器中获取到这个实现类的实例。在这个例子中,StructureMap会在事件发生的任何时候,来调用这个处理器的handler方法。我们这里没有像Scheduler聚合根那样去进行注册,原因是我实现了IHandler,所以会通过自动扫描来完成注册的任务,所以在Scheduler找那个没有被界面逻辑污染。你知道,我们的实体也是一个事件处理器,只是我们需要手动的注册罢了(参考Scheduler中的事件处理器)。

领域驱动视频(六)

在UI级别,我们希望能够为任何东西添加事件处理程序,以查看我们希望在UI级别或应用程序的其他区域对它们做出响应时是否能够做些什么。也许我们想发送电子邮件,也许我们想要将某样东西记录到日志文件中。不管它是什么,我们都可以在任何地方添加处理器。所以现在我们有两个分离的处理器响应相同的事件,我这里说两个分离的处理器,是因为一个是纯后台的,服务端有针对AppointmentUpdatedEvent事件做出相应的相应的代码,也就是我们之前看到的Schedule 聚合根所做的工作。然后,我们又看到针对UI的处理器。在这种情况下,我们与浏览器对话的方式是通过SignalR,这就是为什么这是在hub文件夹中因为这是SignalR中心所在的位置。SignalR被内置到ASP中,允许你有两个方向你的客户端和服务器之间的通信,所以浏览器中的Javascript代码可以直接调用服务器,这并不是最有趣的,更有趣的是,您的服务器可以直接调用的客户端Javascript代码,这就是我们所做的这些警报。是啊,SignalR真是太好了。
这就是DDD的设置,它真正利用了这类功能。如果你想了解更多关于SignalR的知识,当然你可以在这里找到一个很好的课程。好的,让我们回顾一下Appointment。我们可以看到,我们通过限制发生在某些变化时发生时候需要做的事情来保持这个实体非常干净,然后其他负责处理这些事件的事情发生在应用程序的其他地方。有一点逻辑说,当我们更新时间的时候我们也想要产生这个事件,你看到所有其他的魔法发生,Scheduler将会检查我们的冲突,并且通过singleR来交给前台展示,当然你还可以为AppointmentUpdatedEvent.事件创造更多的响应处理器。而这些都不需要让我们的Appointment类知道。它需要做的只是说,嘿,我更新了。好的,记住这是一个基本的课程,所以我们知道在这个过程中没有很多真正的商业逻辑,但这是为了让它保持在你可以理解的一个可管理的大小。在真实的应用程序中与真正的复杂性会有更多,当事情发生变化时能够封装,这样你就可以分离出逻辑和单独测试它将使它更容易为你控制您的应用程序的复杂性。当然,这就是为什么我们在这里使用领域驱动设计的原因?完全正确。

6.6 领域事件边界

关于域事件,最后要记住的一点是它们最终将被消费。您刚刚看到,我们将域事件定义为核心域模型的一部分。防止这些域事件变得臃肿的一种方法是确保它们只拥有特定消费者所需的信息,而这些信息通常只是在同一个领域模型中。当您的有界上下文需要暴露超出其边界的事件时,您可能希望将您的领域事件转换为某种跨领域类型。这是因为您的领域事件可能在事件本身中没有包含足够的信息,以便外部系统有效地执行它。在我们的示例中,如果我们要公开一个包含Appointment的事件(注:在源码中发现,老师的代码确实以Appointment实体对象导航属性的方式来构建的事件),它只包括医生、病人、客户端、房间和预约类型的id,因为这是我们定义预约的方式。在最坏的情况下,这些id会在我们的有限上下文之外毫无意义,而且,最好的情况是,外部系统需要额外的请求来获取这些其他对象的详细信息,这将是非常低效的。为了避免这种情况,我们可以处理领域内的领域事件。

领域驱动视频(六)

例如,在这样的服务中,通过一些消息基础设施像外部系统暴露了一个不同的事件。在这个示例中,您可以看到,我们正在创建一个使用动态类型的自定义事件,我们将用预约的实际细节来填充它。然后,我们将发布此消息用于外部消费。可以想象,在获取Client的name、Patient的name和Appointment 的类型等信息这部分中(上图中见注解,作者并没写),我们使用的是存储库来检索,或者类似的东西从我们的领域中提取信息。当我们的核心域产生的事件是针对于UI展示的话,那么这么做也是有意义的。在我们刚刚完成的示例中,我们直接在UI中编写了一个领域事件处理程序。是可以正常运行的,很明显,但是如果你发现这使得UI离你的领域太近或如果UI事件处理程序所需要的信息超越你的领域事件本身,您可能想要提出一个定制的事件,专门设计来为您的UI层提供它所需要的信息。你可以用我们刚刚展示的跨系统事件的方法来做这个。

6.7 防腐层

在这个模块中,我们要讨论的最后一个主题是防腐层。顾名思义,防腐层有助于防止领域模型中的腐败。正确的。就像超级英雄帮助打击腐败一样,当你的模型需要与其他系统或有界的环境进行交互时,这些层为你的模型提供了一种安全感。
返回到我们的IDDD图谱,您可以看到,防腐层作为上下文映射的一部分,被用来转换和隔离,在有界上下文和外部系统之间映射。当您的系统需要与其他系统进行通信时,尤其是那些没有编写或建模的遗留应用程序,以及您当前的系统,您需要注意不要让这些系统的假设和设计决策流进您的模型中。例如,如果其他系统模型包括一个customer,即使该客户引用相同的实际业务客户,它很可能会以不同于您系统中的客户的方式建模。最好有一个可以转换其他系统模型的层。在DDD,这是防腐层的工作。正确的。就像我们一开始所说的,甚至其他限界上下文在您自己的系统可能是不同的,应该有一个反腐败层来保护从一个到另一个这两个截然不同的模型,当然,遗留应用程序经常使用来自新系统的非常不同的模型。
然而,防腐层并不是一种设计模式,它通常由几种设计模式组成。该层的工作仅仅是在外系统模型和您自己的系统之间进行转换。除了翻译对象本身之外,反腐败层还可以清理您必须与其他系统通信的方式。
它可以提供一个facade来简化API或适配器,以使外部系统以一种已知的方式运行。在Pluralsight的设计模式库中,您可以了解更多关于这些设计模式的信息。我们通常最关心的是在与遗留系统进行通信时进行反腐败。埃里克·埃文斯注意到这一点很重要“即使其他系统设计得很好,也不是基于与客户端相同的模型。而其他的系统通常都设计得不好”。
因为这是基础课程,我们不会深入地挖掘反腐败层,因为他们可以相当复杂,以及定制的每个场景,但这里有一个例子的结构,来自埃里克埃文的书展示一个反腐败层如何将你美丽的系统与右边的一个不那么美丽的系统连接起来。

领域驱动视频(六)

我真的很喜欢这个图表,我想埃里克把它放在一起很有趣。天哪,你对史蒂夫有什么印象?当然,在中间,你可以看到反腐败层是如何使用一个facade和一些adapter的,但在右边,它保护我们免受一个大的,复杂的界面,一些混乱的类,以及一些我们甚至不想知道的东西的影响。当然,你自己的系统是由一个优雅的类组成的,一个非常富有表现力的类,当然,甚至是更优秀的东西组成的,甚至是一些我们也应该重构的东西。为了使你的系统免受其他系统的影响,你需要做的就是把你的系统放在这个层里面,这样你就可以简化你与其他系统的交互,确保他们的领域决策不会影响到你。

6.8 术语

领域驱动视频(六)

我们已经在这个模块中介绍了一些新的主题,还有一些新的术语,我们想要回顾。
域事件实际上是一种类型的对象,代表域内发生的事情,系统的其他部分可能会对去感兴趣,并将行为绑定到该事件上,这是一个很好的方法让你系统解耦和让你单个对象更简单,因为他们不必知道所有的行为可能发生当它发生。
我们也提到了好莱坞的原则,可以总结为:“别调用给我们,我们会调用你的。”依赖对象不必担心与控制对象通信,比如聚合根。利用领域事件,我们可以确保控制对象能够调用依赖对象,因此,我们设置我们的Scheduler实体在需要更改的时候从预约中接收回调,当然这可能会潜在着有冲突的属性值。
控制反转通常被称为IoC,它是一种模式,可以让我们构建应用程序不调用我们的代码,我们将去调用的模式。您仍然需要使用IoC容器结构映射来协调对事件处理程序的调用,并基于我们定义的领域事件应该触发的位置,并且我们还向您指出了更多的关于Pluralsight的深度IoC课程。
最后,我们看了反腐败层,它可以用来确保我们工作的模型不会被其他系统的猫O型或者决策而污染。因此,我们将反腐败层放在适当的位置,以保护我们的模型免受其他系统或有界环境的影响。

6.9 资料

领域驱动视频(六)