2008-12-05 4 views
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. 
} 

Мой commen ts к стилям:

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

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

ответ

2

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

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

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

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

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, как показано выше.

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


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

+0

Я вообще не согласен с тем, что NVI - это хорошее решение в конкретном случае, о котором я говорил. Было бы хорошо, если бы Принтер был базовым классом, но добавив, что проводка до необходимость очевидна, часто бывает напрасно. Если я увижу необходимость вывести из принтера, тогда я бы реорганизовал в то время. – 2008-12-18 10:28:57

1

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

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

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

1

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

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

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

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

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

Смежные вопросы