让我们讨论经典的 3 层架构,我们在其中与与数据库交互的 Web API 进行前端通信。让我们看看数据处理管道可能出错的地方:
- 前端没有验证,或者它没有检查所有条件。我们不能假设我们会完美无缺并且可以标准化一切。我们在开发管道中的元素越多,我们的同事或我们忽略它们的可能性就越大。
- API改变了它的业务逻辑,前端还不知道。
- 前端验证无法验证某些条件。例如,产品是否仍有库存,或者用户电子邮件是否是唯一的。查询后端来检查这些条件是不够的。
- 我们的 API 是公开的。大多数 API 使用基于文本的表示形式来表示它们收到的消息。这意味着任何人都可以手工制作 JSON、XML 或纯文本并发送任何数据。这意味着,例如空请求、错误消息格式(XML 而不是 JSON)、未提供所需数据、无效数据格式(字符串而不是数字、数组而不是单个值等)。
- 有人可能故意执行恶意操作,例如发送无效请求以破坏我们的系统或通过数据抓取窃取数据。任何人都可以用棍子戳我们的 API,试图通过分析响应来寻找漏洞并提取数据。
基本上,任何事情都可能发生。如果我们不形成隔离墙,我们可能会遇到真正的麻烦。
数据库也可以是这样。而这甚至是危险的,因为我们认为这是我们的数据,我们可以完全控制。这可能让我们措手不及。数据结构和它的意义都随着软件的寿命而演变。有些字段成为必需的,有些变得过时,有些则被放弃。我们将无法从一开始就提供最终的解决方案。不是说我们有一个大的泥球,我们通过数据库整合多个模块/服务的情况。
拥抱外部世界可能是邪恶的,或者只是与我们预期的不同,这是 "端口与适配器 "的基础,所以需要六边形架构。
业务验证大致流程如下:
1. 把API请求类作为使用原始类型的普通对象。
我假设我可以得到任何东西,空,无效的格式,一切都可能是错误的。我试着去解析,不要验证。这样一个类的任务是将请求中的数据转化为该类的实例。我通常把这样一个类直接放在API项目中。它在C中可能看起来如下
public record AddProductRequest( |
2.在解析了请求之后,我把它映射到真正的合同(例如命令或查询)。
这个合同已经来自领域模块。这就是信任元素的作用。我可以信任这段代码,因为我在我的代码中进行实例化,另外我还负责定义如何进行实例化。我通常会创建一个静态的工厂方法,并且毫不吝啬地在这里使用值对象来执行语义验证。映射代码可能看起来像。
var command = AddProduct.From( |
类型定义:
public record AddProduct( |
和所有的事情一样,这种模式也有它的名字--智能构造器。它来自函数式编程,但正如你所看到的,即使在命令式世界中,它也有很大的意义。
在创建一个命令或查询实例后,我们知道它是正确的。所谓正确,我的意思是它满足了基本的假设,比如:所有的必填字段都有指定的值,字段有正确的类型,验证如开始日期早于结束日期,等等。关键是不要在这里做复杂的领域逻辑验证,而是要做语义验证。
你可能还注意到,我使用了记录类型。这意味着这些类的实例将是不可改变的。现在大多数语言都允许定义这样的结构,例如,Java也有记录,TypesScript有只读类型,函数式语言默认也有。为什么它如此重要?
由于不可变性,我们对我们的对象有了更多的信任。我们知道没有人会通过做意外的牛仔式更新来改变它们。我们可以把它们作为参数传递;它们将永远是我们创建的样子。
3.正确的领域验证应该在业务逻辑中完成。
这就是为什么我喜欢CQRS。由于CQRS,我们知道一个特定的处理程序将执行该命令。业务逻辑将被路由到一个特定的函数或聚合方法。如果我们要改变规则,我们不必用不稳定的眼光来看待整个代码。例如,在命令中验证结束日期是否晚于开始日期是值得的,但我建议在业务逻辑中检查日期是否大于今天的日期。
public class ShoppingCart: Aggregate |
总结一下:
- 我们增加了信任和我们代码的安全性。
- 我们使领域代码的变化与API的变化无关。
- 我们可以逐一切断边缘场景:反序列化、类型的语义验证和业务验证。
- 由于提高了可维护性和认知负荷,我们更容易知道什么、哪里和如何改变。
- 我们减少了所需的测试数量。
当然,所有这些都需要一致性,但一旦我们建立了它,并在我们的类型上仔细工作,每一个新类型都会变得更容易。