3年前 (2021-04-23)  相关技术 |   1 条评论  567 
文章评分 0 次,平均分 0.0

我以前看过很多IT项目。有的设计得很好,有的设计得很差。基于这些经验,我想写一点关于一个示例项目的内容,我还想展示如何用UML对一个示例项目进行建模,以及如果我们将领域驱动的设计原则应用到模型中会发生什么。

在继续之前,您应该阅读Eric Evans的“域驱动设计”和Vaughn Vernon的“实现域驱动设计”两本书。这个例子大部分都是基于他们的工作,如果你想深入研究领域驱动的设计,他们的书是必读的。

需求

一家公司为其提供车身租赁服务。他们有一些雇员,也有很多自由职业者作为分包商。目前,他们使用Excel表格来管理客户、自由职业者、时间表等等。Excel解决方案的伸缩性不好。它不支持多用户,也不提供安全和审核日志。所以他们决定建立一个新的基于网络的解决方案。以下是核心要求:

  • 必须提供可搜索的自由职业者目录
  • 新的解决方案必须允许存储不同的通信渠道,以联系自由职业者
  • 必须提供可搜索的项目目录
  • 必须提供可搜索的客户目录
  • 必须保留合同下自由职业者的时间表

基于这些需求,开发团队决定使用UML对所有内容进行建模,以获得新解决方案的总体情况。现在让我们看看他们做了什么。

大局

好的,这是他们第一次设计的:

领域驱动设计示例
这很直截了当。有客户,自由职业者,项目和时间表。还有一种用户管理来支持基于角色的安全性。但是等等,这里出了点问题。有一些隐藏得很好的设计缺陷。你能看见他们吗?它们在这里:

  • 它是一个非常大的对象图。如果他们不在这里使用Hibernate/JPA延迟加载,在重载情况下肯定会耗尽内存
  • 为什么用户和角色之间的关联是双向的?
  • ContactType有一些布尔标志来显示它是什么类型的,电子邮件、电话、手机
  • 自由职业者类拥有一个项目列表。这也意味着在不修改freeloper对象的情况下不能添加项目。这可能会导致事务在重载情况下失败,因为可能有多个用户正在为同一个客户添加项目。
  • 联系信息是什么意思?要求中规定了“通信信道”。是一样的吗?

整个模型似乎更像是一种实体关系图,而不是一个软件模型。还有,业务逻辑是什么?团队希望围绕模型创建一些业务服务来存储和检索数据,实体只是由JPA管理的pojo。

一个贫血的领域模型。团队也承认这一点。但解决办法是什么呢?嗯,一位高级团队成员建议使用领域驱动的设计原则来建模解决方案。好的,现在让我们看看DDD如何改进设计。

DDD方式

在深入研究领域驱动设计之前,我们应该先谈谈DDD背后的原理。

DDD背后的一个原则是,通过使用相同的语言来创建相同的理解,从而弥合领域专家和开发人员之间的鸿沟。另一个原则是通过应用面向对象的设计和设计模式来减少复杂性,以避免重新发明轮子。

但什么是域名?领域是一个“知识领域”,例如公司经营的业务。一个领域也被称为“问题空间”,所以我们必须为这个问题设计一个解决方案。

好的,让我们看看要求。我们可以认为有一个“身体租赁”领域,这是完全正确的。但如果我们更深入地研究这个领域,我们会发现一些叫做“子域”的东西。可能有以下子域:

  • 身份和访问管理子域
  • 自由职业者管理子域
  • 客户管理子域
  • 项目管理子域

啊!我们可以把大问题分解成小问题。这可以帮助我们设计更好的解决方案。

分离的域可以很容易地可视化。在DDD术语中,这称为上下文映射,它是任何进一步建模的起点。

领域驱动设计示例
现在我们需要将子域问题空间与我们的解决方案设计对齐,我们需要形成一个解决方案空间。DDD术语中的解空间也称为有界上下文,最好将一个问题空间/子域与一个解空间/有界上下文对齐。

构建模块

领域驱动设计的构建块被划分为战术和战略模式

请注意,以下架构模式和类图与技术无关。这个解决方案可以使用JavaSE/EE、C#甚至JavaScript来实现。没关系,我们可以利用每一项目标技术实现同样的好处。

新的大局

好的,现在让我们来看看域模型的新大图:

领域驱动设计示例

好吧,这里发生了什么?现在,每个已识别的子域都存在有界上下文。有界的上下文是孤立的,它们彼此一无所知。它们只由一组常见类型粘合在一起,如UserIdProjectIdCustomerId。在DDD中,这组常见类型称为“共享内核”。我们还可以看到什么是“核心域”的一部分,什么不是。如果一个有界的上下文是我们试图解决的问题的一部分,并且不能被另一个系统所替代,那么它就是“核心域”的一部分。如果它可以被另一个系统替换,那么它就是一个“通用子域”。“身份和访问管理”上下文是一个“通用子域”,因为它可以被现有的IAM解决方案(如activedirectory或其他内容)替代。

我们对模型应用了一套战术和战略模式。这些模式有助于我们建立更好的模型,提高容错性,并提高可维护性。

在每个有界上下文中都有聚合和值对象。聚合是对象层次结构,但只有层次结构的根可以从聚合外部访问。聚合处理业务不变量。对对象树的每次访问都必须经过聚合,而不是其中的一个元素。这大大增加了封装。

聚合和实体是在我们的模型中具有唯一id的对象。值对象不是东西,它们是值或度量,就像用户标识一样。值对象被设计成不可变的,它们不能改变它们的状态。每个状态更改方法都返回值对象的一个新实例。这有助于我们消除不必要的副作用。

设计行为

让我们设计一些行为,“自由职业者迁移到新位置”用例。如果不考虑DDD,我们可以创建一个简单的POJO,如下所示:

领域驱动设计示例

我们可以通过调用实例的setter来更改自由职业者的名称。但是等等!我们的用例在哪里?设置者可能从其他地方被叫来。实现基于角色的安全性可能会变得很麻烦。因为我们在调用setter时没有调用上下文。此外,这个模型中还缺少一个概念,地址。它是以一种非常隐式的方式建模的,只需要自由职业者类的简单属性。

通过应用领域驱动设计,我们得到以下结果:

领域驱动设计示例

这样好多了。现在有一个显式地址类,它封装了整个地址状态。地址更改用例现在被显式地建模为由freelator聚合提供的moveTo()方法。我们只能用这个方法来改变自由职业者的状态。当然,这种方法很容易被某种安全模型所保护。

完整的用例和持久性

现在有一个显式地址类,它封装了整个地址状态。地址更改用例现在被显式地建模为由freelator聚合提供的moveTo()方法。我们只能用这个方法来改变自由职业者的状态。当然,这种方法很容易被某种安全模型所保护。完整的用例和持久性好的,我们继续建模“自由职业者搬到了新的地方” 用例。首先,我们需要一种存储我们的自由职业者聚合。

首先,我们需要一种存储我们的自由职业者聚合。DDD将这种存储称为存储库。使用存储库,我们可以搜索一个自由职业者,例如按姓名,按Id加载一个现有的自由职业者,将其从存储中删除或向存储中添加一个新的自由职业者。根据经验,每种类型的聚合都应该有一个存储库。请注意,存储库是用业务术语描述的接口。我们将在下一章讨论实现。下图显示了建模的用例。您将看到一些新的工件。首先是用户界面,客户端是我们的领域模型。客户机可以是一切,从jsf2.0前端到soapweb服务或REST资源。所以请以一种普遍的方式来考虑客户。客户端向ApplicationService发送命令。ApplicationService将命令转换为域模型用例调用。

领域驱动设计示例

应用程序体系结构

好的,现在让我们看看应用程序体系结构。对于每个有界上下文,都应该有一个单独的部署单元。这可以是javawar文件或ejbjar。这取决于您的实现技术。我们将有界上下文设计为相互独立,这个设计目标也应该反映在独立的部署单元中。

每个部署单元包含以下部分:

  • 域层
  • 基础结构层
  • 和应用层

域层包含独立于基础结构的域逻辑,正如我们在本例中之前建模的那样。基础结构层提供了依赖于技术的工件,比如基于Hibernate的自由存储实现。应用层充当到具有集成事务控制的业务逻辑的网关。

领域驱动设计示例

使用这种架构风格,我们业务逻辑的域层不依赖任何东西。例如,我们可以将存储库实现从Hibernate更改为JPA,甚至可以将NoSQL实现(例如Riak或MongoDB)更改为JPA,而不影响任何业务逻辑。

领域层

域层包含真正的业务逻辑,但不包含任何特定于基础架构的代码。基础架构层提供了特定于基础设施的实现。域模型的设计应按照CQS(命令查询分离)原则进行描述。可以有查询方法只返回数据而不影响状态,还有命令方法,这些方法影响状态,但不返回任何内容。

应用层

应用程序层从用户界面层接收命令,并将这些命令转换为域层上的用例调用。应用程序层还为业务操作提供事务控制。应用层负责通过中介或数据转换器模式将聚合数据转换为特定于客户端的表示模型。

基础结构层

基础架构层为所有其他层提供与基础设施相关的部分,例如Hibernate或JPA支持的实现。聚合数据可以存储在RDMBS中,如Oracle或MySQL,也可以存储为XML/JSON,甚至可以将GoogleProtocolBuffers序列化对象存储在基于关键值或基于文档的NoSQL引擎中。这取决于您,只要存储提供事务控制并保证一致性。基础结构可以最好地描述为“域模型周围的一切”,因此,如果我们与其他系统交互,数据库、文件系统资源甚至Web服务消费者。

客户端/用户界面层

客户端层使用应用程序服务并调用这些服务的业务逻辑。每次调用都是一个新事务。

客户端层几乎可以是任何东西,从JSF2.0支持Bean作为视图控制器到SOAP web服务端点或RESTfulWeb资源。甚至Swing、AWT或OpenDolphin/JavaFX也可以用于创建用户界面。

上下文集成

考虑到主体租赁领域的以下要求:

  • 只有在没有分配项目时才能删除客户
  • 一旦输入时间表,客户需要计费

同步集成

我们从第一个开始。在这种情况下,客户管理受限上下文需要检查是否有为给定客户注册的项目,然后才能删除客户。这需要两个有界上下文的同步集成。

有很多机会。首先,我们希望上下文彼此独立。那我们怎么处理呢?以下是一个设计,用于客户限定上下文,以与项目管理有界上下文交互:

领域驱动设计示例

有一个新术语:领域服务。什么是领域域服务?领域域服务实现了无法由实体、聚合或ValueObject实现的业务逻辑,因为它不属于该逻辑。例如,如果业务逻辑调用包括跨多个域对象的操作,或者在本例中与另一个有界上下文集成。

ApplicationService调用CustomerServiceDeleteCustomerByd方法。如果给定的CustomerId存在项目,CustomerService通过调用customerExists()来询问ProjectManagementAdapter。只有返回false,客户才从CustomerReportSite中删除。

ProjectManagementAdapter有两种实现,一个是SOAP,另一个是基于REST的。我们可以使用SOAP调用XML封送和使用完整的JAX-WS堆栈的完整web服务操作,或者我们可以使用REST和调用http://example.com/customers/customerId/projects然后获取404(未找到)或20x(Ok)HTTP响应代码。这取决于你,但剩下的一个将不那么复杂,更容易集成,也可以扩展得更好。此外,我们还可以从REST开始,如果需要,可以切换到SOAP。在不影响域层的情况下,很容易更改实现,我们只使用适配器的另一个实现。

在项目管理有界上下文端,有一个ApplicationWebService作为REST资源或SOAP服务公开,实现了通信的服务器部分。此服务或资源委托给ProjectApplication服务,ProjectDomainService委托ProjectDomainService询问是否有为给定CustomerId注册的项目。

领域驱动设计示例

在任何情况下,我们都必须注意交易边界。Web服务或REST资源调用不会立即升级事务,使用XA/两阶段提交会增加复杂性并降低可伸缩性。最好不要实际删除客户,而是将其标记为逻辑删除。在事务失败或并发问题的情况下,很容易将客户恢复到其原始状态。

这里您还可以看到为什么基础结构层位于所有其他层之上。它必须能够委托给它,或者基于在下面的层中定义的接口实现特定于技术的工件。

异步示例

好,现在我们继续一个更复杂的例子。考虑一下这样的要求,即一旦输入了时间表,就需要向客户计费。

这是一个非常有趣的问题。这很有趣,因为它不需要同步调用。账单可以及时寄出,也可以在几个小时后或月末与其他账单一起寄出。或者账单可以由客户的大客户经理或其他什么人来充实,自由职业者的管理环境根本不在乎。

我们如何用DDD模式来建模?这里的关键是短语“一旦时间表是……”,这是我们领域中与业务相关的事件,此类事件可以建模为域事件!

域事件被创建并转发到事件存储区,并存储在那里以供进一步处理。EventStore是绑定上下文部署单元的一部分,在存储中存储事件是在ApplicationService管理的运行事务下完成的。在基础设施方面,有一个计时器将存储的事件转发到最终的消息传递基础设施,例如基于JMS或AMQP的,甚至调用REST资源也可以被视为消息传递。

那我们为什么需要当地的活动商店呢?嗯,消息传递基础设施可能暂时不可用,但这不应影响我们正在运行的有界上下文。因此,当基础设施再次可用时,事件将排队并传递。如果我们将消息传递基础结构直接与事件生产者耦合,那么在基础结构出错的情况下,生产者可能无法发送消息。即使我们使用消息传递,如果出现问题,这可能会对整个基础设施产生连锁反应,这就是我们使用消息传递的原因:系统解耦

以下是自由职业者管理环境的建模方法:

领域驱动设计示例

freelorservice创建一个timesheetented域事件并将其转发到EventStoreEventStore基本上是另一个存储库。然后,JMSMessagingAdapterEventStore获取挂起的事件,并尝试将它们转发到目标消息传递基础结构,直到传递成功。但这种转发是在另一个事务中处理的,例如,可以由计时器触发。

好的,客户管理上下文如何处理事件?其模型如下:

领域驱动设计示例

同样,基础结构层必须位于所有其他层,因为它必须在上下文集成的情况下调用应用程序服务。

这是位于基础结构层的JMSMessageReceiver的来源。MessageReceiver还负责重复事件消除。这可能发生在系统发生故障的情况下,当已经交付的事件被重新交付或出现其他错误时。由于基础结构层位于应用程序层之上,因此它可以调用CustomerApplicationServiceCustomerApplicationService本身调用CustomerServiceCustomerService实现发送账单的业务逻辑。

在此场景中,事务边界位于ApplicationService。我们可以认为JMSMessageReceiver可能会调用CustomerService,并围绕JMS事务进行调用。这也是一个可行的解决办法。

棘手的部分是重复事件消除。这可能发生在基础设施故障或系统中断的情况下。这可以通过给每个事件一个唯一的id来避免,并跟踪哪些id已经被处理。

另一个棘手的部分是事件排序。这取决于消息传递基础结构。如果基础设施支持事件排序,则一切正常。否则,这必须由我们自己来执行。在任何情况下,将事件设计为幂等运算都是一种很好的做法。这意味着每一个事件都可以被多次处理,并且每次都有相同的结果,没有不必要的副作用。

从多个有界上下文或聚合中查询数据

有时我们需要收集分布在多个聚合甚至有界上下文中的数据。这可能是一项艰巨的任务。在一个有界上下文中,我们可以使用专门的数据库视图并使用Hibernate或JPA检索数据,但是在多个有界上下文中分散获取数据可能会导致许多远程方法调用和其他问题;此解决方案可能无法很好地扩展。我们还必须考虑使用视图可能会破坏精心设计的聚合的业务不变性。这是一个我们必须处理的问题!

现在,有什么解决办法?我们可以考虑CQR或命令查询!基本上,我们将模型分为包含业务逻辑的命令模型和用于检索数据的查询模型。因此,对于本例,命令模型将由我们要查询的所有有界上下文和一个查询模型组成,该模型用于查询聚合数据(并经过优化以有效地查询数据)。命令模型和查询模型是使用域事件同步的!一旦在命令模型中触发了业务操作,查询模型就会发出和处理域事件,并更新数据。

使用CQRS,我们可以设计高性能的数据处理系统,并且与商业智能的集成不再是问题。想想看:查询模型基本上可以是一个数据仓库。

小结

我非常喜欢领域驱动设计背后的理念。使用这种技术,即使是非常复杂的领域逻辑也可以很容易地提取和建模。这将带来更好的系统、更好的用户体验以及更可靠和可维护的解决方案。

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1846.html

关于

发表评论

表情 格式
  1. 请问timesheetented域事例创建的时候,怎么跟聚合根timesheet创建联系起来?也就是timesheet创建的时候跟timesheetented域事例创建是否是同一时间?

    liuxinsudi 评论达人 LV.1 2年前 (2022-07-08) [0] [0]

登录

忘记密码 ?

切换登录

注册