воскресенье, 18 ноября 2007 г.

Проектирование по контракту (часть 2ая)

В предыдущем посте я начал рассказывать про проектирование по контракту (DBC). А закончил я рассказ на том, что сделал некий небольшой обзор уже существующих попыток внедрить проектирование по контракту в .NET. Теперь же наступило время продолжить ;)
Стоит сказать, что изначально, идею проектирования по контракту я хотел использовать с целью тестирования кода. Идея была приблизительно следующая: описываются предусловия, постусловия и инварианты с помощью атрибутов, затем запускается некая среда, которая подгружает созданную сборку, на основе атрибутов генерирует некий набор случайных тестов, и запускает их. Тут может возникнуть вопрос - какой именно набор тестов, что значит случайный и что эти тесты проверяют? Да и, вообще, зачем все это нужно? Отвечу на вопросы по порядку.
Начну, наверное издалека, а именно с вопроса - чем меня не удовлетворяют стандартные автоматизированные тесты на основе NUnit? А не удовлетворяют они меня тем, что все тесты приходится придумывать самому. Согласен, что в ряде случаев это правильно, но не всегда. Например, есть у нас метод принимающий целочисленный параметр. Для этого метода известно, что в зависимости от того, в какой диапазон попадет этот параметр, метод может повести себя по разному. Предположим что таких диапазонов 3. В таком случае мы пишем 3 теста, в каждом из которых проверяем метод при подаче в него параметра из соответствующего диапазона (1го, 2го и 3го), ну или пишем один большой тест (что, лично я, делать не советую ;). Вроде все правильно, только вот из каждого диапазона мы проверим только несколько значений. А вдруг, на самом деле существует некий 4ый диапазон для чисел из которого наш метод будет выполняться неправильно? В таком случае наши тесты не найдут этот самый диапазон. И это при том, что проверяемый нами метод полностью покрыт тестами. Естественно, что сейчас найдутся люди, которые скажут, что оттестировать все варианты невозможно, но тем не менее...
Лично мне в данной ситуации более логичным кажется в каждом их трех тестов брать не конкретный параметр из заданного диапазона, а случайно сгенерированный. Естественно, что тогда нельзя будет точно определить выходной параметр метода, но, скорее всего некие ограничения на него наложить будет можно (сумма двух положительных - положительна). При таком методе тестирования, теоретически, может сгенерироваться параметр из нашего 4го диапазона и в таком случае тест может и не пройти (а может и пройти, выходные параметры мы же проверяем не на точное соответствие). Конечно для верности в тесте надо будет сгенерировать несколько входных параметров. В итоге мы получаем некое стохастическое тестирование. А вдруг да и грохнется наш тест на каких то непредвиденными нами входными данными? Причем, в первую очередь, имеется виду не то, что сам метод не выполнится (это уже фуззи тестирование), а то, что выходные параметры не пройдут проверку.
Конечно нельзя сказать, что данный метод тестирования лучше традиционного. Лучше сказать так - они бы прекрасно дополняют друг друга.
Ну вот мое "издалека" и кончилось. Я думаю, что смышленные уже догадались, что три описанных выше теста с легкостью могут быть превращены в три различных контракта для метода. Осталось только автоматизировать процесс автоматической генерации данных, ну а еще придумать удобный способ описания этих самых контрактов (а иначе толку от идеи мало, т.к. легче будет писать такие тесты ручками). В итоге получаем ответы на наши вопросы. Генерируется набор тестов, который проверяет определенный контракт метода генерируя случайным образом входные данные для этого метода.
Теперь, наверное, стоит ответить на вопрос, какое отношение все это имеет к первому моему посту, где я рассказывал про Spec# и eXtensible C#? А отношение следующее - лично по моему сугубо личному мнению, велосипеды получились неудобные, т.к. использоваться проектирование по контракту должно не как велосипед. Поясню. Теория проектирования по контракту вообще говоря, не подразумевает использования пред пост условий и инвариантов для более простой записи проверяющих условий в коде. В первую очередь она служит для того, что бы описать некие логические ограничения классов во время проектирования (именно проектирования, а не кодирования), которые нельзя описать с помощью обычных конструкций языка. Ну, например, нельзя компилятору объяснить, что количество элементов в коллекции должно быть больше нуля, т.к. с точки зрения компилятора int может применять и отрицательное значение. Нет, конечно можно сделать свойство в котором метод set будет все это проверять, но проблема не в этом. Проблема в том, что само по себе условие о том, что свойство Count должно быть больше нуля никак не отражается в описании класса (в описании интерфейса этого класса, так будет лучше) - программист сам понимает, что он должен сделать такое ограничение, ну или проектировщик интерфейсов описывает это дополнительно где нибудь (например в тех. задании на реализацию этого интерфейса, или в remarks к свойству). Существующие же решения просто предлагают краткую форму написания проверок.
Еще пару слов об автоматизированном тестировании, а точнее о TDD. Это очень хорошая методология, правда ;) На самом деле TDD позволяет как раз частично описать те самые ограничения (и не только их). Соотвественно, потом появляется возможность спокойно реализовать код и проверить его с помощью тестов на соответствие этих самых ограничений (а не написать код, а потом писать тесты, которые "подтверждают" что это самый код работает). На самом деле всвязи с этим у TDD прослеживается хорошая связь с DBC. Только вот DBC содержит более "абстрактные" проверки (TDD проверяет конкретный пример - частный случай, а DBC говорит, как должно быть в общем). Но не смотря на связь нельзя сказать, что TDD это частный случай DBC или наоборот... просто здесь есть некие пересечения, есть что то общее, но только в некоторых частях обоих методологий.
Немного отступлю от темы, что бы еще раз разрекламировать TDD. Недавно у меня была следующая проблема: я уезжал из города на неделю, но необходимо было придумать задание, которое бы выполнили в мое отсутствие. Задание заключалось в том, что бы создать DataSet определенной структуры. Можно конечно было просто сказать - сделайте DataSet соответствующий структуре такой то БД, но боюсь, что "реализаторы" не могли в полной мере понять, как же надо соответствовать структуре БД, да и проверять потом сделанную работу просмотром кода не очень то удобно. Соответственно были сделаны тесты, которые просто проверяли, что DataSet содержит внутри себя такое то кол-во таблиц, что таблицы имеют такие то названия, что в таблицах есть такие то столбцы такого то типа, что заданы определенные ключи и определенные связи между таблицами. Другими словами я описал то, что хотел увидеть в результате, но в форме тестов. В итоге люди, особо не понимающие, что, например, столбец должен иметь такой тип данных и именно такое название, зачем нужны связи между таблицами и т.д. смогли реализовать код (хотя конечно не за неделю, но тут уже проблема в другом). Задание же для них было приблизительно следующее: все кружочки должны быть зеленого цвета. Самое главное, что и я был спокоен относительно того, что код был реализован правильно.
Но вернемся к нашей теме. А начну я с того, что брошу еще один камень в парк велосипедов. Камень этот имеет название "постусловия". Что такое постусловия? Это утверждения, которые говорят о корректности выполненого метода. Что же происходит с ними, например, в eXtensible C#? Постусловия превращаются в подобие Assert, которые вызываются перед return метода... Т.е. получается, каждый раз, когда вызывается метод, он проверяет себя - а правильно ли я вообще выполнился? С одной стороны это, может быть и правильно - метод поймет, что сделал какую то глупость во время выполнения приложения... т.е. приложение более устойчиво к своим же ошибкам? Ну это зависит от того, что в такой ситуации сделает метод. Наверное логичнее всего отправить отчет microsoft - пусть разбираются ;) А если честно, то получается, что тесты перетекли в уже готовое приложение и выполняются каждый раз во время выполнения метода (что естественно сказывается на производительности, причем, по моему, это нельзя оправдать большей надежностью).
С предусловиями все таки все не так плохо. Часть из них действительно необходимо включать в результирующий код, например предусловие о том, что какой то параметр метода не должен быть равен null. С другой стороны возникает вопрос, что делать если предусловие не выполняется? Генерировать исключение, просто выходить из метода?
Ну или еще милый пример (это если я, конечно, все таки понимаю проектирование по контракту)... Допустим у нас есть несколько контрактов на метод сложения двух чисел: "если оба числа положительные, то результат положителен", "если оба числа отрицательные, то результат отрицателен". Ведь у обоих контрактов есть предусловие и постусловие.... причем если для конкретного предусловия не выполняется его постусловие, то метод реализован неправильно, но вод вставлять эти пред и пост условия в конечный исполняемый код... извините, но по моему это неправильно. К тому же тут наблюдается следующая ситуация: несоответствие одному из предусловий еще не говорит о том, что входные параметры неверны, ведь они вполне подходят для другого предусловия. В данном примере входные параметры даже могут не удовлетворять обоим предусловиям. В общем случае тут получается ситуация, когда по одному из контрактов метод с данными входными параметрами выполняться не должен, а по другому должен. Что делать? Это зависит от самих контрактов. Например контракт должен быть обязателен к исполнению, или нет. Тут мне, наверное, стоит оговориться - на сколько я помню, в самой теории DBC нет вывода нескольких типов контрактов (а жаль). Кстати, что думаете об этом? Путаю ли я тестирование и проектирование по контракту? :)
Следующий пункт в моем не структурированном высказывании будет посвящен связи проектирования по контракту и аспектному программированию. Связь может быть натянутой, ну или немного искуственной, сейчас объясню почему. Для начала скажу, что связь проектирования по контракту с аспектным программированием появилось только из-за того, как это проектирование по контракту реализовываться. Ведь самым логичным подходом считается следующий: ловим момент вызова метода, проверяем все наши предусловия, если они прошли проверку, то вызываем метод, получаем выходные параметры, проверяем на их основе постусловия, если все хорошо, то возвращает результат дальше. Вот и получается, что у нас что то делается в начале вызова метода и в его конце. Лично мое мнение - это все неправильно. По крайней мере про постусловия я уже говорил выше, да и с предусловиями все не так ясно. Могу лишь согласится, что аспектное программирование логично применять при верификации кода в тестах по контрактам и инвариантам.
Кстати об инвариантах. Как не жаль, но инварианты это единственное, что на данный момент до сих пор осталось только в теории... По хорошему, инварианты нужно проверять и в предусловиях и в постусловиях, в реальности же, необходимость в этом наблюдается не всегда (в принципе отчасти это и логично). Из-за этого, скорее всего, было принято единственное решение - вообще их не использовать, а жаль... Лично я вижу следующее решение данной проблемы: для каждого контракта указывать, какие именно инварианты имеет смысл проверять в данном контракте. Так же, по хорошему, необходимо проверять часть инвариантов, когда происходит изменение свойств и полей. Хотя, учитывая, что свойство - это просто два метода (а на метод можно навесить контракт), то проблема остается актуальной только для обычных полей.
Подводя промежуточные итоги я могу лишь сказать, что или идеи DBC написаны не очень четко (т.е. иногда возникают спорные вопросы относительно того, что же это такое... ну вот как у меня с несколькими контрактами), или везде, где утверждается использование DBC - оно используется только отчасти (а точнее - используется название "проектирование по контракту" для каких либо целей, но никакое это не проектирование по контракту)...
P.S. Честно говоря, уже забыл с чего я начал этот пост, и что именно хотел сказать в его начале... Наверное это пока что только начало рассуждений о том, что же на самом деле такое "проектирование по контракту". И, надеюсь, что вы поможете ответить мне на этот вопрос ;)

4 комментария:

  1. По поводу TDD, ты на самом деле испытал не все способы. Я теперь понял, что тебе надо было - это называется DataProvider, не знаю есть ли такая возможность в нюните, но она позволяет очень коротко обозначать входные данные для тестов и результаты:
    http://www.phpunit.de/pocket_guide/3.2/en/writing-tests-for-phpunit.html
    Думаю там есть и генераторы (либо их можно написать :) ). В любом случае, просто подумай над этим :)

    Еще стоит посмотреть в сторону ООАП, что касается "проектирования через интерефейс", тогда станет понятно, что дизайн с конктрактами - это расширение этой концепции.

    По поводу TDD и DBC не согласен: это совершенно разные вещи и они вообще не пересекаются :) TDD - это способ создания диазна, а DBC - это способ уточнения интерейфеса.

    Кейс про TDD очень понравился.

    Тема производительности не раскрыта. В свое время много вопросов вызвала дискуссия: надо ли проверять массив на упорядоченность (предусловие) при бинарном поиске. Надо? :)

    Проблему с несколькими "не обязательными" контрактами я не понял, ведь их можно объединить с помощью OR или проблема в синтаксисе: и есть отделение предусловий от постусловий?

    За аспекты - зачот. :) Очень на самом деле глубокая связь. Только сегодня читал про TDD, BCD и аспекты.

    Если нужны четкие определения - читай про верификацию программ, там на основе матлогики все сделано :)

    ЗЫ Если есть возможность настроить блоггер, чтобы отвечать можно было на странице с постом - сделай плиз.

    ОтветитьУдалить
  2. Да и еще: http://en.wikipedia.org/wiki/Behavior_driven_development, если не читал еще.

    ОтветитьУдалить
  3. По поводу TDD и DBC: Наверное я просто не очень хорошо сделал, что назвал это TDD. Почитал http://www.AgileDev.ru и понял, что скорее всего всегда использовал test-first development, а не TDD (т.к. проекты по большей части были студенческие). В том числе это касается и того кейса ;) Т.е. я имел в виду написание тех тестов, с помощью которых как раз можно уточнить некоторые тонкости интерфейсов, а не той методологии TDD, которая позволяет отточить интерфейсы. Признаю - ошибся немножко ;) Зря заменил NUnit на TDD
    Ведь уточнить интерфейс на данном этапе кроме как тестами, по моему нельзя...
    По поводу производительности: ну наверное придется тогда написать про нее, раз эта тема интересует ;) Вобще, я хотел показать, что, к примеру eXtensible C# плохо поступает с постусловиями... Что это вообще только использование терминов проектирования по контракту для внедрения проверок. Ну скоро будет еще одно интересное замечание, может станет яснее.
    Проблема с несколькими необязательными контрактами: как раз я хочу рассмотреть пред и пост условия без отрыва ;)
    Четкие определения не в плане проверок, а в плане теории DBC. Например в теории не говорится, что надо делать, если контракт не проходит. Или это просто означает что код неверен?

    ОтветитьУдалить
  4. Тесты не уточняют интерфейс (хотя с точки зрения документации с этим можно согласиться), они его создают.

    Верификация программ - это и есть теория. Если тебе нужен эталон - тогда смотри Eiffel.

    ОтветитьУдалить