验证与业务规则的区别 - Mark Seemann


验证是区别于业务规则的定义。

本文提出了软件开发中验证的定义:
介绍了我目前是如何区分验证和业务规则的。
我发现这种区分是有用的,尽管这也许是一个因果关系颠倒的例子。
我的定义是这样的:

验证是一个决定数据是否可以接受的纯函数。

我使用了可接受这个词,因为它暗示了与Poste l定律 的联系。在验证时,你可能希望允许输入有一定的灵活性,即使严格来说,它不完全符合规范。

然而,这并不是我定义中的关键因素。关键是,验证应该是一个纯粹的函数。


业务规则 #
在我解释上述定义的好处之前,我认为概述一下开发者面临的典型问题会很有帮助。

我在《适合你的代码》中的论述是,了解人类认知的局限性是使代码库可持续发展的主要因素

这再次解释了为什么封装是一个如此重要的想法:
你想把知识限制在适合你脑袋的小容器里。信息不应该从这些被封装容器中泄漏出来,因为这将影响你在试图理解其他代码时跟踪太多的东西。

在讨论封装的时候,我强调的是契约而不是信息隐藏。
根据面向对象软件构建的精神,契约是一组前提条件、不变条件和后置条件。
前提条件与验证的主题特别相关,但我经常遇到这样的情况:一些开发者很难确定验证的终点和业务规则的起点。

以一个在线餐厅预订系统为例:
实现一个功能,使用户能够进行预订。
为了达到这个目的,我们决定引入一个Reservation(预约/预订)类。
那么,创建这样一个类的有效实例的前提条件是什么?

当我进行这样的练习时,人们很快就会发现这样的要求:

  • 预约应该有一个日期和时间。
  • 预订应该包含客人的数量。
  • 预约应包含预约人的姓名或电子邮件(或其他数据)。

一个常见的建议是,餐厅也应该能够满足预订;也就是说,它不应该被全部预订完,它应该在所需的时间内有一个适当数量的可用桌子,等等。

然而,这并不是创建一个有效的预订Reservation对象的前提条件,而是一个业务规则。

前提条件是自成一体的 #
你如何区分前提条件和业务规则?这与输入验证有什么关系?

请注意,在上面的例子中,我列出的三个前提条件是自成一体的。它们是关于对象或值的构成部分的声明。

另一方面,餐厅应该能够容纳预订的要求涉及到一个更广泛的背景上下文:
餐馆的餐桌布局、先前的预订、开业和关门时间,以及其他业务规则。

正如Alexis King所指出的,验证是一个解析问题

你收到结构化程度较低的数据(CSV、JSON、XML等),并试图将其投射到结构化程度较高的格式(C对象、F记录、Clojure Map等)。
当输入参数能满足前提条件时,就成功了,否则就失败了。

为什么我们不能添加比要求更多的前提条件?
考虑一下Postel定律。一个操作(包括对象构造器)在接受的内容上应该是开放自由的。虽然你必须在某个地方划清界限(如果日期缺失,你就不能真正使用预订),但一个对象不应该要求超过需要它的最初目的。

一般来说,我们观察到,预设条件越少,创建一个对象(或相当的功能数据结构)就越容易
作为一个反例,这解释了为什么Active Record与单元测试是对立的。
一个先决条件是有一个可用的数据库,虽然在测试中不是不可能实现自动化,但这是很麻烦的事情。
在测试中使用POJO更容易。而单元测试,作为API的第一个客户,告诉你使用该API有多容易。

与第三方的锲约#
如果验证从根本上说是解析,那么操作应该是纯函数似乎是合理的。

毕竟,解析器是对不变的(较少结构化的)数据进行操作。

一个编程语言的解析器将文本文件的内容作为输入。
几乎不需要更多的输入,而且输出被期望是确定性的。
毫不奇怪,Haskell非常适用于编写解析器。

然而,你不必相信验证本质上就是解析的说法,所以考虑一下另一个角度。

验证是你为处理输入而进行的数据转换步骤。
数据来自于你系统的外部来源:

  • 它可以是一个用户填写的表格,
  • 另一个程序发出的HTTP请求,
  • 或者是一个通过FTP接收文件的批处理作业。

即使你没有与任何第三方达成正式的协议, Hyrum's law 也意味着协议合同确实存在。你有责任注意这一点,并尽可能地使其明确。

这样的合同协议应该是稳定的。第三方应该能够依赖确定性的行为。
如果他们某天提供了数据,而你接受了它,你就不能在第二天以数据格式错误为由拒绝同样的数据。
充其量,随着时间的推移,你的输入可能是不固定的;换句话说,你明天可能接受你今天不接受的东西,但你明天可能不会拒绝你今天接受的东西。

同样地,你不能让验证规则在一分钟内无规律地接受数据,在下一分钟内拒绝同样的数据,然后又接受它。

这意味着验证至少必须是确定性的:相同的输入应该总是产生相同的输出。

这是实现参照性透明的一半途径。你在验证逻辑中需要副作用吗?几乎不需要,所以你还不如把它作为纯函数来实现。


本末倒置 #
你可能仍然认为我的定义有一种寻找问题的解决方案的味道。是的,纯函数很方便,但这是否自然而然地意味着验证应该作为纯函数来实现?这难道不是一个糟糕的转述案例吗?

到底什么是验证,什么是业务规则?
验证与业务规则类似两个用来分类的标准:

  • 首先地确定验证这个分类标准的大小,使其与应用中的验证完全一致。
  • 然后,业务规则分类装入剩下的东西。

我的经验是,这种对验证的看法效果很好。

在很大程度上,这是因为我认为验证是一个已解决的问题。能够从一个更大的问题中抽出一大块并把它放在一边是很有成效的:我们知道如何处理这个问题。这块就没有风险。

业务规则的变化
让我们回到验证的角度,作为你的系统和第三方之间的技术合同。虽然这个合同应该尽可能的稳定,但商业规则是会变化的。

考虑一下在线餐厅预订的例子。想象一下,你是第三方程序员,你已经开发了一个客户端,可以代表用户进行预订。当用户想要预订时,总是存在着不可能的风险。你的客户端应该能够处理这种情况。

现在,这家餐厅变得如此受欢迎,以至于它决定改变一条规则。早些时候,你可以为一个人、三个人或五个人预订,尽管该餐厅只有两个人、四个人或六个人的桌子。基于其新发现的受欢迎程度,该餐厅决定只接受整桌的预订。除非是在同一天,而且他们还有空余的桌子。

这改变了系统的行为,但没有改变合同。三个人的预订仍然有效,但会因为新规则而被拒绝。

"以相同速率变化的事物属于一致性;以不同速度变化的事物属于分离性。
肯特-贝克

业务规则的变化速度与先决条件不同,因此将这些问题分离开来是有意义的。

结论 #
既然“验证”是一个已解决的问题,那么能够识别什么是“验证”,什么是“其他”东西就很有用。

只要 "输入的规则 "是自足的(或可参数化的),是确定的,并且没有副作用,你就可以用应用中验证来模拟它。

同样有用的是,能够发现应用中验证不适合的时候:如果某些业务操作涉及不纯的动作,它就不适合使用“验证”模式。

这并不意味着你不能用纯函数实现业务规则。