Как вы проверяете внутреннее состояние объекта? - PullRequest
7 голосов
/ 05 декабря 2008

Мне интересно услышать, какую технику (ы) вы используете для проверки внутреннего состояния объекта во время операции, которая, с его собственной точки зрения, может дать сбой только из-за плохого внутреннего состояния или инвариантного нарушения.

Мой основной упор делается на 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 ]

6 голосов
/ 05 декабря 2008

Вы можете использовать метод NVI ( Non-Virtual-Interface ) вместе с шаблоном 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 как показано выше.

Я бы действительно отговаривал от проверки состояния, но тогда ничего не делал.


Пользователь класса сам может до вызова метода утверждать, что его предварительные условия выполнены. Поэтому, как правило, если класс отвечает за какое-то состояние и обнаруживает, что состояние недопустимо, он должен утверждать. Если класс находит условие, которое должно быть нарушено, что не входит в его ответственность, он должен выполнить команду throw.

2 голосов
/ 05 декабря 2008

Этот вопрос лучше всего рассматривать в сочетании с тем, как вы тестируете свое программное обеспечение.

Важно, чтобы попадание сломанного инварианта во время тестирования регистрировалось как ошибка с высокой степенью серьезности, так же как и при аварии. Сборки для тестирования во время разработки могут быть сделаны для остановки и диагностики выходных данных.

Может быть целесообразно добавить защитный код, скорее как ваш стиль 3: ваш DebugBreak будет выводить диагностику в тестовых сборках, но будет просто точкой останова для разработчиков. Это делает менее вероятной ситуацию, когда разработчик не может работать из-за ошибки в несвязанном коде.

К сожалению, я часто видел, как это было сделано наоборот, где разработчики получали все неудобства, но тестовые сборки проходили через сломанные инварианты. Множество странных ошибок в поведении регистрируется, хотя на самом деле причиной является одна ошибка.

1 голос
/ 05 декабря 2008

Сложный вопрос:)

Лично я, как правило, просто выбрасываю исключение, поскольку я обычно слишком увлекаюсь тем, что я делаю, когда реализую что-то, чтобы позаботиться о том, о чем должен позаботиться ваш дизайн. Обычно это возвращается и кусает меня позже ...

Мой личный опыт со стратегией "делай что-нибудь ведение журнала, а потом ничего не делай больше" заключается в том, что она тоже возвращается, чтобы укусить вас - особенно если она реализована, как в вашем случае ( нет глобальной стратегии, каждый класс потенциально может сделать это по-разному).

Что бы я сделал, как только обнаружу проблему, подобную этой, это поговорить с остальной частью моей команды и сказать им, что нам нужна какая-то глобальная обработка ошибок. То, что будет делать обработка, зависит от вашего продукта (вы не хотите просто ничего не делать и регистрировать что-то в тонком, ориентированном на разработчика файле в системе Air Traffic Controller, но это будет работать нормально, если вы создаете драйвер для, скажем, принтер :)).

Я предполагаю, что я говорю, что, имхо, этот вопрос нужно решить на уровне разработки приложения, а не на уровне реализации. - И, к сожалению, нет волшебных решений: (* ​​1009 *

1 голос
/ 05 декабря 2008

Это хороший и очень актуальный вопрос. ИМХО, любая архитектура приложения должна обеспечивать стратегию сообщения о нарушенных инвариантах. Можно решить использовать исключения, использовать объект «реестр ошибок» или явно проверить результат любого действия. Может быть, есть даже другие стратегии - это не главное.

В зависимости от возможного громкого сбоя плохая идея: вы не можете гарантировать, что приложение будет аварийно завершать работу, если вы не знаете причину инвариантного нарушения. Если это не так, у вас все еще есть поврежденные данные.

Решение NonVirtual Interface от litb - отличный способ проверки инвариантов.

...