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

StackOverflow https://stackoverflow.com/questions/343605

  •  19-08-2019
  •  | 
  •  

Вопрос

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

Основное внимание я уделяю 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 объект).

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

Это было полезно?

Решение

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top