我们需要验证整个模块的使用情况还是仅验证公共方法的参数?


9

我听说建议您验证公共方法的参数:

动机是可以理解的。如果将以错误的方式使用模块,则我们希望立即引发异常,而不是任何不可预测的行为。

令我困扰的是,错误的参数并不是使用模块时唯一的错误。这是一些错误情况,如果我们遵循建议并且不希望错误升级,则需要添加检查逻辑:

  • 来电-意外参数
  • 来电-模块处于错误状态
  • 外部通话-返回意外结果
  • 外部调用-意外的副作用(两次进入调用模块,破坏了其他依赖状态)

我试图考虑所有这些情况,并用一种​​方法(对不起,不是C#的人)编写一个简单的模块:

public sealed class Room
{
    private readonly IDoorFactory _doorFactory;
    private bool _entered;
    private IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        if (doorFactory == null)
            throw new ArgumentNullException("doorFactory");
        _doorFactory = doorFactory;
    }
    public void Open()
    {
        if (_door != null)
            throw new InvalidOperationException("Room is already opened");
        if (_entered)
            throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;
        _door = _doorFactory.Create();
        if (_door == null)
            throw new IncompatibleDependencyException("doorFactory");
        _door.Open();
        _entered = false;
    }
}

现在很安全=)

这很令人毛骨悚然。但是,请想象一下,在具有数十种方法,复杂状态和大量外部调用(嗨,依赖注入爱好者!)的真实模块中,它有多令人毛骨悚然。请注意,如果您正在调用一个可以覆盖其行为的模块(C#中的非密封类),则您在进行外部调用,并且后果在调用者的范围内是不可预测的。

总结一下,正确的方法是什么?为什么?如果您可以从以下选项中选择,请回答其他问题。

检查整个模块的使用情况。我们需要单元测试吗?有这种代码的例子吗?是否应该限制依赖项注入的使用(因为它将导致更多的检查逻辑)?将这些检查移至调试时是否可行(发行版中不包括)?

仅检查参数。根据我的经验,参数检查(尤其是null检查)是最无效的检查,因为参数错误很少会导致复杂的错误和错误升级。大多数情况下,您会NullReferenceException在下一行得到一个。那么,为什么参数检查是如此特别?

不要检查模块的使用情况。这是非常不受欢迎的意见,您能解释为什么吗?


在现场分配期间应进行检查,以确保保持不变。
Basilevs 2015年

@Basilevs有趣的...是从代码合同意识形态还是更古老的东西?您能推荐一些值得阅读的东西吗(与您的评论有关)?
astef

这是关注点的基本分离。您的所有案例均已涵盖,而代码重复却最少,责任也得到了很好的定义。
Basilevs 2015年

@Basilevs因此,根本不要检查其他模块的行为,而要检查自己的状态不变量。听起来很合理。但是,为什么在有关参数检查的相关问题中看不到这个简单的收据?
astef

好了,仍然需要一些行为检查,但是它们只能在实际使用的值上执行,而不是在其他地方转发的值。例如,您依赖于List实现来检查OOB错误,而不是检查客户端代码中的索引。通常它们是低级框架故障,不需要手动发出。
Basilevs 2015年

Answers:


2

TL; DR:验证状态更改,取决于当前状态的[validity]。

下面,我仅考虑启用发布的验证。仅调试活动断言是一种文档形式,它以其自己的方式有用,并且超出了此问题的范围。

考虑以下原则:

  • 常识
  • 快速失败
  • 建议零售价

定义

  • 组件-提供API的单元
  • 客户端-组件API的用户

可变状态

问题

在命令式语言中,错误症状及其原因可能会因数小时的繁重工作而分开。由于对当前状态的检查不能揭示完整的腐败过程,因此,错误源可以隐藏自身并变异导致无法解释的失败。

状态的每次更改都应精心设计和验证。处理可变状态的一种方法是将其保持在最低水平。这可以通过以下方法实现:

  • 类型系统(const和final成员声明)
  • 引入不变量
  • 通过公共API验证组件状态的每个更改

在扩展组件的状态时,请考虑让编译器强制新数据保持不变。另外,强制执行所有合理的运行时约束,将可能的结果状态限制为最小的可能良好定义的集合。

// Wrong
class Natural {
    private int number;
    public Natural(int number) {
        this.number = number;
    }
    public int getInt() {
      if (number < 1)
          throw new InvalidOperationException();
      return number;
    }
}

// Right
class Natural {
    private readonly int number;
    /**
     * @param number - positive number
     */
    public Natural(int number) {
      // Going to modify state, verification is required
      if (number < 1)
        throw new ArgumentException("Natural number should be  positive: " + number);
      this.number = number;
    }
    public int getInt() {
      // State is guaranteed by construction and compiler
      return number;
    }
}

重复与责任凝聚

问题

检查操作的前提条件和后置条件会导致客户端和组件中的验证码重复。验证组件调用通常会迫使客户承担组件的某些职责。

尽可能依靠组件执行状态验证。组件应提供不需要特殊使用验证(例如,参数验证或操作序列执行)的API,以使组件状态保持良好定义。他们有义务根据需要验证API调用参数,通过必要的方式报告失败,并努力防止其状态损坏。

客户应依靠组件来验证其API的使用。不仅避免了重复,而且客户端不再依赖于组件的额外实现细节。将框架视为组件。仅当组件的不变性不够严格或将组件异常封装为实现细节时,才编写自定义验证代码。

如果某项操作不会更改状态且未在状态更改验证中涵盖,请在尽可能深的级别上验证每个参数。

class Store {
  private readonly List<int> slots = new List<int>();
  public void putToSlot(int slot, int data) {
    if (slot < 0 || slot >= slots.Count) // Unnecessary, validated by List, only needed for custom error message
      throw new ArgumentException("data");
    slots[slot] = data;
  }
}

class Natural {
   int _number;
   public Natural(int number) {
       if (number < 1)
          number = 1;  //Wrong: client can't rely on argument verification, additional state uncertainity is introduced.  Right: throw new ArgumentException(number);
       _number = number;
   }
}

回答

将描述的原理应用于所讨论的示例时,我们得到:

public sealed class Room
{
    private bool _entered = false;
    // Do not use lazy instantiation if not absolutely necessary, this introduces additional mutable state
    private readonly IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        // Rely on system null check
        IDoor door = _doorFactory.Create();
        // Modifying own state, verification is required
        if (door == null)
           throw new ArgumentNullException("Door");
        _door = door;
    }
    public void Enter()
    {
        // Room invariants do not guarantee _entered value. Door state is indirectly a part of our state. Verification is required to prevent second door state change below.
        if (_entered)
           throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;     
        // rely on immutability for _door field to be non-null
        // rely on door implementation to control resulting door state       
        _door.Open();            
    }
}

摘要

客户状态由自己的字段值和组件状态的一部分组成,这些值未被其自身的不变式覆盖。验证仅应在客户端的实际状态更改之前进行。


1

一个类负责自己的状态。因此,请在一定程度上进行验证,以使其保持或处于可接受状态。

如果将以错误的方式使用模块,则我们希望立即引发异常,而不是任何不可预测的行为。

不,不要抛出异常,而要提供可预测的行为。国家责任的必然结果是使类/应用程序具有尽可能的宽容性。例如,传递nullaCollection.Add()?只是不要添加并继续前进。您获得null创建对象的输入吗?创建一个空对象或默认对象。以上,door已经是open?那么,继续前进。DoorFactory参数是否为空?创建一个新的。当我创建一个时,enum我总是有一个Undefined成员。我自由使用Dictionarys并enums明确定义事物;这对于实现可预测的行为大有帮助。

(嗨,依赖注入爱好者!)

是的,尽管我走过参数谷的阴影,但我不会害怕任何争论。在前面,我还尽可能使用默认和可选参数。

以上所有内容使内部处理得以继续进行。在一个特定的应用程序中,我在多个类中有数十种方法,只有一个地方会引发异常。即使那样,这也不是因为空参数或我无法继续处理它,而是因为代码最终创建了一个“非功能” /“空”对象。

编辑

完整引用我的评论。我认为设计在遇到“空”时不应简单地“放弃”。特别是使用复合对象。

我们在这里忘记了关键概念/假设- encapsulationsingle responsibility。在客户端交互的第一层之后,几乎没有空检查。该代码是可以容忍的。类是使用默认状态设计的,因此无需编写代码就可以像交互代码中充斥着错误,流氓垃圾一样工作。复合父级不必深入子级即可评估有效性(并暗示,请检查所有角点和缝隙是否为空)。父母知道孩子的默认状态意味着什么

结束编辑


1
不添加无效的收集元素是非常不可预测的行为。
Basilevs 2015年

1
如果所有接口都将以一种如此宽容的方式进行设计,那么有一天,由于常见错误,程序将意外地醒来并破坏人类。
astef

我们在这里忘记了关键概念/假设- encapsulationsingle responsibility。在null客户端交互的第一层之后,几乎没有检查。该代码是<strike> tolerant </ strike>健壮的。类是使用默认状态设计的,因此无需编写代码就可以像交互代码中充斥着错误,流氓垃圾一样工作。复合父级不必深入子级即可评估有效性(并暗示要检查null所有的角落和缝隙)。父母知道孩子的默认状态意味着什么
Radarbob 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.