Как вы проверяете внутреннее состояние объекта?
-
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.
}
Мои комментарии к стилям:
- Я думаю, что предпочитаю второй стиль, при котором сбой не скрывается, при условии, что нарушение прав доступа действительно приводит к сбою.
- Если в инварианте задействован не NULL-указатель, то я склоняюсь к первому стилю.
- Мне очень не нравится третий стиль, так как он скрывает множество ошибок, но я знаю людей, которые предпочитают его в рабочем коде, потому что он создает иллюзию надежного программного обеспечения, которое не дает сбоев (функции просто перестанут работать, как в очередь на сломанном
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 является отличный способ проверить инварианты.
Сложный вопрос:)
Лично я склонен просто генерировать исключение, поскольку обычно я слишком увлекаюсь тем, что делаю при реализации, чтобы позаботиться о том, о чем должен заботиться ваш дизайн.Обычно это возвращается и кусает меня позже...
Мой личный опыт применения стратегии «сделай немного журнала, а потом больше ничего не делай» показывает, что она тоже может вас укусить, особенно если она реализована так, как в вашем случае (нет глобальной стратегии , каждый класс потенциально может сделать это по-разному).
Как только я обнаружу подобную проблему, я бы поговорил с остальными членами моей команды и сказал им, что нам нужна какая-то глобальная обработка ошибок.То, как будет осуществляться обработка, зависит от вашего продукта (вы не хотите просто ничего не делать и регистрировать что-то в тонком файле, предназначенном для разработчиков, в системе авиадиспетчера, но это будет работать нормально, если вы создаете драйвер для скажем, принтер :) ).
Полагаю, я говорю о том, что, по моему мнению, этот вопрос вам следует решать на уровне дизайна вашего приложения, а не на уровне реализации.- И, к сожалению, волшебных решений не существует :(