如何验证对象的内部状态?

Joh*_*ell 7 c# c++ validation invariants

我很想听听您在操作过程中使用什么技术来验证对象的内部状态,从它自己的角度来看,只能因为内部状态不良或不变违规而失败.

我主要关注的是C++,因为在C#中官方和流行的方式是抛出一个异常,并在C++中有不只是一个单一的方式做到这一点(OK,不是真的在C#或者,我知道).

请注意,我不是在讨论函数参数验证,而是更像是类不变完整性检查.

例如,假设我们想要一个异步打印作业的Printer对象Queue.对于用户来说Printer,该操作只能成功,因为异步队列的结果会在另一时间到达.因此,没有相关的错误代码传达给调用者.

但是对于该Printer对象,如果内部状态不好,则该操作可能会失败,即类不变被破坏,这基本上意味着:一个错误.该条件不一定是Printer对象的用户感兴趣的.

就个人而言,我倾向于混合三种内部状态验证方式,我无法确定哪一个是最好的,如果有的话,哪一个绝对是最差的.我想听听你对这些问题的看法,以及你在这个问题上分享你自己的经验和想法.

我使用的第一种风格 - 以可控制的方式比损坏的数据更好地失败:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in both release and debug builds.
    // Never proceed with the queuing in a bad state.
    if(!IsValidState())
    {
        throw InvalidOperationException();
    }

    // Continue with queuing, parameter checking, etc.
    // Internal state is guaranteed to be good.
}
Run Code Online (Sandbox Code Playgroud)

我使用的第二种风格 - 比腐败数据更难以控制崩溃:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in debug builds only.
    // Break into the debugger in debug builds.
    // Always proceed with the queuing, also in a bad state.
    DebugAssert(IsValidState());

    // Continue with queuing, parameter checking, etc.
    // Generally, behavior is now undefined, because of bad internal state.
    // But, specifically, this often means an access violation when
    // a NULL pointer is dereferenced, or something similar, and that crash will
    // generate a dump file that can be used to find the error cause during
    // testing before shipping the product.
}
Run Code Online (Sandbox Code Playgroud)

我使用的第三种风格 - 比腐败数据更好地默默地和防御性地纾困:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in both release and debug builds.
    // Break into the debugger in debug builds.
    // Never proceed with the queuing in a bad state.
    // This object will likely never again succeed in queuing anything.
    if(!IsValidState())
    {
        DebugBreak();
        return;
    }

    // Continue with defenestration.
    // Internal state is guaranteed to be good.
}
Run Code Online (Sandbox Code Playgroud)

我对风格的评论:

  1. 我认为我更喜欢第二种风格,如果访问冲突实际上导致崩溃,则不会隐藏故障.
  2. 如果它不是一个涉及不变量的NULL指针,那么我倾向于倾向于第一种风格.
  3. 我真的不喜欢第三种风格,因为它会隐藏很多错误,但我知道人们更喜欢它在生产代码中,因为它创造了一个不会崩溃的强大软件的错觉(功能将停止运行,如在在破碎的Printer物体上排队).

你更喜欢这些吗?或者你有其他方法来实现这一目标吗?

Joh*_*itb 6

您可以将一种称为NVI(非虚拟接口)的技术与template method模式一起使用.这可能是我会这样做的(当然,这只是我的个人意见,这确实是有争议的):

class Printer {
public:
    // checks invariant, and calls the actual queuing
    void Queue(const PrintJob&);
private:
    virtual void DoQueue(const PringJob&);
};


void Printer::Queue(const PrintJob& job) // not virtual
{
    // Validate the state in both release and debug builds.
    // Never proceed with the queuing in a bad state.
    if(!IsValidState()) {
        throw std::logic_error("Printer not ready");
    }

    // call virtual method DoQueue which does the job
    DoQueue(job);
}

void Printer::DoQueue(const PrintJob& job) // virtual
{
    // Do the actual Queuing. State is guaranteed to be valid.
}
Run Code Online (Sandbox Code Playgroud)

因为Queue是非虚拟的,所以如果派生类重写DoQueue特殊处理,仍会检查不变量.


您可以选择:我认为这取决于您要检查的条件.

如果是内部不变量

如果它是不变量,则您的班级用户不应该违反它.班级应该关心它的不变性.因此,我会assert(CheckInvariant());在这种情况下.

它只是一种方法的先决条件

如果它仅仅是该类用户必须保证的前提条件(例如,仅在打印机准备就绪后打印),我将std::logic_error如上所示抛出 .

我真的不鼓励检查一个条件,但后来什么都不做.


在调用满足前置条件的方法之前,类的用户本身可以断言.所以一般来说,如果一个类负责某个状态,并且它发现一个状态无效,那么它应该断言.如果班级发现违反的条件不属于其职责范围,则应抛出.


Jam*_*kin 2

最好结合您测试软件的方式来考虑这个问题。

重要的是,在测试过程中遇到损坏的不变量会被归档为高严重性错误,就像崩溃一样。可以在开发期间进行测试构建来阻止死机并输出诊断。

添加防御性代码可能是合适的,就像您的风格 3 一样:您DebugBreak将在测试版本中转储诊断信息,但这只是开发人员的一个断点。这使得开发人员因不相关代码中的错误而无法工作的情况变得不太可能。

可悲的是,我经常看到相反的情况,开发人员会遇到所有不便,但测试构建却可以通过破坏的不变量进行。许多奇怪的行为错误被提交,而实际上一个错误就是原因。