Существует много разных взглядов на разработку архитектуры и дизайна современных приложений. Некоторые архитекторы стремятся продумать все до мелочей, разрисовать use case-ы всех классов и модулей, проанализировать миллион возможных способов их использования, все их обязательно задокументировать и уже потом приступить к этапу кодирования.
Другие, наоборот, считают, что «думать уже поздно» и давным-давно пора «делать», поэтому они кидаются на баррикады с криками «Ура», выдавая на гора тонны никому не нужного кода. Как и любая крайность, такой подход не приводит ни к чему хорошему. Но, как и во многих других случаях, существует промежуточный вариант, когда проектированию и архитектуре уделяется должное внимание, когда они не ставятся во главу угла, а используются для выявления правильных абстракций и поиска компромиссов в противоречивых требованиях заказчика.
Когда речь заходит о дизайне классов, архитектуре модулей или об ответственности различных слоев приложения, то решающую роль при их проектировании играют понятия сцепления (cohesion) и связанности (coupling). Еще Кристофер Александер, «папа» шаблонов проектирования, писал о том, что основной задачей при декомпозиции системы является осуществление двух условий: (1) максимизация связей внутри компонентов (высокое внутреннее сцепление, tight internal cohesion) и (2) минимизация связей между компонентами (низкая внешняя связанность, loose external coupling).
ПРИМЕЧАНИЕ
Подробнее об истории возникновения такого уникального понятия, как шаблоны проектирования (design patterns) можно прочитать в статье: Шаблоны проектирования. История успеха.
Благодаря Бобу Мартину и некоторым другим известным личностям, в нашем арсенале появились принципы, такие как S.O.L.I.D., которые позволяют определить более точно (или, быть может, более формально), отвечает ли дизайн системы приведенным выше основополагающим принципам или нет. Я же обычно, прежде чем переходить к «тяжелой артиллерии», в виде подобных принципов использую более простой подход. Я задаю себе следующий вопрос: «а насколько реально покрыть основную функциональность этого класса модульными тестами?» Если ответ положительный, то наверняка указанный класс обладает достаточно высоким сцеплением и низкой связанностью с другими классами. Если даже чисто теоретическое написание юнит-теста невозможно, поскольку ответственность класса непонятна, он содержит кучу несвязанных друг с другом полей и методов, и зависит от двух десятков других сущностей, тогда с дизайном явно что-то не так.
Рисунок 1 – И как это чудо тестировать? А ведь в реальной жизни связей может быть в пару раз больше
СОВЕТ
Используйте принцип «тестируемости» класса в качестве «лакмусовой бумажки» хорошего дизайна класса. Даже если вы не напишите ни строчки тестового кода (хотя зря!), ответ на этот вопрос в 90% случаев поможет понять, насколько все «хорошо» или «плохо» с его дизайном.
Когда речь заходит об архитектуре приложения или модуля, то «тестируемость» не может быть критерием качества. Конечно же, мы можем создать интеграционные тесты целых подсистем, но они, скорее всего, дадут совсем немного полезной информации об адекватности их архитектуры.
При решении архитектурных вопросов, таких как выбор платформы построения распределенных приложений, выбор UI-фреймворка или архитектуры слоя доступа данных, разумно задавать себе другой вопрос: «А что будет, если принятое сейчас решение будет ошибочным?» или «А вообще, должен ли я принять окончательное решение прямо сейчас?».
Абстракция и инкапсуляция, все еще являются нашими лучшими друзьями и вот как раз они прекрасно применимы, как к отдельным классам, так и целым модулям или слоям приложения. Использования WCF должно быть максимально спрятано в коммуникационном слое, UI фреймворк не должен торчать из всех базовых классов, а архитектура слоя доступа к данным не должна налагать ограничения на бизнес-классы приложения. Конечно, согласно “закону дырявых абстракций”, периодически ненужные подробности все же будут пробираться в другие слои приложения, но нам нужно хотя бы постараться их ограничить.
Хорошая архитектура не должна быть продумана до мелочей; хорошая архитектура должна быть продумана достаточно хорошо, чтобы понять, насколько сильно архитектурные ошибки в одной части приложения поломают совсем другие логически не связанные модули.
СОВЕТ
Если «лакмусовой бумажкой» качества дизайна классов была их тестируемость, то лакмусовой бумажкой качества архитектуры можно считать ее «гибкость». Спросите у себя: «А что будет, если текущее архитектурное решение окажется неверным?», «Какое количество модулей подвергнется при этом изменениям?». По возможности, архитектурные решения не должны «вырубаться в камне», и последствия архитектурных ошибок должны быть в разумной степени ограничены.
Отступление от темы. Clean Architecture от Боба Мартина
О важности прагматичного взгляда на архитектуру написано довольно много. Гради Буч в своей знаменитой книге неоднократно останавливается на важности абстрагирования и инкапсуляции на самых разных уровнях. В замечательной книге «97 этюдов для архитекторов программных систем» многие авторы не раз говорят о вреде «вырубания решений в камне» и преимуществах простоты перед гибкостью в вопросах архитектуры.
Одним из последних известных авторов, тему архитектуры поднял Боб Мартин. В одной из своих презентаций, а также в одном из своих постов об архитектуре Боб написал следующее: … Хорошая архитектура позволяет ОТКЛАДЫВАТЬ принятие ключевых решений… Хорошая архитектура максимизирует количество НЕ ПРИНЯТЫХ решений.
Я, конечно, не на все 100% согласен со стариной Бобом о том, что можно отложить все архитектурные решения, но согласен с тем, что хорошая архитектура позволяет отложить многие из них. И здесь даже дело не только и не столько во времени принятия этих решений, а в стоимости их последующих исправлений.
ПРИМЕЧАНИЕ
Презентацию Боба Мартина, о которой я упоминал выше, можно найти здесь, небольшое видео, с обсуждением этого вопроса, здесь. И вот несколько постов Боба на эту же тему: Clean Architecture и Screaming Architecture.
Заключение
Я очень советую читателям не воспринимать приведенные выше принципы как «серебряную пулю», которая гарантированно даст вам ответ на вопрос о том, как получить «идеальный дизайн» или «идеальную архитектуру». Подобные вопросы могут лишь показать пробелы, но не могут показать правильный путь, они способны лишь зажечь тревожный огонек в вашей голове и побудить вас к более тщательному анализу дизайна класса или принимаемых архитектурных решений.
И опять юнит тесты... :) Понятно, что если компоненты легко заменять, то их и легко подменять для тестов. Но если ты делаешь wrapper что бы внести какую-то стороннюю библиотеку себе в приложение, то этот враппер - обычно сложно тестировать так как хорошая прослойка не должна вносить особой потери быстродействия (быть достаточно тонкой), а значит очень хорошо прилегать к целевой библиотеке. А подобная "звёздочка", приведенная на картинке - характерна всяким объектам объединяющим функциональность, например - делаешь свою однородную сеть для передачи чего-то поверх кучи разных протоколов, каждый из которых выполняет свою особую роль (один предоставляет средства передачи коротких сообщений, другой - уведомления, третий- временное хранилище) и в реализации функций используются сразу несколько (огромную картинку нужно закачать и послать уведомление).
ОтветитьУдалитьЯ бы хотел немного возразить на предыдущий комментарий. Целью юнит-теста является, в первую очередь, показать, работает ли условие, являющееся индикатором работоспособности тестируемого кода. Если да, то ни о каком быстродействии речь не идет. Тест - это не боевой код, это дополнение к нему. И тестирование враппера, как Вы пишете, входит в его обязанности лишь в том свете, чтобы показать - враппер в тесте сработал в нужном контексте, а значит, он сработает в данном контексте и в боевом коде. И юнит-тест - это один из индикаторов того, что мы имеем адекватную архитектуру, которую можно протестировать по частям. А значит, она достаточно гибкая.
ОтветитьУдалитьСергей, спасибо за хорошую статью!
ОтветитьУдалитьТестирование враперов - легко :)
Что мешает сделать мок и подсовывать его? Сложнее тестировать многопоточность и асинхронность, но тоже решаемо.
Ony, вообще в дизайне лучше избегать таких вот звездочек. Если же есть такие классы, то они не должны содержать сложную или запутанную функциональность. Я бы сказал так: что если получается вот такая звездочка, то скорее всего плохой дизайн.
Чуть поправлю nwm, целью юнит теста является проверка соблюдение контракта класса. Это предусловие, постусловие и инвариант.
На счет архитектуры. Есть отличный принцип здравого смысле, всегда когда что-то делаешь, надо думать, кто этим будет пользоваться и как часто. И еще считаю что вся документация должна быть минимально достаточной. Архитектурную документацию возможно и стоит держать, чтобы знать где какой модуль по какому протоколу и с кем общается. Документацию для дизайна лучше генерить из кода, либо рисовать непосредственно для последующего обсуждения. Хранить кипу диаграмм классов бессмысленно, т.к. ее надо будет либо поддерживать, либо она станет не актуальной.
@ony: с тобой мы уже это дело обсудили;) так что продолжать флеймить не будем:)
ОтветитьУдалить@Slava: +100 500!
Хорошая статья, согласен с обоими критериями, как наиболее быстрым способо получения "оценки качества" кода/архитектуры.
ОтветитьУдалить@Александр: спасибо, я рад, что не одинок в своих выводах:)
ОтветитьУдалитьСпасибо за статью.
ОтветитьУдалить