Как вы проверяете внутреннее состояние объекта?

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

Мой основной упор делается на С++, поскольку в С# официальным и распространенным способом является исключение, а на С++ существует не один единственный способ сделать это (нормально, на самом деле не на С#, я это знаю).

Обратите внимание, что я не говорит о проверке параметров функции, но скорее как проверки целостности класса.

Например, предположим, что мы хотим, чтобы объект 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.
}

Мои комментарии к стилям:

  • Я думаю, что предпочитаю второй стиль, когда сбой не скрыт, при условии, что нарушение прав действительно вызывает сбой.
  • Если это не указатель NULL, связанный с инвариантом, я склоняюсь к первому стилю.
  • Мне действительно не нравится третий стиль, так как он скроет множество ошибок, но я знаю людей, которые предпочитают его в производственном коде, потому что он создает иллюзию надежного программного обеспечения, которое не падает (функции просто перестанут функционировать, как в очереди на сломанном объекте Printer).

Вы предпочитаете какой-либо из них или у вас есть другие способы достижения этого?

Ответ 1

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

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

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

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

Ответ 2

Вы можете использовать метод 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, как показано выше.

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


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

Ответ 3

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

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

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

Ответ 4

Жесткий вопрос:)

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

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

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

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