56

我对检查数据合法性是调用者还是被调用者的责任感到困惑

被调用者是否应该检查传入的参数是否不应该null并且满足其他一些要求,以便被调用者方法可以正常成功执行,并捕获任何潜在的异常?还是调用者有责任这样做?

4

13 回答 13

75

消费者端(客户端)提供者端(API)验证。

客户应该这样做,因为这意味着更好的体验。例如,为什么网络往返只是被告知您有一个错误的文本字段?

提供者应该这样做,因为他们不应该信任客户端(例如 XSS 和中间人攻击)。你怎么知道请求没有被拦截?验证一切。

有几个级别的valid

  1. 存在所有必填字段,格式正确。这是客户验证的内容。
  2. # 1 加上字段之间的有效关系(例如,如果存在 X,则需要 Y)。
  3. #1 plus #2 plus 业务有效:符合所有业务规则进行正确处理。

只有提供方可以做#2 和#3。

于 2013-06-19T09:40:33.960 回答
19

对于 API,被调用者应始终进行适当的验证,并对无效数据抛出描述性异常。

对于任何具有 IO 开销的客户端,客户端也应该进行基本验证......

于 2013-06-19T09:44:50.537 回答
11

验证:调用者与被调用者

TLDR 版本是两者。

长版本涉及谁、为什么、何时、如何和什么。

两个都

两者都应该准备好回答“这些数据可以可靠地操作吗?”这个问题。 我们是否对这些数据足够了解,可以用它做一些有意义的事情?许多人会建议永远不应该相信数据的可靠性,但这只会导致鸡和蛋的问题。从两端无休止地追逐它不会提供有意义的价值,但在某种程度上它是必不可少的。

两者都必须验证数据的形状以确保基本可用性。如果其中任何一个人不认识或不理解数据的形状,就无法知道如何以任何可靠性进一步处理它。根据环境,数据可能需要是特定的“类型”,这通常是验证形状的简单方法。我们经常考虑那些提供共同血统证据的类型,这些证据可以追溯到特定的祖先,并保留了具有正确形状的关键特征。如果数据不是内存结构,其他特征可能很重要,例如,如果它是流或运行上下文之外的其他资源。

许多语言通过类型或接口检查将数据形状检查作为内置语言功能包括在内。然而,当有利于组合而不是继承时,提供一个良好的机制来验证特征存在是实现者的责任。实现这一点的一种策略是通过动态编程,或者特别是通过类型自省、推理或反射。

被调用者必须验证它将对其进行操作的给定上下文的域(输入集)。被调用的设计总是表明它只能处理这么多的输入情况。通常这些值被分解为输入的某些子类或类别。我们验证被调用的域,因为被调用与局部约束密切相关。它比任何人都更清楚什么是好的输入,什么不是。

  • 正常值:域的这些值映射到一个范围。对于每foo一个,只有一个bar

  • 超出范围/超出范围值:这些值是通用域的一部分,但不会映射到被调用上下文中的范围。这些值不存在定义的行为,因此不可能有有效的输出。经常超出范围检查需要范围、限制或允许的字符(或数字或复合值)。基数检查(多重性)和随后的存在性检查(空或空)是范围检查的特殊形式。

  • 导致不合逻辑或未定义行为的值:这些值是特殊值或边缘情况,本来是正常的,但由于算法设计和已知的环境约束,会产生意想不到的结果。例如,对数字进行操作的函数应防止除以零或累加器溢出,或意外丢失精度。有时操作环境或编译器会警告这些情况可能会发生,但依赖运行时或编译器并不是一个好的做法,因为它可能并不总是能够推断出什么是可能的,什么是不可能的。这个阶段应该主要是通过二次验证来验证调用者提供了良好、可用、有意义的输入。

呼叫者

来电者很特别。调用者有两种情况需要验证数据。

第一种情况是赋值或显式状态更改,其中至少一个数据元素通过某种显式机制、内部或外部通过其容器中的某些东西发生更改。这有点超出了问题的范围,但要记住一些事情。重要的是要考虑状态变化发生时的上下文,以及描述状态的一个或多个元素受到影响。

  • 自我/参照完整性:考虑使用内部机制来验证其他参与者是否可以引用数据的状态。当数据没有一致性检查时,只能假设它处于不确定状态是安全的。那不是中间的,而是不确定的。认识你自己。当您不使用机制来验证状态更改的内部一致性时,数据不可靠,这会导致第二种情况出现问题。确保调用者的数据处于已知的良好状态;或者,处于已知的转换/恢复状态。在你准备好之前不要打电话。

第二种情况是数据调用函数时。呼叫者对被呼叫者的期望只有这么多。调用者必须知道并尊重被调用者只识别某个域。调用者也必须是自利的,因为它可能会在被调用者完成后继续存在很长时间。这意味着调用者必须帮助被调用者不仅成功,而且还要适合任务:输入的错误数据会产生错误的数据输出。同样,即使是关于被呼叫者的良好数据输入和输出也可能不适用于呼叫者的下一件事。好的数据输出实际上可能是调用者的坏数据。被调用的输出可能会使调用者的当前状态无效。

好的,足够的评论,调用者应该具体验证什么?

  • 逻辑和正常:给定数据,是否是符合目的和意图的好策略?如果我们知道它会因某些值而失败,那么大多数时候在没有适当的守卫的情况下执行调用是没有意义的。如果我们知道被调用者无法处理零,请不要要求它,因为它永远不会成功。什么更昂贵且更难管理:[冗余(我们知道吗?)]保护子句,或异常[发生在可能长时间运行的、外部可用资源依赖进程的后期]?实现可能会发生变化,并且会突然发生变化。在调用者中提供保护可以减少更改该实现的影响和风险。

  • 返回值:检查未成功完成。这是调用者可能需要也可能不需要做的事情。在使用或依赖返回的数据之前,检查替代结果,如果系统设计包含可能伴随实际返回值的成功和失败值。

脚注:如果不清楚。Null 是域问题。这可能是也可能不是合乎逻辑和正常的,所以这取决于。如果 null 是函数的自然输入,并且可以合理地预期该函数会产生有意义的输出,则将其留给调用者使用。如果调用者的域使得 null 不符合逻辑,那么在这两个地方都要提防它。

一个重要的问题:如果您将 null 传递给被调用者,并且被调用者正在生产某些东西,那这不是一种隐藏的创建模式,从无到有创建一些东西吗?

于 2013-06-19T20:39:09.857 回答
9

这都是关于“合同”的。那是一个被调用者,它决定哪些参数是好的。您可以输入“null”参数无效的文档,然后抛出NullPointerExceptionorInvalidArgumentException就可以了。

如果返回 null 参数的结果有意义 - 在文档中说明。通常这种情况是一个糟糕的设计 - 创建一个具有更少参数而不是接受 null 的覆盖方法。

只记得抛出描述性异常。根据经验法则:

  1. 如果调用者传递了错误的参数,与文档中描述的不同(即 null、id < 0 等) - 抛出未经检查的异常(NullPointerExceptionInvalidArgumentException
  2. 如果调用者传递了正确的参数,但可能存在导致无法处理调用的预期业务案例 - 您可能希望抛出检查的描述性异常。例如 - 对于getPermissionsForUser(Integer userId)调用者传递userId不知道这样的用户是否存在,但它是一个非空整数。您的方法可能会返回权限列表或抛出UserNotFoundException. 它可能是一个检查异常。
  3. 如果根据文档参数是正确的,但它们会导致处理内部错误 - 您可能会抛出未经检查的异常。这通常意味着您的方法没有经过很好的测试;-)
于 2013-06-19T10:04:52.587 回答
4

这要看情况。

如果您可以确定如何处理被调用方内部的无效数据,那么就在那里进行。

如果您不确定(例如,因为您的方法非常通用并且用于几个不同的地方和方式),那么让调用者决定。

例如,想象一个 DAO 方法必须检索某个实体,而您却找不到它。你能决定是抛出一个异常,还是回滚一个事务或者只是认为它没问题?在这种情况下,绝对由调用者决定如何处理它。

于 2013-06-19T09:47:45.187 回答
4

两个都。这是双方良好的软件开发问题,并且独立于环境(C/S、Web、内部 API)和语言。

被调用者应该根据有据可查的参数列表验证所有参数(您确实记录了它,对吗?)。根据环境和体系结构,应实现良好的错误消息或异常,以明确指示参数有什么问题。

调用者应确保在 api 调用中仅传递适当的参数值。应尽快捕获任何无效值,并以某种方式反映给用户。

正如生活中经常发生的那样,任何一方都不应该只是假设对方会做正确的事情而忽略潜在的问题。

于 2013-06-19T17:04:26.020 回答
4

取决于您是名义上、防御性还是完全编程。

  • 如果您进行防御性编程(我个人最喜欢大多数 Java 方法),则验证方法中的输入。当验证失败时,您会抛出异常(或以其他方式失败)。
  • 如果您名义上编程,则不会验证输入(但希望客户端确保输入有效)。当验证会对性能产生不利影响时,此方法很有用,因为验证会花费大量时间(例如耗时的搜索)。
  • 如果你完全编程(我个人最喜欢大多数 Objective-C 方法),你验证方法中的输入,但是你将无效输入更改为有效输入(比如通过将值捕捉到最接近的有效值)。

在大多数情况下,您会进行防御性(快速故障)或完全(故障安全)编程。名义编程是有风险的 IMO,在期望来自外部来源的输入时应避免。

当然,不要忘记记录所有内容(特别是在名义上编程时)。

于 2013-06-26T12:43:19.307 回答
3

我要从不同的角度来看待这个问题。在包含的应用程序中工作,调用者和被调用者都在相同的代码中。然后,被调用者的合同要求的任何验证都应由被调用者完成。

因此,您编写了一个函数,并且您的合约说“不接受 NULL 值”。您应该检查未发送 NULL 值并引发错误。这样可以确保您的代码是正确的,并且如果其他人的代码正在做一些不应该的事情,他们会很快知道。

此外,如果您假设其他代码会正确调用您的方法,而他们没有正确调用,这将使跟踪潜在错误的来源变得更加困难。

这对于“早期失败,经常失败”至关重要,其想法是在检测到问题后立即提出错误条件。

于 2013-06-19T12:49:50.847 回答
2

验证数据是被调用者的责任。这是因为只有被调用者知道什么是有效的。这也是一个很好的安全实践。

于 2013-06-19T09:41:49.097 回答
2

它也需要位于客户端和服务器(被调用者和调用者)端的两端。

客户 :

  1. 这是最有效的一种。
  2. 客户端验证将减少对服务器的一个请求。
  3. 减少带宽流量。
  4. 耗时(如果它有来自服务器的延迟响应)

服务器 :

  1. 不要相信 UI 数据(由于黑客)。
  2. 大多数后端代码会被重用,所以我们不知道数据是否会为空等。所以我们需要验证被调用者和调用者方法。

总的来说, 1. 如果数据来自 UI,最好在 UI 层进行验证并在服务器层进行双重检查。2.如果数据在服务器层本身传输,我们需要在被调用者上进行验证,并且为了双重检查,我们也需要在调用者端进行。

谢谢

于 2013-06-19T09:54:08.380 回答
2

以我的拙见,再用几句话解释原因,大多数时候这是被调用者的责任,但这并不意味着调用者总是无事可做。

原因是被调用者最有能力知道它需要做什么来完成它的工作,因为它是做工作的那个人。因此,自我验证的对象或方法是很好的封装。如果被调用者不能对空指针做任何事情,那是一个无效的参数,应该被扔掉。如果有参数超出范围,这也很容易防范。

然而,“对法律的无知不是辩护”。对于调用者来说,简单地将其提供的所有内容推入其辅助函数并让被调用者整理出来,这不是一个好的模式。调用者这样做时不会添加任何值,一方面,特别是如果调用者向被调用者推送的数据是它自己的调用者提供的数据,这意味着调用堆栈的这一层可能是多余的。它还使调用者和被调用者的代码都非常复杂,因为双方都“防御”了对方不想要的行为(被调用者试图挽救可行的东西并测试一切,而调用者将调用包装在 try-catch 语句中,试图更正呼叫)。

因此,调用者应该验证它所知道的有关传递数据的要求的信息。当调用存在固有的时间开销时尤其如此,例如调用服务代理时。如果您必须等待一秒钟的大部分时间才能发现您的参数错误,那么在执行相同的客户端需要几个滴答声时,优势是显而易见的。被调用者的保护条款正是如此;在丑陋的事情被排除在实际工作程序之外之前的最后一道防线和优雅的失败。

于 2013-06-19T15:32:46.413 回答
0

调用者和被调用者之间应该有一些称为合同的东西。如果输入数据在指定值中,被调用者确保它做正确的事情。他仍然应该根据这些规范检查传入的数据是否正确。在 Java 中,你可以抛出一个InvalidArgumentException.

调用者也应该在合同规范内工作。他是否应该检查他交出的数据取决于具体情况。理想情况下,您应该以不需要检查的方式对调用者进行编程,因为您确信数据的有效性。如果它是例如用户输入,您无法确定它是否有效。在这种情况下,您应该检查它。如果你不检查它,你至少必须处理异常并做出相应的反应。

于 2013-06-19T09:57:40.393 回答
0

被调用者有责任检查它收到的数据是否有效。不执行此任务几乎肯定会导致软件不可靠,并使您面临潜在的安全问题。

话虽如此,如果您可以控制客户端(调用者)代码,那么您还应该在那里至少执行一些验证,因为它会带来更好的整体体验。

作为一般规则,尽可能早地发现数据问题,这样可以减少进一步的麻烦。

于 2013-06-19T12:26:06.583 回答