Как стать автором
Обновить

Комментарии 56

Респект за статью, пишите еще. Поддержал, как смог
Спасибо. буду стараться.
Когда ожидать продолжения?
НЛО прилетело и опубликовало эту надпись здесь
Спасибо! С удовольствием прочитал бы продолжение!

А кстати избавить код от некрасивых проверок возвращаемого функциями значения также может паттерн Null object
en.wikipedia.org/wiki/Null_Object_pattern
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
да, конечно, гарантирует завершениЕ подпрограммы. Спасибо! Исправил.
Кстати, да. Перенесите топик в ненормальное программирование, или еще куда — пусть и другие прочитают.
Я у себя для такого случая завёл макросы
(сразу оговорюсь, это Си++)
precond(ptr != 0); invariant(sz = size()); postcond(*ptr > 0);

В отличие от стандартного assert'а, эти кидают исключения в релизе, а в дебуге это просто ассерт.
Конечно, assert предполагает то, чего не должно быть никогда (т.е. если ассерт сработал, надо менять код программы), но к сожалению, люди ошибаются, а в релизе если выстрелит, то ассерт там уже промолчит, а программа навернется позднее и окажется в неизвестном состоянии.
Просто исключения я предпочитаю кидать тогда, когда ошибка вполне может быть, и код программы тут не виноват.
Ну а коды возврата — это когда и ошибки-то никакой нет по сути, вполне нормальная ситуация.
Конечно, возможны разные варианты. Например, можно обойтись вообще без генерации исключений. Например, предположим, что вызывающая программа ничего не знает (да и не должна знать) о классе ComponentInfo. Она его просто делегирует другому классу, который и производит различные манипуляции с этим объектом. Конечно, в этом случае генерация исключения в приведенном примере нам не подходит. Вот небольшой модифицированный примерчик этого:

public ComponentInfo GetComponentInfo(string id)
{
   ComponentInfo info = new ComponentInfo();
   try
   {
     //собираем этот объект разными способами
    //с возможной генерацией исключения   
   }
   catch(ContractException ex)
   {
      //обеспечиваем возвращение валидного объекта
      info.Name = «Не известно»;
      info.Description = «Искомая деталь не найдена»;
      info.Price = 0;
   }
   return info;
}* This source code was highlighted with Source Code Highlighter.


Кстати, в «доверительном варианте» try-catch не нужен, исключение же всё равно полетит дальше.
Или это для примера так написано?
да, try-catch можно смело убрать :)
пытался сделать пример нагляднее
НЛО прилетело и опубликовало эту надпись здесь
исходя из ваших примеров (первый и последний) вы просто выкинули вылидацию
>Вся ответственность за передачу «правильных» данных лежит на вызывающей программе.
поясните пожалуйста вот эту часть

у вас сохранились конспекты этой лекции?
Да, валидацию я выкинул. Это как раз и есть основа принципа проектирования по контракту.
Очень много литературы учит нас использовать так называемое «защитное программирование» (defensive programming) (в следующих статьях постараюсь сделать сравнение обоих подходов).
Оно и говорит, что лучше на всякий случай перестраховаться и проверить (доверяй, но проверяй ;)).
А проектирование по контракту предлагает использовать некое соглашение, контракт, чтобы убрать лишние проверки (тем самым уменьшив объем кода и упростив читабельность программы).
Попробую привести пример из реальной жизни.
Допустим, наша компания производит слоеное тесто для выпечки пирогов. С конечным покупателем (читай, пользователем) мы заключаем такой контракт:
Если покупатель предварительно 15 минут разморозит наше тесто, затем сдобрит его маслом и будет выпекать в течении 10 минут, то у него получатся вкусные пирожки.
Т.е. мы обязуемся предоставить покупателю готовый продукт при выполнении некоторых условий.

Но тут другая компания (ООО «СкоростьНашеВсе») решила помочь покупателям и продавать уже готовые горячие пирожки.
Она решила не тратить время на разморозку и выпекала всего 7 минут. В итоге, пирожки ее приготовления получились не очень вкусными и несовсем готовыми.
А вот тут уже конечному покупателю выбирать: покупать пирожки этой фирмы, обратится к другой или сделать все самому по инструкции (читай, контракту).

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

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

//вызывающая программа


//обеспечиваем предусловия для вызываемого метода
if(String.IsNullOrEmpty(objectName) || objectName.Contains(«@»))
{
   //выводим сообщение об ошибке, прекращаем выполнение программы или передаем управление куда-либо
}

SomeObject some = GetSomeObject(objectName);

//т.к. мы выполнили предусловия по нашему контракту, то можем смело дергать методы объекта
//не боясь «object reference» исключения
Console.WriteLine(some.Name);
Console.WriteLine(some.Description);

...


//вызываемая программа

//нет необходимости выполнять проверку на пустую строку или еще на что-нибудь
//полагаемся на контракт
public SomeObject GetSomeObject(string objectName)
{
   //по условиям контракта должны вернуть объект
   SomeObject result = new SomeObject();

   try
   {
      //собираем объект
   }
   catch(SomeException ex)
   {
      //не удалось собрать объект
      //но условия контракта выполнить надо
      result.Name = «Unknown name»;
      result.Description = «Can't find object»;
   }
   
   return result;
}
получается что вместо одной проверки внутри вызываемой подпрограммы, мы делаем проверку перед каждым ее вызовом? Что мы тогда выигрываем в читабельности кода, если мы вынесли проверку из оной подпрограммы и разместили во всех местах ее вызова?
согласен, появляется соблазн сделать проверку внутри подпрограммы. Приведу классический пример с функцией вычисления квадратного корня (назовем ее CalcSqrt(), т.е. это будет наша функция (абстрагируясь от реализации для различных языков)).
Данная функция имеет предусловие — ее аргумент не должен быть отрицательным. Если, к примеру, пользователь вводит отрицательное, то это забота вызывающей программы проверить это число.
Одна вызывающая программа завершит работу аварийно, другая выдаст предупреждение и заставит пользователя вводить еще раз значение. А третья программа вообще схитрит — она сделает это число положительным, а потом, когда получит результат, то прибавит мнимую единицу.
Поэтому, это не забота функции CalcSqrt() обрабатывать предусловия.
Но ведь вызванная функция может сгенерировать исключение, а вызывающая — обработать его как хочет, чем это плохо?
Тем, что нужно исключение(я) ловить и обрабатывать. DBC говорит о том, что если перед вызовом ситуация удовлетворяет предусловию функции, то она выполнится без ошибок и ничего обрабатывать не придётся. И перед вызовом проверять ничего не нужно, т.к. убедиться в выполнении предусловия можно исходя из постусловий и инвариантов предшествующих вызовов. Проверять нужно только внешние данные, от которых нельзя ждать выполнения каких-либо контрактов.
А если функция вызывается много раз на разных данных? Обрамлять её каждый раз проверками утомительно, а обработку исключений можно написать в один блок после вызовов.
так я об этом и писал чуть выше. возможны вызовы различными клиентами, соответственно, возможны и различные поведения в случае ошибки. Или вы предлагаете создать несколько типов объектов-исключений для описанного выше примера?
Кроме того, г-н Blackened написал хорошее замечание чуть выше.
> DBC говорит о том, что если перед вызовом ситуация удовлетворяет предусловию функции, то она выполнится без ошибок

К сожалению это так только если не взаимодействовать с внешним миром.
Если же имеем обрывы связи, файлы с оибками и т.п — то надо заранее это все обрабатывать и куда-то делать эти предпроверки. Возможно в функции типа bool ВсеOK?(окружение).

Эхх если бы среда програмирования сама прятала бы эти проверки — то мороки бы и не было. Были бы они в Debug mode в виде assert-ов, но показывались только по желанию. Чтобы красиво было :)
>Если же имеем обрывы связи, файлы с оибками и т.п — то надо заранее это все обрабатывать и куда-то делать эти предпроверки… Эхх если бы среда програмирования сама прятала бы эти проверки

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

> Были бы они в Debug mode в виде assert-ов, но показывались только по желанию.

assert-ы к внешнему миру не применимы, т.к. они помогают удостовериться, что всё идёт так, как предполагает разработчик, а предположения относительно внешнего мира делать трудно.
Лекций, к сожалению, нет :(
Могу посоветовать вам почитать его книгу «Object-Oriented Software Construction», Bertrand Meyer, Prentice Hall, 2nd edition 1997.
Спасибо за интересный материал. Надеюсь, развитие темы не задержится? :)
Забавная штука — research.microsoft.com/SpecSharp/, как раз по теме. Вот только бы развили ее до промышленного уровня.
К сожалению нормально заработала у меня только на VS2008 SP1
По-логике, все условия и инварианты должны быть статически проверяемые, т.е. все глюки отлавливаются на этапе компиляции. Но это только в теории, на практике две проблемы:

1. Контракты формулируют довольно сложные математические теоремы над данными, автоматическое доказательство\опровержение которых, часто очень трудоёмкий\ресурсоёмкий (читай длительный) процесс. Компилировать исходник по часу не каждому понравится.

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

Несколько лет уже размышлю над контрактами, и чем больше думаю, тем чаще вспоминаю виртовский Оберон :)
Spec# как раз и пытается решить задачу статической проверки соблюдения контрактов еще на этапе компиляции
Пытается, только что он будет делать когда я потребую в контракте передачу функции только n-битного простого числа, и передам например такое:

25195908475657893494027183240048398571429282126204032027777137836043662020
70759555626401852588078440691829064124951508218929855914917618450280848912
00728449926873928072877767359714183472702618963750149718246911650776133798
59095700097330459748808428401797429100642458691817195118746121515172654632
28221686998754918242243363725908514186546204357679842338718477444792073993
42365848238242811981638150106748104516603773060562016196762561338441436038
33904414952634432190114657544454178424020924616515723350778707749817125772
46796292638635637328991215483143816789988504044536402352738195137863656439
1212010397122822120720357

Компилировать программу придётся не один год в распределённой сети из не одной тысячи машин :)
Ну, даже банальная проверка на null может сэкономить некоторое количество нейронов. Кроме того контрактное программирование без документации, имхо, это смерть. Спасает только читабельная сигнатура метода типа

string ToPhysicalPath(string notNullVirtualPathWithSlashesAsSeparatorsAndNoEndingSlash) :)

Spec# же включает хоть какое нибудь описание контракта в сигнатуру метода, которая потом в IntelliSense отображается
>Кроме того контрактное программирование без документации, имхо, это смерть.
вот это самое главное замечание, которое я, к своему стыду, забыл упомянуть.
На счёт проверок на null, попадалась мне на глаза с полгода назад любопытная «бумага», описывался в ней способ статической проверки на основе уже имеющейся информации об исключениях в существующем C#/CIL коде. Вывод был такой: если в программе обработка и кидание исключений сделаны правильно, надобность в пред/пост условиях не столь велика. Хотя, для отделения мух от котлет, конечно лучше иметь в арсенале и исключения и весь спектр средств контрактного программирования.

Радует, что программирование наконец-то переходит на семантический уровень, только и «мозг» компиляторма надо будет иметь соответсвующий. Отчего-то вспоминается проблема останова ;)
Интересный материал. Надо будет попробовать. Жду продолжения
очень интересен процесс модифицации кода(от рефакторинга, до изменения функционала), тестирования и дебага. Не смотря на парадигму «обязался — сделай» всеравно возможны ошибки, которые довольно просто найти используя валидацию.
Быть более конкретным — как я узнаю в большой системе почему «пирожки ее приготовления получились не очень вкусными и несовсем готовыми»?
Весьма интересный подход. Записываюсь в ряды ожидающих продолжения.
(for var i = 0; i < links.length; i++) {
(function(i) {
links[i].onclick = function() {
alert(i);
}
})(i);
}
окном ошибся =)

это не контракт, а доверенность. а когда все друг-другу доверяют — невыполнение контракта одним звеном приводит к падению другого и танцам с бубном вокруг отладчика

а вот контракт выглядит так: каждый объект обязуется реализовать некоторый интерфейс (подписывает контракт) и за выполнением контрактов следит специальный объект-аудитор (обычно его роль возлагается на интерпретатор, что сильно уменьшает гибкость). соответственно на продакшене и на тестинге могут быть разные аудиторы — разной степени скрупулёзности и с разными последствиями в случае нарушения контракта.
Вот мой совет для данной ситуации – если вы вызываете подпрограмму, которая была написана вами, то не пишите лишнего кода. Доверяйте себе.


Не согласен. Поступая таким образом, вы устанавливаете неявные условия, необходимые для корректной работы программы, и если они будут нарушены (кто-то или вы сами случайно передадите неверные исходные данные), то программа поведёт себя непредсказуемо. Я считаю, что нужно избегать таких неявных зависимостей, так как это грозит неприятностями в будущем.
интересная методика. Местами согласен.
Но не для русских программистов. Мозг построен немного не так =)
НЛО прилетело и опубликовало эту надпись здесь
> Центральными фигурами это концепции
Скорее всего, имело ввиду: «Центральными фигурами этоЙ концепции».
Первый пример «качественного ПО» пропустит код длиной более 9 символов.
спасибо. поправил
Постусловия выражают состояния «окружающего мира» на момент выполнения подпрограммы.

Мне кажется лучше сказать на момент завершения подпрограммы.

Кроме того, наличие постусловия в подпрограмме гарантирует ее завершение (т.е. не будет бесконечного цикла, например).


Интересно, что в классической логике Хоара наличие постусловия никак не связано собственно с её завершением. Формулируется два понятия:

Программа с предусловие P и постусловием Q корректна, если начиная работать на входных данных удовлетворяющих P, она либо не завершается, либо завершается в состоянии удовлетворяющем Q.

Программа с предусловие P и постусловием Q тотально корректна, если начиная работать на входных данных удовлетворяющих P она завершается в состоянии удовлетворяющем Q.

Вообще завершение или не завершение программы больше зависит от предусловия, чем от постусловия.
Мног интересного, для размышления. Насколько понимаю, такой код по сути не переносим в другие проекты? Тогда польза такого кода в его компактности?
Переносим, но только вместе со своими контрактами
Почему не переносим? Очень даже, проверено опытом. ;)
Как правильно было сказано — переносим со своими контрактами (важна документация данных контрактов). Кроме того, переносимость наоборот облегчается за счет снижения связанности модулей.
А уж использовать такой модуль или нет (со всеми контрактами) — решать клиенту. См. мой комментарий про пирожки ;)
наличие постусловия в подпрограмме гарантирует ее завершение


Очень странная для меня фраза. Банальная проверка завершения программы во всех случаях (т.н. Проблема остановки), как мне известно, ещё не имеет общего решения.
а есть пример явного профита? типа: до контракта программист мучился, после применения контракта все встало на свои места. Или: до применения контракта тратили 2 часа, после применения 2 минуты
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории