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.) выполнения недопустимы. Точнее, в работающей программе нарушений контракта не может быть в принципе.
Re: Design by Сontract
От: Андрей Коростелев Голландия http://www.korostelev.net/
Дата: 12.04.08 10:11
Оценка: 1 (1) +1
Здравствуйте, Юрий Жмеренецкий, Вы писали:

ЮЖ>Как уже было сказано выше, это насильно создает новые состояния. В клинических случаях они сливаются в одно — в корневом try/catch:


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

Вообще, в такого рода обсуждениях не может быть универсального правильного совета, пока не ясно, что представляет собой f(). Очевидно, что подход к проверке предуловий будет различен, в случае, если f() является частью loosely-coupled интерфейса для конечного пользователя, и в случае, если f() предназначена для взаимодействоя в рамках high-cohesive системы.
-- Андрей
Re: Design by Сontract
От: Геннадий Васильев Россия http://www.livejournal.com/users/gesha_x
Дата: 12.04.08 20:06
Оценка:
Здравствуйте, Юрий Жмеренецкий, Вы писали:

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


Э-э-э... Стой. Раз уж мы формулируем контракт, то я не вижу никаких причин к тому, чтобы отказываться от выбрасывания исключений при его нарушении. Тестовые сценарии далеко не всегда покрывают всё множество ситуаций, с которыми может столкнуться программа, потому не вижу смысла отказываться от проверок в run-time. Мы всегда должны предусматривать возможность недопустимости входных данных, если не отследили все возможные варианты использования. Так что, деваться некуда...

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

Одним словом, что так поверни, что эдак... Что тебе не нравится-то?
Я знаю только две бесконечные вещи — Вселенную и человеческую глупость, и я не совсем уверен насчёт Вселенной. (c) А. Эйнштейн
P.S.: Винодельческие провинции — это есть рулез!
Re[2]: Design by Сontract
От: Юрий Жмеренецкий ICQ 380412032
Дата: 13.04.08 14:07
Оценка: +2
Здравствуйте, Геннадий Васильев, Вы писали:

ГВ>Здравствуйте, Юрий Жмеренецкий, Вы писали:


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


ГВ>Э-э-э... Стой. Раз уж мы формулируем контракт, то я не вижу никаких причин к тому, чтобы отказываться от выбрасывания исключений при его нарушении.

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

ГВ>... Мы всегда должны предусматривать возможность недопустимости входных данных, если не отследили все возможные варианты использования.

Зачем всегда? только на верхнем уровне. Проверка входных данных при использовании DbC будет выполняться автоматически как часть предусловий. Повторная проверка становится ненужной в принципе.

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

ГВ>Одним словом, что так поверни, что эдак... Что тебе не нравится-то?

Вот это не нравится:
assert(condition()); /*1*/
if(!condition())     /*2*/
 //...

Имхо, либо только 1(DbC), либо только 2(тоже применяемый подход и я его не отрицаю) но не 1 и 2 вместе.
Re[2]: Design by Сontract
От: Юрий Жмеренецкий ICQ 380412032
Дата: 13.04.08 14:08
Оценка:
Здравствуйте, Андрей Коростелев, Вы писали:

АК>Здравствуйте, Юрий Жмеренецкий, Вы писали:


ЮЖ>>Как уже было сказано выше, это насильно создает новые состояния. В клинических случаях они сливаются в одно — в корневом try/catch:


АК>Если введение нового ошибочного состояния не вписывается в текущий контекст — ошибка просто прокидывается выше по стеку вызовов, покуда не встретится контекст, знающий что с ошибкой делать. Причем, при использовании исключений для этого как правило даже не придется писать дополнительный код.


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

АК>Вообще, в такого рода обсуждениях не может быть универсального правильного совета, пока не ясно, что представляет собой f().


Речь не об универсально-правильном совете...

АК>Очевидно, что подход к проверке предуловий будет различен, в случае, если f() является частью loosely-coupled интерфейса для конечного пользователя, и в случае, если f() предназначена для взаимодействоя в рамках high-cohesive системы.


Какой может быть подход в проверке предусловий? предусловия не нужно проверять. В этом смысл DbC. Они _всегда_ выполняются. Иначе теряется их смысл...
Если есть контракт(и не важно в чем он выражен, в документации, в ограничении на типы или другими путями), то он должен соблюдаться. Контекст использования в данном случае не имеет значения. При этом данные(аргументы, etc.) будут проверены явно при первой возможности.
Re[3]: Design by Сontract
От: Андрей Коростелев Голландия http://www.korostelev.net/
Дата: 13.04.08 15:31
Оценка:
Здравствуйте, Юрий Жмеренецкий, Вы писали:

ЮЖ>Вот это не нравится:

ЮЖ>
assert(condition()); /*1*/
ЮЖ>if(!condition())     /*2*/
ЮЖ> //...

ЮЖ>Имхо, либо только 1(DbC), либо только 2(тоже применяемый подход и я его не отрицаю) но не 1 и 2 вместе.

Дело в том, что под assert-ом не стоит понимать assert из стандартной библиотеки С++. Наличие дополнительной проверки в приведенном тобой коде — это лишь следствие особенности реализации assert-а в С++.

Abstractions should not depend upon details. Details should depend upon abstractions.

Dependency Inversion Principle
-- Андрей
Re[3]: Design by Сontract
От: Андрей Коростелев Голландия http://www.korostelev.net/
Дата: 13.04.08 16:39
Оценка:
Здравствуйте, Юрий Жмеренецкий, Вы писали:

АК>>Если введение нового ошибочного состояния не вписывается в текущий контекст — ошибка просто прокидывается выше по стеку вызовов, покуда не встретится контекст, знающий что с ошибкой делать. Причем, при использовании исключений для этого как правило даже не придется писать дополнительный код.


ЮЖ>Если такой контекст не существует, то исключение будет перехвачено в корневом обработчике. Мой поинт в том что этот обработчик на самом деле _не_знает_ что что делать с этим исключением. и замалчивание — есть скрытая ошибка.


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

АК>>Очевидно, что подход к проверке предуловий будет различен, в случае, если f() является частью loosely-coupled интерфейса для конечного пользователя, и в случае, если f() предназначена для взаимодействия в рамках high-cohesive системы.


ЮЖ>Какой может быть подход в проверке предусловий? предусловия не нужно проверять. В этом смысл DbC. Они _всегда_ выполняются. Иначе теряется их смысл...


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

Я говорю, что для того, чтобы это было верно, вызывающий и вызываемый код должны быть связаны друг с другом в степени, достаточной для того, чтобы вызываемый доверял вызывающему. Другими словами, вызывающий и вызываемый должны быть частью некоторой high-cohesive системы. Очевидно, что такой степенью доверия не обладают системы с loosely-coupled интерфейсами, где вызываемый код (например, библиотека) должен быть лоялен к его неверному использованию конечным пользователем (приложением).
-- Андрей
Re[4]: Design by Сontract
От: Юрий Жмеренецкий ICQ 380412032
Дата: 13.04.08 17:48
Оценка:
Здравствуйте, Андрей Коростелев, Вы писали:

АК>Здравствуйте, Юрий Жмеренецкий, Вы писали:


ЮЖ>>Вот это не нравится:

ЮЖ>>
assert(condition()); /*1*/
ЮЖ>>if(!condition())     /*2*/
ЮЖ>> //...

ЮЖ>>Имхо, либо только 1(DbC), либо только 2(тоже применяемый подход и я его не отрицаю) но не 1 и 2 вместе.

АК>Дело в том, что под assert-ом не стоит понимать assert из стандартной библиотеки С++. Наличие дополнительной проверки в приведенном тобой коде — это лишь следствие особенности реализации assert-а в С++.


В первом посте я немного поторопился, — под _COM_ASSERT имелся ввиду именно assert из C++.
Всяческие ASSERT, _COM_ASSERT, MY_ASSERT,... возможно имеют другую семантику.

Смысл DbC:
* Существует контракт(спецификация) между клиентом и поставщиком, одним из компонентов которого являются пред/пост условия.
* Нарушение контракта с чьей-либо стороны -> дефект
* дефект -> программа некорректна и подлежит исправлению.

Мои утверждения:
Ограничения на входные параметры являются предусловиями. Обеспечение выполнения предусловий — это задача клиента, а не поставщика. Понятно, что в сам контракт можно добавить выброс исключения при неверных параметрах. Но это будет уже не assert, а один из путей выполнения контракта. Имхо, плохого, т.к. предусловия отсутствуют и(как бы странно это не звучало) мы не в состоянии узнать когда нам нужно вызывать метод. Если есть предусловия — то все просто: как только они выполняются(*) — вызываем метод. В случае отсутствия предусловий можно только _попытаться_ вызвать метод и проверить результат.

(*) это явное место проверки(или вывод корректности из текущего окружения).
Re[4]: Design by Сontract
От: Юрий Жмеренецкий ICQ 380412032
Дата: 13.04.08 18:13
Оценка:
Здравствуйте, Андрей Коростелев, Вы писали:

АК>Пусть приложение использует некоторый API. Документация на этот API, кроме всего прочего, содержит спецификацию ошибок, возникающих при работе с ним. Если в приложении не предусмотрена обработка какой-нибудь из этих ошибок, значит это проблема дизайна приложения.


Теоретически — да. В таком случае может возникнуть ситуация, когда мы не в состоянии корректно обработать возникщую ошибку. Но на пректике обработать абсолютно все возможные варианты малореально. Здесь надо как-то разделять ошибки по неким классам эквивалентности и гарантировать наличие стабильных состояний при их возникновении.

ЮЖ>>Какой может быть подход в проверке предусловий? предусловия не нужно проверять. В этом смысл DbC. Они _всегда_ выполняются. Иначе теряется их смысл...


АК>Ты утверждаешь, что ответвенность за соблюдение предусловий — всегда забота вызывающего, и вызваемый код должен полагаться на то, что предусловия выполнены. В качестве механизма, регулирующего это поведение, вызываемый код используюися assert-ы.


Да.

АК>Я говорю, что для того, чтобы это было верно, вызывающий и вызываемый код должны быть связаны друг с другом в степени, достаточной для того, чтобы вызываемый доверял вызывающему. Другими словами, вызывающий и вызываемый должны быть частью некоторой high-cohesive системы. Очевидно, что такой степенью доверия не обладают системы с loosely-coupled интерфейсами, где вызываемый код (например, библиотека) должен быть лоялен к его неверному использованию конечным пользователем (приложением).


Согласен. Но в этом случае(на границах trusted regions) все данные будут проверяться явно. Например, данные полученные от UI будут проверятся явно, поскольку для более нижних уровней существуют опять таки, предусловия.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.