现在我们需要将子问题空间与我们的解决方案设计对齐,我们需要形成一个解决方案空间。DDD术语中的解决方案空间也称为有界上下文,并且最好将一个问题空间/子域与一个解空间/有界上下文对齐。
构建模块
领域驱动设计的构建模块分为战术和战略模式。我写了一篇关于DDD构建块的文章,所以如果你想深入了解,请访问这篇文章。
请注意,以下架构模式和类图不依赖于技术。该解决方案可以使用Java SE / EE,C#甚至JavaScript实现。无关紧要,我们可以使用每种目标技术存档相同的好处。
新的架构总览
让我们看下新的架构
好的,这里发生了什么?现在每个已识别的子域都有有界上下文。有界上下文是孤立的,彼此一无所知。它们仅由一组常见类型粘合在一起,如UserId,ProjectId和CustomerId。在DDD中,这组常见类型称为“共享内核”。我们还可以看到什么是“核心领域”的一部分,什么不是。如果有界上下文是我们试图解决的问题的一部分,并且不能被另一个系统替换,那么它就是“核心域”的一部分。如果它可以被另一个系统替换,那么它就是“通用子域”。“身份和访问管理”上下文是“通用子域”,因为它可以由现有的IAM解决方案替换,例如Active Directory或其他。
我们将一组战术和战略模式应用于模型。这些模式有助于我们构建更好的模型,提高容错能力并提高可维护性。
在每个有界上下文中都有聚合和值对象。聚合是对象层次结构,但只能从聚合外部访问层次结构的根。聚合处理业务常量。对对象树的每次访问都必须通过Aggregate,而不是通过其中的一个元素。这大大增加了封装性。
Aggregates和Entites是我们模型中具有唯一ID的东西。值对象不是事物,它们是值或度量,如UserId。值对象被设计为不可变的,它们不能改变它们的状态。每个状态更改方法都返回值Object的新实例。这有助于我们消除不必要的副作用。
设计行为
让我们设计一些行为,“Freelancer迁移到新位置”用例。在没有DDD的情况下,我们可以创建一个简单的POJO,如下所示:
我们可以通过调用实例的setter来更改Freelancer的名称。可是等等!我们的用例在哪里?可以从其他地方调用setter。实施基于角色的安全性可能会变得很麻烦。因为我们在调用setter时没有调用上下文。此外,这个模型中还缺少一个概念,即地址。它只是通过Freelancer类的简单属性以非常隐式的方式建模。
通过应用域驱动设计,我们得到以下结果:
这要好得多。现在有一个显式的Address类,它封装了整个地址状态。现在,地址更改用例显式建模为Freelancer聚合提供的moveTo()方法。我们只能使用此方法更改Freelancer状态。当然,这种方法可以通过某种安全模型轻松保护。
完整的用例和持久性
好的,我们继续模仿“自由职业者迁移到新位置”的用例。首先,我们需要为Freelancer Aggregate提供一种存储空间。DDD将这样的存储称为存储库。使用存储库,我们可以按名称搜索Freelancer,通过Id加载现有的Freelancer,从存储中删除它或向存储添加新的Freelancer。根据经验,每种类型的聚合都应该有一个存储库。请注意,存储库是业务术语中描述的接口。我们将在下一章讨论实现。
下图显示了建模的用例。你会看到一些新的工件。首先是用户界面,我们的域模型的客户端。客户端可以是一切,从JSF 2.0前端到SOAP Web服务或REST资源。所以请以一般方式考虑客户。客户端向ApplicationService发送命令。ApplicationService将命令转换为域模型用例调用。因此,FreelancerApplicationService将从FreelancerRepository加载Freelancer Aggregate,并在Freelancer Aggregate上调用moveTo()操作。FreelancerApplicationService也构成了事务边界。每次调用都会导致新的事务。基于角色的安全性也可以使用FreelancerApplicationService实现。将事务控制保留在域模型之外始终是一个很好的选择。事务控制更多是技术问题,而不是业务问题,因此不应在域模型中实现。