“无映射”策略
了解“无映射”策略及其适用场景和实现方法。
简介
在前面的章节中,我们讨论了 Web 层、应用层、领域层和持久化层,以及每一层在实现用例时所承担的职责。
然而,我们几乎没有触及一个令人头疼却又无处不在的话题:各层模型之间的映射问题。我敢打赌,你一定曾经参与过这样的讨论——是否应该在多个层之间使用相同的模型,以避免编写额外的映射代码。
这种争论通常如下所示:
支持映射的开发者:
如果我们不在层之间进行映射,就必须在两个层中使用同一个模型,这会导致层与层之间高度耦合!
反对映射的开发者:
但如果我们 确实 在层之间做映射,就会产生大量样板代码,对于许多用例来说这是过度设计,因为它们只是执行 #key# CRUD:创建、读取、更新和删除 #key# 操作,且各层本就使用相同的模型!
像大多数技术争论一样,双方的观点都有其合理之处。接下来,我们将探讨几种常见的映射策略及其优缺点,帮助这些开发者做出更明智的选择。
第一种策略,就是完全不做映射。
不做映射
上图展示了我们示例应用 BuckPal 中“转账”用例所涉及的关键组件。
在 Web 层,Web 控制器调用 SendMoneyUseCase 接口来执行该用例,而该接口接收一个 Account 对象作为参数。这意味着 Web 层和应用层都需要访问 Account 类——它们共享同一个模型。
在应用的另一侧,持久化层与应用层之间也存在同样的关系。
由于所有层都使用相同的模型,因此无需在它们之间实现任何映射逻辑。
影响与后果
但采用这种设计会带来哪些后果呢?
Web 层和持久化层可能对模型有特殊要求。例如,若 Web 层通过 REST 暴露模型,则模型类可能需要一些注解,用于定义如何将字段序列化为 #key# JSON:JavaScript 对象表示法 #key#。同样地,如果我们在持久化层使用了 #key# ORM:对象关系映射 #key# 框架,也可能需要添加一些定义数据库映射的注解。
在当前设计中,所有这些特殊需求都必须在 Account 领域模型类中处理,即使领域层和应用层对此毫不关心。这违反了单一职责原则,因为 Account 类可能因 Web 层、应用层或持久化层的变化而被迫修改。
除了技术性需求外,每一层还可能需要在 Account 类中添加特定的自定义字段,这可能导致领域模型变得碎片化——某些字段仅在一个层中有意义,而在其他层中毫无用途。
但这是否意味着我们永远不应采用“不做映射”的策略?当然不是。
尽管这种方式看起来有些“脏”,但在某些场景下,它完全是合理且有效的选择。
考虑一个简单的 #key# CRUD:创建、读取、更新和删除 #key# 用例。我们真的有必要把字段从 Web 模型映射到领域模型,再从领域模型映射到持久化模型吗?其实没有必要。
那么,领域模型上的 #key# JSON:JavaScript 对象表示法 #key# 或 #key# ORM:对象关系映射 #key# 注解真的会造成困扰吗?即便持久化层发生变化时我们需要调整一两个注解,那又如何?
只要所有层所需的结构和数据完全一致,“不做映射”就是一个完全可接受的设计方案。
然而,一旦我们在应用层或领域层中不得不处理 Web 层或持久化层的具体细节(除了注解之外),我们就应当转向其他的映射策略。
这里给开头那两位开发者一个启示:即使我们过去选择了某种映射策略,未来仍然可以根据需要进行调整。

根据我的经验,许多用例最初都是简单的 #key# CRUD:创建、读取、更新和删除 #key# 操作。随着业务发展,它们可能会演变为具有丰富行为和复杂校验的完整业务用例,届时才真正需要更复杂的映射策略。但也有很多用例会一直保持 #key# CRUD:创建、读取、更新和删除 #key# 的简单状态,这时我们会庆幸当初没有投入精力去实现复杂的映射机制。