Design by Сontract
От: Юрий Жмеренецкий ICQ 380412032
Дата: 12.04.08 01:50
Оценка: +1 -1
Это, собственно, ответ на сообщение из этой
Автор: Odi$$ey
Дата: 02.04.08
ветки.

_FR>Вот, кстати, отыскал одной С++-ной библиотеке (comutil.h из поставки 2008ой студии):

_FR>
_FR>inline BSTR _bstr_t::Detach()
_FR>{
_FR>    _COM_ASSERT(m_Data != NULL && m_Data->RefCount() == 1);

_FR>    if (m_Data != NULL && m_Data->RefCount() == 1) {
_FR>        BSTR b = m_Data->GetWString();
_FR>        m_Data->GetWString() = NULL;
_FR>        _Free();
_FR>        return b;
_FR>    } 
_FR>    else {
_FR>        _com_issue_error(E_POINTER);        
_FR>    }
_FR>}
_FR>


_FR>Вот это красота. На ней моя позиция и основана: если условие зависит от "наружных" данных, то надо делать проверки и бросать исключения. Асерт тут опционален и требует отдельного обсуждения.


Никакая это не красота... И вот почему:

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

Пример:
void stateA(input i)
{    
  if(i == B)
    stateB(i);
  else if(i == C)
    stateC(i);
  else
    stateD(i);
}

void stateB(input i)
{
  assert(i == B);
  if(i != B)  // абсурд...
  //...
}


По аналогии с лексером: в случае если все stateX сами проверяют свои параметры — лексер получится с откатами. он входит в одно состояние — обламывается, запускает процедуру отката, входит в следующее — обламывается и т.д. Обломы соответственно проявляются в виде исключений(кодов возврата) что приводит тому, что в КА появляются новые(искуственные) состояния. Если же мы выполняем все пред/пост условия, то никаких лишних движений выполнять не приходится.

Еще пример:
// pre: a > 0
void f1(int a);

// pre: a, b > 0
void f2(int a, int b)
{
  assert(a > 0); // это обязано выполняться
  assert(b > 0); // и это тоже, иначе - нарушение контракта со всеми вытекающими

  if(b == 2)
    f1(a); // Здесь не должно быть никаких проверок, 
           // т.к. предусловие для f1 уже выполнено
  ///...
}

Теперь вот об этом:
// pre: a > 0
void f1(int a)
{
  assert(a > 0);
  if(a <= 0) // все так же - абсурд..
    throw invalid_argument();
  ///...
}

Вопрос: Почему проверка производится именно здесь ? Если клиент заинтересован в успешном выполнении, то но сам может проверить условие... Ответ, вероятно, будет такой: "чтобы не дублировать проверки при многократных вызовах(в различных местах)"

Но с другой стороны:
Как уже было сказано выше, это насильно создает новые состояния. В клинических случаях они сливаются в одно — в корневом try/catch:
try
{
 return real_main(args);
}
catch(...)
{
  message("oops");
  return -1;
}

Тем самым замазывая/скрывая потенциальную ошибку.
Хорошо, клиент может перехватывать исключения(в отдельном состоянии):
// pre: a > 0
void f1(int a)
{
  assert(a > 0);
  if(a <= 0)
    throw invalid_argument();
}

void f(int a)
{
  try
  {
    f1(a); // на 'а' нет ограничений
  }catch(invalid_argument)
  {
    stateR();
  }
}


Но чем это лучше чем:
void f(int a)
{
  if(a <= 0)
    stateR();
}

?
Еще небольшое имхо по поводу 'catch(...)':
Если исключение возбуждается только для того что бы быть перехваченным в корневом обработчике, то(за редкими исключениями) имеет место такая трансформация:
было:
if(!condition())
  throw exception();
     
стало:
assert(condition());

Иначе это "замазывание" потенциальной ошибки.

P.S. Такой поход c пред/пост условиями — собственно и есть subj. При чем тут assert'ы ? Они просто наблюдают за выполненим контракта и процессами перехода в тех местах где это не может сделать компилятор(статически). И если срабатывает assert, то код требует изменения. Точно так же как и в случае если "срабатывает"(compilation failed) компилятор. В идеале нарушения контракта надо детектировать на этапе компиляции. Превращение нарушений контракта в ошибки(исключения, etc.) выполнения недопустимы. Точнее, в работающей программе нарушений контракта не может быть в принципе.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.