在前面的章节中,我们讨论了 Web、应用程序、域和持久层以及每个层对实现用例的贡献。 然而,我们几乎没有触及每层模型之间映射的可怕且无处不在的主题。我敢打赌,您曾经
讨论过是否在两个层中使用相同的模型以避免实现映射器。
争论可能是这样的:
专业地图开发人员:
如果我们不在层之间映射,我们必须在两个层中使用相同的模型,这意味着层将紧密耦合!
Contra-Mapping 开发者:
但是,如果我们确实在层之间进行映射,我们会生成大量样板代码,这对于许多用例来说都是多余的,因为它们只执行 CRUD 并且跨层具有相同的模型!
正如此类讨论中经常发生的情况一样,争论双方都有道理。让我们讨论一些映射策略及其
优缺点,看看我们是否可以帮助这些开发人员做出决定。
“无映射”策略
第一个策略实际上根本不是映射。
图 22 显示了与我们的 BuckPal 示例应用程序中的“发送资金”用例相关的组件。
在Web层,Web控制器调用SendMoneyUseCase接口来执行用例。该接口采用 Account 对象作为参数。这意味着 Web 层和应用程序层都需要访问 Account 类 - 两者都使用相同的模型。 在应用程序的另一端,我们在持久性和应用程序层之间具有相同的关系。
由于所有层都使用相同的模型,因此我们不需要在它们之间实现映射。但这样的设计会带来什么后果呢? Web 层和持久层可能对其模型有特殊要求。例如,如果我们的 Web 层通过 REST 公开其
模型,则模型类可能需要一些注释来定义如何将某些字段序列化为 JSON。如果我们使用 ORM 框架,持久层也是如此,这可能需要一些定义数据库映射的注释。
在示例中,所有这些特殊要求都必须在 Account 域模型类中处理,即使域和应用程序层对它们不感兴趣。这违反了单一职责原则,因为由于 Web、应用程序和持久层的原因,必须更改 Account 类。
除了技术要求之外,每一层可能都需要帐户上的某些自定义字段类。这可能会导致域模型支离破碎,某些字段仅在一层中相关。
但这是否意味着我们永远不应该实施“无映射”策略?当然不是。
尽管“无映射”策略可能感觉很脏,但它可能是完全有效的。 考虑一个简单的 CRUD 用例。我们真的需要将相同的字段从 Web 模型映射到域模型以及从域模型映射到持久性模型吗?我想说我们不这样做。
那么领域模型上的 JSON 或 ORM 注释又如何呢?他们真的打扰我们吗?即使如果持久层中发生某些变化,我们必须更改域模型中的一两个注释,那又怎样呢?
只要所有层都需要完全相同的结构中的完全相同的信息,“无映射”策略就是一个完全有效的选择。
然而,一旦我们处理应用程序或域层中的网络或持久性问题(也许除了注释之外),我们就应该转向另一种映射策略。
从这里的介绍中可以给两位开发人员一个教训:即使我们过去已经决定了某种映射策略,但我们可以在以后更改它。
根据我的经验,许多用例都是从简单的 CRUD 用例开始的。后来,它们可能会成长为一个成熟的业务用例,具有丰富的行为和验证,从而证明更昂贵的映射策略是合理的。或者他们可能永远保持其 CRUD 状态,在这种情况下,我们很高兴我们没有投资不同的映射策略。
“双向”映射策略
每个层都有自己的模型的映射策略就是我所说的“双向”映射策略,如图 23 所示。
每一层都有自己的模型,该模型可能具有与领域模型完全不同的结构。
Web 层将 Web 模型映射到传入端口期望的输入模型。它还将传入端口返回的域对象映射回 Web 模型。 持久层负责传出端口使用的域模型与持久模型之间的类似映射。 两个图层都在两个方向上映射,因此称为“双向”映射。 每个层都有自己的模型,每个层都可以修改自己的模型而不影响其他层(只要内容不变)。
Web 模型可以具有允许最佳地呈现数据的结构。领域模型可以具有最适合实现用例的结构。并且持久化模型可以具有 OR-Mapper 将对象持久化到数据库所需的结构。 这种映射策略还可以产生一个干净的域模型,不会被网络或持久性问题弄脏。它不包含 JSON 或 ORM 映射注释。满足单一职责原则。
“双向”映射的另一个好处是,它是继“无映射”策略之后概念上最简单的映射策略。映射职责很明确:外层/适配器映射到内层的模型并映射回来。内层只知道自己的模型,可以专注于领域逻辑而不是映射。 与所有映射策略一样,“双向”映射也有其缺点。
首先,它通常会出现大量样板代码。即使我们使用众多映射框架之一来减少代码量,实现模型之间的映射通常也会占用我们很大一部分时间。部分原因是调试映射逻辑很痛苦,尤其是在使用将其内部工作隐藏在通用代码和反射层后面的映射框架时。
另一个缺点是领域模型用于跨层边界通信。传入端口和传出端口使用域对象作为输入参数和返回值。这使得它们很容易受到外层需求触发的变化的影响,而领域模型最好只根据领域逻辑的需求而发展。 就像“无映射”策略一样,“双向”映射策略也不是灵丹妙药。然而,在许多项目中,这种映射被认为是我们必须在整个代码库中遵守的神圣法则,即使对于最简单的 CRUD 用例也是如此。这不必要地减慢了开发速度。 任何映射策略都不应被视为铁律。相反,我们应该针对每个用例做出决定。
“完整”映射策略
另一种映射策略是我所说的“完整”映射策略,如图 24 所示。
这种映射策略为每个操作引入了单独的输入和输出模型。我们不使用域模型跨层边界进行通信 ,而是使用特定于每个操作的模型 , 例如 SendMoneyCommand , 它充当图中 SendMoneyUseCase 端口的输入模型。我们可以将这些模型称为“命令”、“请求”或类似名称。
Web层负责将其输入映射到应用层的命令对象。这样的命令使得与应用层的接口非常明确,几乎没有解释的空间。每个用例都有自己的命令以及自己的字段和验证。无需猜测哪些字段应该填写,哪些字段最好留空,因为否则它们会触发我们当前用例不需要的验证。
然后,应用程序层负责将命令对象映射到根据用例修改域模型所需的任何内容。
当然,从一层映射到许多不同的命令比单个 Web 模型和域模型之间的映射需要更多的映射代码。然而,与必须处理许多用例而不是仅一个用例的需求的映射相比,这种映射更容易实现和维护。
我不提倡将这种映射策略作为全局模式。它在 Web 层(或任何其他传入适配器)和应用程序层之间最好地发挥其优势,以清楚地划分应用程序的状态修改用例。由于映射开销,我不会在应用程序和持久层之间使用它。
另外,在某些情况下,我会将这种映射限制为操作的输入模型,并简单地使用域对象作为输出模型。例如,SendMoneyUseCase 然后可能会返回一个包含更新后余额的 Account 对象。 这表明映射策略可以而且应该混合。映射策略不需要成为所有层的全局规则。
“单向”映射策略
还有另一种映射策略,它具有另一组优点和缺点:图 25 中描绘的“单向”策略。
在此策略中,所有层中的模型都实现相同的接口,该接口通过提供相关属性的 getter 方法来封装域模型的状态。
领域模型本身可以实现丰富的行为,我们可以从应用程序层内的服务访问这些行为。如果我们想将域对象传递到外层,我们可以在不映射的情况下这样做,因为域对象实现了传入和传出端口所期望的状态接口。
然后,外层可以决定是否可以使用该接口,或者是否需要将其映射到自己的模型中。他们不会无意中修改域对象的状态,因为修改行为不是由状态接口公开的。
我们从外层传递到应用层的对象也实现了这个状态接口。然后,应用程序层必须将其映射到真实的域模型中,以便访问其行为。这种映射非常适合工厂的 DDD 概念。 DDD 中的工厂负责从某种状态重构域对象,这正是我们正在做的事情。
映射职责很明确:如果一个层从另一个层接收到一个对象,我们将其映射到该层可以使用的对象。因此,每一层仅映射一种方式,这就是“单向”映射策略。 然而,由于映射跨层分布,该策略在概念上比其他策略更困难。
如果各层的模型相似,则该策略可以最好地发挥其优势。例如,对于只读操作,Web 层可能根本不需要映射到自己的模型中,因为状态接口提供了它所需的所有信息。
何时使用哪种映射策略?
这是一个价值百万美元的问题,不是吗?
答案是常见的、令人不满意的:“这要看情况”。
由于每种映射策略都有不同的优点和缺点,我们应该抵制将单一策略定义为整个代码库的硬性全局规则的冲动。这违背了我们的直觉,因为在同一代码库中混合模式感觉不整洁。但是,仅仅为了满足我们的整洁感而故意选择一种不是最适合某项工作的策略,是不负责任的、简单的。
此外,随着软件随着时间的推移而发展,昨天最适合工作的策略今天可能不再是最适合工作的策略。无论如何,我们可以从一个简单的策略开始,让我们能够快速改进代码,然后转向更复杂的策略,帮助我们更好地解耦各层,而不是从固定的映射策略开始并随着时间的推移保持它。
为了决定何时使用哪种策略,我们需要在团队内就一套指导方针达成一致。这些指南应该回答在哪种情况下哪种映射策略应该是首选的问题。他们还应该回答为什么他们是首选,以便我们能够评估这些原因在一段时间后是否仍然适用。
例如,我们可以为修改用例定义与查询不同的映射指南。此外,我们可能希望在 Web 和应用程序层之间以及应用程序和持久层之间使用不同的映射策略。
这些情况的指导方针可能如下所示:
如果我们正在处理修改用例,“完整映射”策略是 Web 层和应用程序层之间的首选,以便将用例彼此解耦。这为我们提供了明确的每个用例验证规则,并且我们不必处理在特定用例中不需要的字段。
如果我们正在处理修改用例,“无映射”策略是应用程序和持久层之间的首选,以便能够快速发展代码而无需映射开销。然而,一旦我们必须处理应用程序层中的持久性问题,我们就会转向“双向”映射策略,以将持久性问题保留在持久层中。
如果我们正在处理查询,“无映射”策略是 Web 层和应用程序层之间以及应用程序层和持久层之间的首选,以便能够快速改进代码而无需映射开销。然而,一旦我们必须处理应用程序层中的 Web 或持久性问题,我们就会分别在 Web 和应用程序层或应用程序层和持久层之间采用“双向”映射策略。
为了成功应用此类指南,它们必须存在于开发人员的脑海中。因此,指南应该作为团队的努力不断讨论和修订。
这如何帮助我构建可维护的软件?
通过传入和传出端口充当应用程序各层之间的看门人,它们定义了各层如何相互通信,从而定义了我们是否以及如何在各层之间进行映射。
通过为每个用例设置窄端口,我们可以为不同的用例选择不同的映射策略,甚至可以随着时间的推移而演变它们而不影响其他用例,从而在特定时间为特定情况选择最佳策略。
与简单地对所有情况使用相同的映射策略相比,根据情况选择映射策略肯定更困难,并且需要更多的沟通,但它会为团队奖励一个代码库,该代码库只做它需要做的事情并且更容易维护,只要因为映射指南是已知的。