Wednesday, February 28, 2007

Объектно-ориентированному дизайну пора потесниться

Явно пришло время изменений. Предыдущий пост был про идею новой методологи разработки, а сегодня пришло время пересмотреть основы программирования и дизайна – OOP и OOD.

Большинство изменений происходят постепенно. За текучкой мы эти изменения не замечаем.

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

OOD сейчас не то, чтобы устарел (к процедурному стилю никто переходить не собирается), но многими принципами приходится поступаться ради удобства тестирования.

Основная проблема – область видимости. Закон инкапсуляции гласит, что все внутреннее устройство объекта никого не интересует и должно быть запрятано внутрь. Однако им приходится часто поступаться - ради возможности протестировать работу приватного метода, его приходится делать публичным. Т.е. теперь все внутренне устройство становится интересно тесту.

Самое неприятное - когда красиво спроектированную архитектуру приходится портить ради тестов. Какой выход из этой ситуации?

Для того, чтобы не ломать архитектуру UnitTest-ы класса можно размещеть в том же юните, что и сам класс. Но такое решение имеет свои недостатки – резкое увеличение объема кода одного юнита, сложность распараллеливания работ над реализацией класса и тестами к нему. Но основная проблема в том, что такое решение подходит только для UnitTest-ов. Для функциональны тестов, использующих объекты, расположенные в разных юнитах нужны дополнительные хитрости – например использование тестовых классов, отнаследованных от тестируемых классов, которые дают публичный доступ ко внутренним процедурам тестируемых классов. Но и тут могут возникнуть проблемы, особенно при сложных структурах наследования. Возможность использовать подобные ухищрения зависит от языка и от утилит тестирования.

Roy Osherove предлагает кардинально другое решение. Он предлагает поменять подход к дизайну и делать его не только объектно-ориентированным, но и тесто-ориентированным. Итак, на замену OOD идет OOTD (Object Oriented Testable Design).

На мой взгляд идея хорошая. Признание факта противоречия классических канонов OOD и тестирования – это хорошая причина уделить внимание проблемам тестирования еще на этапе проектирования.

11 comments:

Sergey Rozovik said...

Я вот сейчас крамолу скажу, за которую меня многие будут готовы порвать:) Не надо зацикливаться на unit тестах. Это хорошая практика, она сильно помогает в ведении других практик (напр., рефакторинга), но это не серебрянная пуля. Вот создали методологию Test Driven Development, но практика ее применения показывает по большей части скверные результаты. Методика сложна и неустойчива. Очень легко свалить проект разработки в разработку тестов, забыв о функционале, либо наоборот, выпасть из методики, дописывая тесты находу перед сдачей проекта.
Теперь вот - Test Oriented Design. Ну, действительно есть свои нюансы, которые накладывает модульное тестирование на дизайн классов. Иногда приходится вводить дополнительные классы или уровни. Иногда это идет на пользу общему дизайну. Иногда - нет. Но это все - рабочие моменты. Весь этот экстрим с тестированием приватных методов - признак плохого дизайна и ничего более. Послулат инкапсуляции никто не отменял. А в соответствии с ним, весь "внутренний" функционал класса легко тестируется через его внешний интерфейс, т.е. через его публичные члены.
Если при написании тестов возникают трудности - это хороший повод подумать над улучшением дизайна. Вот и весь OOTD.

Nikolai Voynov said...

Абсолютно согласен с мыслью Сергея: "Если при написании тестов возникают трудности - это хороший повод подумать над улучшением дизайна."
TDD ставит акцент именно на этом, что если будете начинать с тестов у вас получится хорошая архитектура - тесты подскажут правильные варианты работы с классом. Т.е. все эта трудность написания тестов простая проблема дизайна.

Alex Ott said...

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

Elena Makurochkina said...

2 Sergey Rozovik:

Зацикливаться, в общем то, ни на чем не надо. Серебряной пули не существует, а наилучшая пробивная сила получается при применении разных практик в правильной пропорции.

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

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

С Test Driven Development та же ситуация – возможность и целесообразность применения зависит от продукта. Хорошо подходит для конечнопользовательских продуктов. Для корпоративных систем – очень ограничено и, скорее, не при создании системы, а при внесении функциональных изменений.

На уровне классов, в большинстве случаев, есть более чем один вариант реализации взаимодействия классов. OOTD – это повод при выборе конкретного решения лишний раз подумать а удобно ли его будет тестировать. Речь же не идет об отказа от принципов объектно-ориентированности. Просто в список требований, которым должен удовлетворять дизайн добавляется еще один пунктик – удобство тестирования.

Elena Makurochkina said...

2 Alex Ott:

«Я участвовал в проектировании системы, где использовался специализированный язык, и требования к разработке предполагали 100 процентное покрытие кода тестами. Но все тесты проводились через публичные интерфейсы.»

В системе все интерфейсы были публичные или приватный функционал тестировался только через публичные функции (т.е. использовалось функциональное тестирование)?

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

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

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

«изменение архитектуры системы ради удобства разработки, может дорого обойтись»

Идея OOTD как раз и состоит в том, чтобы не менять архитектуру ради тестирования (это как раз сейчас часто происходит), а сразу разрабатывать ее с учетом требований к последующему тестированию.

halabala said...

Глупая идея. Что вы предлагаете, отменить инкапсуляцию? Вспомните? для чего она нужна вообще.

Как вы предлагаете тестировать private методы если к ним как правило нет спецификации, т.е. соответственно критерии "черного ящика" отпадают сразу. Да и вообще порой только программист понимает private метод и для чего он нужен.

Нужно тестировать контракт класса/пакета.

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

Alex Ott said...

2Elena:
касательно проектов - все тестировалось через публичные функции.
Все-таки разработка архитектуры с учетом тестирования - странная вещь, у меня юнит-тесты могут сильно поехать из-за изменения внутренностей классов, не затронувших публичные интерфейсы, получается лишний геморой.
Для критичных проектов как раз и должно использоваться функциональное тестирование, только подбирая наборы тестов таким образом, чтобы протестировать все внутренние функции. В упомянутом проекте, существовали утилиты, которые вычисляли по какой строке кода тесты прошли, а по какой - нет, и соответственно выдавали репорты
P.S. я чем дальше, тем больше убеждаюсь, что никакие методологии не заменят здравого смысла, а все остальное - лишь мода и методы для одной из возможных областей применения. Просто народ обажает кричать о серебрянной пуле и одном возможном пути

Elena Makurochkina said...

Объектно-ориентированность со всеми присущими ей принципами никто отменять не призывает, в том числе инкапсуляцию. Как можно тестировать приватный функционал в посте описано.

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

При применении любой практики главное - включать голову :-)

Elena Makurochkina said...

2 Alex Ott:

"Для критичных проектов как раз и должно использоваться функциональное тестирование, только подбирая наборы тестов таким образом, чтобы протестировать все внутренние функции."

Использование юнит тестов – не отменяет тестов функциональных. И наоборот. Когда проектом занимается более одного человека тестирование происходит на разных уровнях. При передаче юнита от одного разработчика другому лучшая защита «от дурака» - юнит тесты написанные первым разработчиком. Сложную (логическую или вычислительную) ф-ю дешевле проверить юнит тестом. Ф-ю, критичную к качеству входящих параметров тоже дешевле проверить в юнит тесте. А поверх всего этого еще и функционатьный тест.

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

"чем дальше, тем больше убеждаюсь, что никакие методологии не заменят здравого смысла"

Здравый смысл – это главное. Изучение же различных методологий наталкивает на интересные решения и помогает смыслу быть более здравым. Методологии – это некий наработанный опыт и его изучение помогает не наступать на грабли, по которым уже кто-то потоптался и не изобретать всем известных велосипедов.

"Просто народ обожает кричать о серебрянной пуле и одном возможном пути"

Да, народ обожает кричать о том что именно вот эта вот последняя методология (или технология) самая универсальная (я, кстати, уже писала и по перерекламленность Agile и про то что SOA, объявленная недавно самой-самой универсальной имеет свои ограничения). И такой крик иногда довольно сильно раздражает. Но это не значит что умному человеку услышав крик надо заткнуть уши. Можно послушать и подумать можно ли это применять, и в каких ситуациях.

PS: А в посте, кстати, крика нет. Оно как и разговора о серебрянной пуле.

halabala said...

"Если есть вопросы зачем тестировать приватные функции (то-есть зачем вообще нужны юнит тесты) – то по этому поводу написано очень много (начать можно с википедии). "

Т.е. вы хотите сказать, что юнит тесты как раз и нужны для тестирования private методов,
тогда, пожалуй, кто-то из нас не понимает что такое unit тесты. Большинство tool's для unit тестов вообще не поваляют тестировать private методы (есть не нормальные исключения). Рекомендуя вам для полноты картины попробовать написать unit тест, тестирующий private метод.

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

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

ps:
А если требуется покрытие всех путей (если вспомнить теорию: к примеру требуется критерий потока
управления, покрытие операторов - C0, к слову сказать саамы слабый критерий). То уж будьте добры построить такой набор тестов, чтоб он покрыл все private методы, или доказать что данный метод вообще не используется(ключевое слово доказать).

Elena Makurochkina said...

2 halabala:

"Т.е. вы хотите сказать, что юнит тесты как раз и нужны для тестирования private методов"

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

"Вообще не понятно как тестировать private метод, т.к. как он должен работать знает только его автор, и использует он его только в тех случаях, когда он работает."

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

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

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

"Но все же, рекомендую доверять не wiki а собственному опыту."

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