我注意到由公司Domain Language和Citerus开发的DDD Sample App使用单个Maven模块,其中每个层作为该模块内的单独Java包。这是已经确立的最佳实践,还是我应该考虑更细粒度的模块布局?
通常模块分离和打包是部署和开发实践的问题。还有谁需要使用这段代码?如果我想改变功能Y,它是否都在X包中?
注意:示例应用程序被打包为单个应用程序,以便作为学习工具轻松消费。但是,以下是一些推荐使用它作为示例并假装它是“真正的”时的建议。我将在真空中对其进行一些假设,以说明的目的,但负责任的DDD实践者会与领域专家和开发团队进行访谈,以验证关于域、上下文边界和这些上下文之间关系的所有假设。你不能独自完成DDD。
第一步是集中精力确保正确建模和定义上下文边界。我不会过多地担心基础架构层,而是会关注域内各种上下文及其模型。示例应用程序中关键的区别在于这些不同上下文之间,该应用程序中有三个上下文:
如果你注意到,它们由根java包明确分隔开来:
层主要是为了促进这些上下文之间的通信,并将基础架构问题与域工作分开,从而使测试更容易进行,域责任更明确。基础架构很重要,但示例应用程序在此处使用XML,在那里使用JMS,在Hibernate中使用更多次,这些都是次要关注点,域建模才是重点。
示例应用程序非常清楚地表明了这种分离,很容易看出哪些是聚合根:
按照聚合根将Java包分组是一种可靠的最佳实践。在货物聚合范围之外,单个Leg没有任何意义;在航程聚合范围之外,计划没有任何意义;在HandingEvent聚合范围之外,HandlingHistory没有任何意义。将领域模型与基础设施隔离并使其可测试是一个好习惯。但是您可能不会将这种解耦延伸到模块级别。说所有领域对象都存在于一个JAR文件中,而所有基础架构都存在于另一个JAR文件中并不是一个规则。开发和版本负担可能会变得痛苦。
关键在于各个上下文模型如何单独/不共用。在预订上下文中,路线是一条行程线路,具有一系列Legs。在路由领域中,它是Graph,以计算机科学中的真正含义为基础,因此该领域可以使用大学算法课程中学过的遍历图算法解决路由问题。
两个上下文,预订和路由,处于紧密的合作关系,它们在两个模型之间维护一个共享接口,包括Edges和Nodes以及Itineraries和Legs。这种模型之间的翻译在ExternalRoutingService中进行管理,那里TransitPath变成了Itinerary。显然,这是一个非常关键的集成点,应该在测试中得到很好的覆盖,并通过持续集成进行管理。
另一个上下文是第三方集成,用于向应用程序报告有关货物状态的HandlingEvents。这通过一种称为发布语言的模式实现。简而言之,我们不关心第三方的货物模型长什么样子,只要他们按照我们定义的发布XML规范HandlingReport为我们报告处理事件即可。
第三方上下文与预订领域之间的关系被称为服从者,他们按照我们定义的规范提交数据,我们不会改变我们的模型使其更容易适应他们。他们需要遵守我们的规范。话虽如此,这只是我对情况的猜测,事实上可能存在一个非常重要的供应商,他们实际上定义了XML模型而不是我们。只有通过对虚构团队的采访才能真正表征这一点。
总的来说,将与聚合相关的所有类(例如在同一个包中)紧密分组。明确定义上下文边界,并确保有清晰的集成点,定义上下文伙伴关系、共享内核、发布语言、开放主机服务、遵从者等关系。
基于这一点,在示例中,我们可以将各种上下文封装到单独的maven模块中,例如货物预订、路径查找和事件处理聚合。如果在开发方法论和团队组织的实践情况下有意义,并且只有在这种情况下才这样做。
正确设置上下文边界。正确地将聚合定义为良好垂直性质。减少耦合以清晰定义接口。
在您的上下文中查找模块。它们是最自然的单独模块的候选项,而分离可能有助于更严格地执行和记录上下文边界。但是,像许多软件设计一样,这不是一个硬性规则,这真的取决于具体情况。我可以设想并见过/编写了应用程序,它们具有不同的写入模型和读取模型(考虑规范化和反规范化,例如报告),每个上下文都可能仍然打包在单个模块中。
另一个要点是小心共享聚合根,这是DDD共享核心模式,应该非常谨慎地使用,因为它可能很快演变成大型混乱的域模型,无法满足任何上下文的需求。请注意,示例应用程序不在RoutingService和BookingService之间共享模型。将领域的所有聚合根放在单个模块中可能会无意中鼓励这种实践。