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

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

Так и пришли к концепции Хаскеля.
Объясните подробней, как это относится к концепции Хаскеля? Интересно.
Здесь в статье речь шла о избыточности проверок: классы/компоненты/модули обычно устраиваются так, что они могут работать полностью независимо от остальных, — бери и включай в другой проект. Из-за своей универсальности им приходится проводить самостоятельные проверки входных данных: в одном проекте их посадят за ту самую баррикаду и будут кормить только отборными продуктами, а в другом — отправят на передовую, и мало ли, что их ожидает. Авторы же концепции предлагают, ориентируясь на конкретный проект, убирать лишние проверки.

Хаскель вспомнился потому, что там строгое разделение чистой функциональной части от операций с побочными действиями, которые являются потенциально опасными. Пока мы в функциональной части, вокруг нас кристальной чистоты мир, радуга и прочие соответствующие атрибуты. А при выходе наружу — IO и др. — нас уже могут покусать злые волки.
Вообще, может быть, сравнение не совсем корректное, но что-то общее есть.
Развейте мысль, пожалуйста?
Легко.
Начнём со связи с внешнем миром. В хаскеле для этого используется полиморфный тип IO, который прозрачно намекает что все обращения к такому типу могут сопровождаться побочными эффектами.

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

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

Дальше идёт следующий встроенный барьерный механизм: например класс Functor. Он позволяет совершать операции над полиморфным типом: то есть для типа f a (а — параметр) и функции (a -> b)
получать тип f b:
fmap :: Functor f => (a -> b) -> f a -> f b

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

let input = readFile "input.file"
let result = fmap clearLogic input
output result


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

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

любая монада на самом деле является аппликативным функтором. Только в силу исторических причин
объявление монады не означает объявления функтора.

Соответственно, монада IO, про которую написано на каждом столбе, является функтором и позволяется безопасно перерабатывать значения внутри себя не опасаясь любых побочных эффектов. Это куда лучше, там традиционный монадный синтаксис:
>>= :: Monad m => (a -> m b) -> m a -> m b

Поскольку такая запись предполагает, что каждый участок вычислений [a -> m b] чистые данные превращает в какую-то гадость (то есть порождает внешние эффекты, ведь m = IO)
НЛО прилетело и опубликовало эту надпись здесь
ой, да все знают «как надо», но почему-то на выходе получается другое. и сразу же оп и все знают почему так вышло
НЛО прилетело и опубликовало эту надпись здесь
Примеры кода где? Системы, которые построены по такому принципу где? Требую кода и зрелищ!
Любая программа на Haskell, взаимодействующая с внешним миром через монады.
Макконнелл, «Совершенный код», страницы 200-220 печатного издания. Копипастить сюда 20 килобайт текста не хочу.
Обычно достаточно сделать выжимку из самых ярких примеров данного подхода и опубликовать ее тут. Никому не интересно читать 20 страниц ради понимания того, что же это за зверь такой новомодный, баррикада…
«защита от дурака»
Вот ссылок на соответствующие изречения Макконнелла и Фаулера явно не хватает.
Дык раз уж знаете, что такие есть — так и сослались бы…
В посте написано: "О баррикаде писали в своё время Макконнелл, Фаулер и может быть кто-то еще."
Вот я и хочу, чтобы ссылки были не голословными, а реальными.
Отписал выше.
Создать защиту от дурака практически невозможно, ведь дураки так гениальны :)
Я бы сказал — совершенно непредсказуемые, на счёт гениальности — не факт.
«Никогда не стоит недооценивать непредсказуемость тупизны» (Большой Куш в переводе Пучкова)
Вспомнилось изречение Бисмарка

«Никогда не замышляйте ничего против России! На любую вашу хитрость Россия ответит своей непредсказуемой глупостью»
В своё время тоже задавался таким вопросом. Правда в разрезе: должны ли protected и private методы проверять входные аргументы как это делают public методы или можно ограничиться Assert'ом. На практике пришлось задуматься о рефакторинге — теперь это непросто замена модификатора доступа, а переписывание всех Assert'ов на нормальные проверки. В итоге победила лень — практически не использую Assert'ы)
у меня местами ассерты по объему сравнимы с кодом
Подход интересный ) Кстати, в web-приложениях как правило есть очевидное место для построения баррикады — прием данных из запросов — и там она обычно и формируется)
Плюс не очевидное для проектировщика (или программиста) поведение пользователя: нажал кнопку Назад или обновил страницу «не вовремя» или внезапно несколько раз и прочее.
Ну да, т.е. проверка состояния тоже важна.
Я бы не сказал, что настолько оно очевидное. В MVC приложениях куда логичнее, по-моему, проверять данные при помещении в модель.
По сути это примерно тоже самое, что я и имел ввиду.
Или я вас неверно понял, или сам не точно выразился :) Я имел в виду, что сами методы проверки находятся в классах модели (или тесно с ними связаны). Что контроллер анализом запроса на корректность не занимается, а просто преобразует HTTP-запрос в понятный для модели вид, в параметры методов и передаёт ей, а уж модель может сообщить контроллеру, что данные некорректны. Не
if ($_GET['param'] > 0) {
  $model->setParam($_GET['param']);
  render('Success');
}
else {
  render('Error', 'Param is not positive');
}
, а, например,
if ($model->setParam($_GET['param'])) {
  render('Success');
}
else {
  render('Error', $model->getErrorMessage());
}

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

P.S. Кстати, «баррикада» — ещё один довод в пользу использования сеттеров/геттеров в модели, а не public свойств.
А если у вас три классических слоя и каждый разрабатывает независимый человек,
или еще интереснее, во втором слое 10 сервисов, каждый из которых разрабатывает независимый человек (который еще и работает на субподряде и контракт у него заканчивается) где тут провести баррикадную черту?

Если не проверять параметры траблшутинг превратится в настоящий ночной кошмар.

«Профайлер показывает, что существенная часть времени уходит именно на проверки. Программа тормозит. Пользователи жалуются.»

Что может быть проще и быстрее проверки на null, или на >0. Если от такого ваша «программа» тормозит у вас серьёзные проблемы ;)
>Что может быть проще и быстрее проверки на null, или на >0. Если от такого ваша «программа» тормозит у вас серьёзные проблемы ;)

а две проверки?
а если функция вызывается в цикле?
а в цикле по битмап-буферу?
Можно делить float на float, любой на любой. В случае N/0.0 получается ± INF, если N != 0.
Сколько можно эту чушь про исключения писать.
а ничего, что кроме JS есть другие языки?

In [2]: float(0)/float(0)
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)

/home/muromec/ in ()

ZeroDivisionError: float division
Я на C++ проверял.
Впрочем я немного погорячился. Деление на ноль выставляет флаги, которые можно проверить и бросить исключение. Возможно некоторые языки так делают.
Что за язык кстати?
Python.
Статья не про деление на ноль, а про валидацию данных.
Да, оффтоп конечно. Но после таких примеров люди вставляют проверки там, где они не нужны.
Что значит не нужны? В подавляющем большинстве случаев деление на нуль — не валидная операция.
если формально подходить, то 9mm прав, функция делает ровно то, что от нее ожидают — делит. :) вот только пример был чисто для абстракции, а в этом оффтопе комменты уже до 5 уровня дошли.
А по-моему, пример как раз очень удачный. Ведь если функция ничего кроме деления не делает, то почему бы не использовать стандартный оператор деления?
А вот если функция понадобилась, значит, это не просто деление, а и дополнительные проверки, которые, если их делать каждый раз при выполнении операции деления в коде, — получится куча дублированного кода.
Например, при делении на нуль хочется всегда выкидывать какой-то свой exception, потому что в нашем приложении такая операция не валидна.
В подавляющем большинстве случаев как раз валидная.

Невалидной она становится если важен порядок бесконечности, что редкость.
Собственно «оперирование двоичными числами почти так-же как простыми вещественными» и было одний из целей стандарта IEEE 754.

НЛО прилетело и опубликовало эту надпись здесь
Сразу вспомнилась цитата из книги Programming Perl:

The trend over the last 25 years or so has been to design computer languages that enforce a state of paranoia. You're expected to program every module as if it were in a state of siege.


Я уж надеялся, что в конце статьи будет какой-то полезный совет, как избежать превращения кода в баррикаду. Жаль.
Такое чувство, что мы с вами читали разные статьи. Вся статья один большой совет: чтобы избежать превращения всего кода в баррикаду разделите код на обособленную баррикаду и код, которые работает с гарантированно безопасными данными.
… гарантированно безопасными данными, которые все равно проверяются ASSERT'ами потому, что ошибка может быть в самой баррикаде. Я так и не понял, чем это отличается от повсеместного баррикадирования. Если вы об экономии нескольких процессорных инструкций с помощью использования препроцессора, то это применимо только к семейству C, в большинстве же остальных языков разницы не будет никакой.
отличие в том, что все ошибки и некорректности в потоках данных внутри баррикады должны быть выявлены на этапах тестирования, после этого все ошибки это исключительные ситуации, требующие исправления
Тогда уж лучше сразу воспользоваться модульным тестированием:
1. Тестовый код будет заведомо отделен от логики приложения в любом языке
2. Проверка результатов будет на выходе тестируемой функции, а не внутри вызывающей ее — если функция вызывается в 10 местах, не нужно писать 10 ассертов
3. Одинаковый формат тестирования что для «внешних», что для «внутренних» функций
Вы правы, «Во всех внутрибаррикадных методах обязаны стоять ASSERTы» — очевидно эта фраза не подходит для каждого языка и не совсем корректна, правильнее будет сказать, что единственная проверка корректности данных в «безопасной» зоне должна осуществляются в процессе отладки «баррикады». В глобальном смысле на саму концепцию это не влияет на мой взгляд.
поправьте пожалуйста если не прав,
MVC является реализацией данной концепции, если абстрагироваться от назначения компонентов?
Нет, дело не в этом.
Вы можете проверять во View, пришли ли правильные данные, а можете построить баррикаду в контроллере или модели и во View не делать никаких проверок.
Client-side и server-side валидации как раз вписываются в эту концепцию — во View по-быстрому проверяем данные на JavaScript'е, в контроллере уже по-взрослому, ну а всякие сервисы/бизнес-логика/репозитории — это уже на «нашей» стороне баррикады.
Вспомнил цитату из одного руководства:
Ограничивайте, отклоняйте и очищайте пользовательский ввод. Иначе говоря, предполагайте, что весь пользовательский ввод является злонамеренным. Проводите проверку длины, диапазона, формата и типа вводимых данных.
Всегда проверяйте ввод на наличие неверных или злонамеренных данных на стороне сервера,
поскольку проверка на стороне клиента может быть без труда подделана.

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

Здесь хотелось бы немного дополнить. Не для опровержения ваших слов, а для более полного раскрытия темы.

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

Всегда уведомляйте пользователя, что что-то пошло не так, давая нужные ему подробности и советы по исправлению входных данных.
В свою очередь хочу добавить: я по возможности стараюсь делать так — сам барьер работает на стороне сервера, но он особо не заморачивается с объяснениями в чем именно проблема, его задача — проверить данные на 100% и при этом потратить минимум ресурсов (соответственно, за этим барьером проверки уже практически не производятся). А вот на клиентской стороне делаются дублирующие проверки, но более конкретные, которые подскажут что именно не так: то ли длина строки, то ли неверный первый символ, то ли недопустимый символ во всей строке в принципе и т.п.

Таким образом мы снижаем нагрузку на сервер/барьер, сохраняем безопасность, но при этом повышаем информирование пользователя до максимума.

На практике конечно есть свои сложности, особенно в отдельных ситуацих (в идеале нужно обеспечить полное соответствие проверок клиента проверкам сервера/барьера, иначе нужно описывать поведение клиента для ошибки, которую клиент пропустил, но поймал барьер, плюс изменение в спецификации требует изменений в двух местах, плюс не все проверки можно сделать на стороне клиента и т.п.), но в большинстве случаев все отлично работает.
Ну если без фанатизма.
Потому что конструкция on error resume next высмеивает весь этот пост!
Я проводя собеседования на вакансию программера С++ сильно удивился, что про исключения знает менее чем каждый второй соискатель.
эмм. А как эти соискатели говорят, как обычно они управляют обработкой ошибок?
вопрос звучал так: как без анализа содержимого делителя обработать ошибку деления на ноль:

предполагается ответ с try...catch

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

Множество раз замечал, как собеседуемые не отвечают на простейшие вопросы только потому, что считают: «здесь серьёзные люди и серьёзное собеседование — значит вопрос с подвохом. Ну не могут же они такую элементарную вещь спрашивать»…

Но это уже тема отдельной статьи…
вопрос звучал так: как без анализа содержимого делителя обработать ошибку деления на ноль:

предполагается ответ с try...catch


Неправильный ответ. Правильный: средствами C++ это сделать невозможно.
ISO/IEC 14882, глава 5 Expressions [expr], параграф 4 говорит нам следующее:
If during the evaluation of an expression, the result is not mathematically defined or not in the range of
representable values for its type, the behavior is undefined. [ Note: most existing implementations of C++
ignore integer overflows. Treatment of division by zero, forming a remainder using a zero divisor, and all
floating point exceptions vary among machines, and is usually adjustable by a library function. —end note ]


Это UB.

В разных РЕАЛИЗАЦИЯХ C++ это можно сделать при помощи implementation-specific функционала. Более того, такие ошибки практически никогда не транслируются в C++ исключения автоматом.

В Windows будет сгенерировано SEH исключение, которое можно словить через __try/__except либо с помощью _set_se_translator сконвертировать в C++ исключение. Кроме того, если включить обработку асинхронных исключений catch-all блок будет ловить и SEH исключения (примечательно, что именно divide-by-zero использовано в качестве примера в статье про разные типы обработки исключений).

В POSIX системах будет сгенерирован сигнал (SIGFPE), который можно обработать предварительной установкой хендлера.

В общем, задавать на собеседовании вопросы про UB можно только если принимаете на какую нибудь senior/principal должность. Новички этого знать не обязаны — им бы с defined поведением разобраться.
спасибо за развернутый комментарий. К сожалению НИ ОДИН из соискателей не упомянул, что с исключениями не все так гладко. Либо пишут try… catch, либо пишут какую-то ерунду.

Вообще, открою секрет, тест изначально создавался для соискателей на Delphi, там с исключениями все на порядок прозрачнее.
Вопрос «как без анализа содержимого делителя обработать ошибку деления на ноль:
предполагается ответ с try...catch» совершенно не подразумевает, что «про исключения знает менее чем каждый второй соискатель.»

Я думаю, что если бы вы захотели поговорить конкретно об исключениях, их преимуществах и недостатках, то процент ответивших подскочил бы, скажем, до 90%.

A так вы задали вопрос, на который можно ответить, например так:
double foo (int a, int b)
{
if (a — b == a)
{
ProcessError(«ololo»);
return 0;
}
return a/b;
}
и это тоже будет являться приемлимым решением, но неверным с вашей точки зрения.

Вообще, реально напрягают интервьеверы, которые задают вопрос не,- «Как ты использовал исключения в своем последнем проекте», а в стиле,- «Ну-ка, попробуй прочитать мои мысли, сынок!»
Какие бы преобразования Вы ни делали над b — это все равно остается «анализом делителя».
обработка исключения при делении на ноль описана почти в каждой книжке по программированию как академический пример
Обработка исключения при делении на ноль — это ОТВРАТИТЕЛЬНО. Это как в Java unchecked exception — ошибка программирования. Это все равно что контроль выхода за границы массива перехватом ArrayIndexOutOfBoundsException.
А если у вас десяток параметров, которые на ноль надо проверить? тоже каждый анализировать? бред
А если у вас десяток коллекций, по которым надо итерировать, вы тоже не пишете цикл от 0 до collection.size(), а ждете исключения?
не понимаю, что общего у деления на ноль и OutOfBounds?

PS: кстати цикл до .size()-1
Ну в Java например, традиционно считается, что runtime (unchecked) exceptions типа обращения по нулевой ссылке, арифметические ошибки типа деления на ноль, выход за границы массива перехватывать не надо — это дурной тон использовать исключения для управления логикой программы. Эти исключения — ошибки программирования. Если вы обращаетесь файла, а его там нет или у вас нет прав — тут можно выбросить исключение, т.к. вы не можете никогда заранее знать, есть файл или нет, и это нельзя заранее проверить.

Проверить, что переменная равна null / 0, что счетчик не выходит за границу массива, и прочее можно легко предварительно — и это более логично.

Я уж даже не говорю о стоимости выбрасывания исключения (это для тех, кто считает что проверки у них занимают много времени).
>> Если вы обращаетесь файла, а его там нет или у вас нет прав
Наличие файла тоже можно безопасно проверить. Да и права при желании. Вобщем-то все практически можно проверить.
Зачем тогда исключения? WinAPI вон практически без исключений написан. а все потому что не умели им пользоваться некоторые товарищи.
Проверять на каждом шаге ResultCode не всегда хорошая идея, если при неудаче все равно вываливаемся в одно и то же место с выдачей ошибки юзеру.
> предполагается ответ с try...catch

Это неправильный ответ. Для usage-ошибок не надо использовать try...catch.

> более половины пишут что всегда анализируют делитель

И правильно делают. Это не от незнания про исключения, а от умения ими не пользоваться в неподходящих местах.

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

Вы и сами бы не прошли собеседования на джуниора.

> предполагается ответ с try...catch

Правильный ответ (с учётом рассуждений про баррикады) может быть примерно таким (на C#).

Вызываемый код:
private double DivUnsafe(double a, double b)
{
Debug.Assert(!/*корректное сравнение b с 0.0*/);
return a / b;
}

public double Div(double a, double b)
{
// Не допускается выбрасывание из библиотеки ZeroDivisionException,
// NullReferenceException и тому подобных исключений.
if (/*корректное сравнение b с 0.0*/)
{
throw new ArgumentException(...);
}
return DivUnsafe(a, b);
}

В библиотечном коде (по «нашу сторону» баррикад) всегда вызываем DivUnsafe, контекст вызова всегда можем контролировать. Пользовательскому коду (на корректность которого мы не можем прямо влиять) предоставляется Div.
Неправильный вызывающий код:
try
{
c = Div(a, b);
}
cactch (ArgumentException ex)
{
// Обработка ошибки, например:
throw new MyBusinessLogicException(..., a, b, ex);
}

Правильный вызывающий код:
if (/*корректное сравнение b с 0.0*/)
{
// Обработка ошибки, например:
throw new MyBusinessLogicException(..., a, b);
}
с = Div(a, b);
статью не хотите написать на эту тему?
Да просто вопросы соискателям нормальные задавать надо.
Позвольте пофантазировать в php на эту тему:
 class ArgumentException extends Exception {
  public function __construct($message, $code, $previous) {
   parent::__construct($message, $code, $previous);
  }
  
  public function __toString() {
   parent::__toString();
  }
 }
 
 interface Ifoo {
  /**
   *
   * @param mixed $a
   * @param mixed $b
   * @return double
   */
  public function Test($a, $b);
 }

 class fooProtected implements Ifoo {
  
  /**
   *
   * @param mixed $a
   * @param mixed $b
   * @return double
   */
  public function Test($a, $b){
   return $this->_Test();
  }
  
  /**
   *
   * @access protected
   * @param double $a
   * @param double $b
   * @throws divide
   * @return double
   */
  protected function _Test($a, $b) {
   return $a / $b;
  }
 }
 
 class barUnProtected extends fooProtected {
  
  /**
   *
   * @access protected
   * @param mixed $a
   * @param mixed $b
   * @return double
   */
  protected function _Test($a, $b) {
   
   //valid
   if(!is_double($a) || !is_double($b))
    throw new ArgumentException('Some variables not convertible to double!');
   
   //user rule
   if($b == 0)
    return 0.0;
   
   return parent::_Test($a, $b);
  }
 }
 
 /**
  *
  * @param Ifoo $dividor
  * @param mixed $a
  * @param mixed $b
  * @throws ArgumentException on un protected
  * @throws Exception
  * @return double
  */
 function dividePleaseTwoStrangeParams($dividor, $a, $b) {
  try
  {
   if($dividor instanceof Ifoo)
   {
    return $dividor->Test($a, $b);
   }
  }
  catch (ArgumentException $exc)
  {
   echo $exc->getTraceAsString();
   return 0.0;
  }
  catch(Exception $exc)
  {
   echo $exc->getTraceAsString();
   return 0.0;
  }
 }


* This source code was highlighted with Source Code Highlighter.


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

По хорошему придется делать dividePleaseTwoStrangeParams() методом класса, и в нем хранить ссылки на Ifoo обьекты и выбирать по ключу, опять же переданному в качестве аргумента функции… Замкнутый круг какой-то получается…

Подскажите, как научить dividePleaseTwoStrangeParams() самому определять, в каком контексте (в смысле с каким $dividor) выполняться?
Какую проблему вы хотите решить, используя этот класс?
Разделение доступа к методу (читать самого метода) для «Своих» и «Чужих».
Для «Чужих» осуществляются различного рода козни проверок, для «Своих» — сразу результат.
*Пример притянут для функций, пользуются которыми по обе стороны*

Почитав комментарии другое направление: вначале отфильтровали, внутрь пустили только валидные данные.
а раньше он бы свернулся…
Поздравляю вас, граданин!
Вы придумали «чистое программирование». Теперь осталось декларировать иммутабельность и реализрватть саму бариикаду на монадах.

Ждем новых, потрясаающих идей -)
Я себе чужих достижений не присваивал и честно признался, что вычитал идею в книгах других авторов. Просто там, чтобы её поймать нужно прочитать 2 книги по 800 страниц, а тут — 3 абзаца по 100 слов. Кому-то может сэкономить время.
Достаточно прочитать статью в википедии о Функциональном Программировании: там даже больше, шире, лучше и разнообразнее.

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

Но вы в любом случае делаете хорошее дело, просвещая общественность -)
я согласен про википедию, но не согласен, что «кому надо — знают». На Хабре много студентов, которые еще учатся. Если не сама статья, то мои ссылки на Макконнелла и Ваши на википедию могут быть полезными.
Есть другой подход, который мне видится более удобный и перспективным.
Можно назвать его «Let It Crash».

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

Собственно подход Erlang, название оттуда же.
НЛО прилетело и опубликовало эту надпись здесь
LetItCrash не спасет от SQL инъекции или XSS уязвимости например…
Данная проблема, как мне кажется, решается не очень сложно.
Пишем код с использованием контрактов вроде Code Contracts. Компилятор проверяет, что внутри библиотеки/баррикады контракты не нарушены. После этого, можно [будет] сказать компилятору убрать код проверок, для которых доказано, что они всегда верны. Таким образом, реальные проверки останутся только по периметру баррикады, а корректность баррикады гарантируется компилятором, анализирующим контракты.

В Code Contracts можно задать (Requires) условия для аргументов метода. Также можно пообещать (Ensures) что для при выходе из метода будут выполнены определённые условия для результата и конечных значений аргументов (да аргументы могут меняться внутри процедуры). Также можно пообещать, что аргументы не изменятся если в методе возникнет исключение. Инварианты пишутся в отдельном методе.
При компиляции с соответствующими опциями, все утверждения проверяются и выводятся ворнинги, причём довольно осмысленные. Например, увидев код вроде int a = 1 << (arg1.Prop — 8), компилятор предложил добавить проверки на то, чтоm 7 < arg1.Prop < 39.
а как доказать, что контракты всегда соблюдаются и соответствующие проверки можно убрать? просто проанализировать граф вызовов и сопоставить requires и ensures — мало…
Компилятор анализирует сам код. Если ты написал Ensures, то он проверит, что ты в самом коде метода не нарушил это условие. Многие стандартные функции помечены как [Pure] — это означает, что они никак не меняют состояние объекта. Ещё есть случай совсем внешних библиотек. У них нет контрактов и система, отдавая им переменную, или принимая от них значение, не знает, что произойдёт. Для таких случаев есть Assume. Assume похож на Requires, но если Requires находится в начале метода и проверяет добросовестность вызывающего метода, то Assume просто делает недоказуемое утверждение (сопровождающееся проверкой в Debug версии). Вообще, задача, конечно, непростая. Как я понимаю, Code Contracts дествуют по принципу «виновен, пока не доказано обратное».

У меня был совершенно глупый случай, когда я не мог понять почему контракт считается недоказаным. У меня в программе был набор списков. Был метод, позволяющий добавить новый список в набор. При этом было условие, что длина нового списка должна быть такая же, как и длины уже существующих (проверялось при помощи Requires). Но инвариант, который постулировал, что длины всех списков равны, оказывался недоказуем. Причина банальна: тот, кто передал мне список для добавления, может всего добавить в него элемент, изменив длину и нарушив мой инвариант.
Если будет интересно, то основной сайт сейчас — research.microsoft.com/en-us/projects/contracts/
Вот тут (http://rise4fun.com/CodeContracts и pexforfun.com/) можно поиграться прямо в браузере — пишется код функции, а Code Contracts и PEX проверяют валидность (первая ссылка) и находят аргументы при которых возникают проблемы (вторая ссылка).
Они и есть темто, ради чего вся эта затея начиналась.

Все это красивые слова и прекрасно в теории, но сделать хорошую работающую программу по такому принципы задача очень сложная. К тому же за счет баррикады можно нехило потерять в производительности.
так утверждается же что наоборот, «баррикада» как раз уменьшает количество мест, где понатыканы тормозящие проверки?
найти бы теперь руководство, в котором учат где конкретно в коде лучше выстраивать эту «баррикаду» :)
В случае Java многие уже используют аннотации типа @NotNull и @Nullable, а IDE проверяет, чтобы это было корректно. В итоге — делаем в одном месте проверку, возвращаем @NotNull — и в остальных местах уже можно не проверять.
Я думаю, через пару-тройку лет подобная фича будет реализована на уровне синтаксиса, причем не только для not-null, а также для range-check и тому подобных условий.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации