7

我很想听听您在操作期间使用什么技术来验证对象的内部状态,从它自己的角度来看,这些技术只会因为内部状态不好或不变的破坏而失败。

我的主要关注点是 C++,因为在 C# 中,官方和流行的方法是抛出异常,而在 C++ 中并没有唯一的方法可以做到这一点(好吧,在 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.
}

我使用的第二种风格 - 比损坏的数据更好的崩溃无法控制:

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.
}

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

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.
}

我对样式的评论:

  1. 我认为我更喜欢第二种风格,其中不隐藏故障,前提是访问冲突实际上会导致崩溃。
  2. 如果它不是不变量中涉及的 NULL 指针,那么我倾向于倾向于第一种样式。
  3. 我真的不喜欢第三种风格,因为它会隐藏很多错误,但我知道有人更喜欢在生产代码中使用它,因为它会产生一种不会崩溃的强大软件的错觉(功能只会停止运行,如损坏Printer对象上的排队)。

您更喜欢其中的任何一个,还是有其他方法可以实现这一目标?

4

4 回答 4

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.
}

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


供您选择:我认为这取决于您要检查的条件。

如果是内部不变量

如果它是一个不变量,那么您的类的用户应该不可能违反它。类应该关心它的不变量本身。因此,我会assert(CheckInvariant());在这种情况下。

它只是一个方法的前提条件

如果这只是类用户必须保证的前提条件(例如,仅在打印机准备好后打印),我会 std::logic_error如上所示抛出。

我真的不鼓励检查条件,但什么也不做。


类的用户可以在调用一个方法之前断言它的先决条件得到满足。所以一般来说,如果一个类对某个状态负责,并且它发现一个状态是无效的,它应该断言。如果该类发现违反了不属于其责任的条件,则应该抛出。

于 2008-12-05T12:05:24.767 回答
2

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

重要的是,在测试期间遇到损坏的不变量将作为高严重性错误提交,就像崩溃一样。可以在开发期间进行测试构建以停止死机并输出诊断信息。

添加防御性代码可能是合适的,就像您的风格 3:您DebugBreak将在测试构建中转储诊断,但对于开发人员来说只是一个断点。这降低了开发人员因不相关代码中的错误而无法工作的情况。

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

于 2008-12-05T12:13:03.643 回答
1

这是一个很好且非常相关的问题。恕我直言,任何应用程序架构都应该提供一种策略来报告损坏的不变量。可以决定使用异常,使用“错误注册表”对象,或者明确检查任何操作的结果。也许还有其他策略——那不是重点。

依赖于一个可能很大的崩溃是一个坏主意:如果你不知道不变破坏的原因,你不能保证应用程序会崩溃。如果没有,您仍然有损坏的数据。

来自 litb的NonVirtual Interface解决方案是一种检查不变量的巧妙方法。

于 2008-12-05T13:21:37.343 回答
1

棘手的问题:)

就我个人而言,我倾向于抛出一个异常,因为在实现某些东西时我通常会过多地关注我正在做的事情来处理你的设计应该注意的事情。通常这会回来并稍后咬我......

我对“做一些记录,然后不再做任何事情”策略的个人经验是,它也会反过来咬你 - 特别是如果它像你的情况那样实施(没有全球策略,每个班级都可能以不同的方式做到这一点)。

一旦我发现这样的问题,我会做的就是与团队的其他成员交谈,并告诉他们我们需要某种全局错误处理。处理将做什么取决于您的产品(您不希望什么都不做,并将某些内容记录在空中交通管制系统中的一个微妙的开发人员意识文件中,但如果您正在制作驱动程序,它会工作得很好,比如说,一台打印机:) )。

我想我说的是,恕我直言,这个问题是您应该在应用程序的设计级别而不是在实现级别解决的问题。- 可悲的是没有神奇的解决方案:(

于 2008-12-05T17:09:40.237 回答