Идеальная архитектура

    Существует много разных взглядов на разработку архитектуры и дизайна современных приложений. Некоторые архитекторы стремятся продумать все до мелочей, разрисовать 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.

    Заключение



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

    Подробнее
    Реклама

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

      0
      Кхм. Вы предлагаете во втором совете писать абстракции от всех используемых фреймворков? Например (условно) возьмём Qt в качестве фреймворка. Так ведь чисто из-за того, что мы будем его использовать он быстро полезет изо всех щелей! И хрен мы его просто заменим. Либо придётся ваять абстракции чуть ли не на каждый чих.

      Или я что-то неверно понял?
        +7
        Идея в том, что хорошо иметь четкие уровни в архитектуре. И если вы используете что-то на UI, то это не значит, что доступ к базе данных или какая-то логика ядра системы должны зависить и вообще знать о существовании UI. Разрабатывая каждый уровень, все остальные компоненты системы, на других уровнях должны представляться абстракциями, почти черными ящиками с моделью «с этого уровня я буду дергать DAO за эти методы и получать вот такие данные».

        Конечно, это все в идеале. Иногда проще, и к этому идут многие фреймворки для rapid development, довольно сильно сделать coupling уровней, примерно как в одном месте мы и из базы почитали и там же и view сгенерировали. Но стоит понимать намечающюся задницу, когда требования поменяются с «зеленых крокодилов» на «синие холодильники»
        +2
        минимизация связей между компонентами (низкая связанность, low cohesion).

        Наверное, loose coupling, имелось ввиду.
          –1
          Да, конечно, очепятался.

          +4
          На мой взгляд:

          Наиболее плохо поддаются проектированию приложения, где главными компонентами выступают GUI и БД. Приложения баз данных трудно спроектировать так, чтобы их конечная архитектура была достаточно гибкой, — на это потребуется не одна мажорная версия кода. Тут проблема в изменчивости (БД), трудности тестирования (GUI) и местечковости (Business Logic + GUI + БД).

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

          Хорошо проектируются приложения, в которых много компонентов, но все они достаточно равноправны. Это, например, компьютерные игры, какие-нибудь редакторы. И хотя на архитектуру придется потратить достаточно много усилий, но так как один ни компонент не будет перетягивать одеяло на себя, риски перекосов ощутимо сглаживаются.
            +1
            Есть два подхода к проектированию ПО. Один из них называется «outside-in approach», описанный в частности в книге “Growing Object Oriented Software”. В нем предлагается начать разработку с UI и двигаться в сторону бизнес-логики. Есть и другой подход, который называется «inside-out», в котором проектирование начинается с бизнес-логики и уже от нее двигаться в сторону UI и базы данных.

            Возможно есть приложения, в которых второй подход не работает, что-то типа сверх тонких клиентов, но в большинстве случаев, подход inside-out будет прекрасно работать. Да, проектированием UI приложения двигает именно UI, но даже если брать разработку сложного UI-приложения, то подход inside-out все еще возможен: бизнес-аналитик, архитектор, заказчик определяет желаемое поведение UI-й части. Затем, с помощью MVXXX паттернов дербанится на части представление и логика представления; после чего разрабатывается именно логика представления на которую уже натягивается сама вьюха.

            В общем, большинство приложений нормально проектируются, хотя, конечно, некоторые типы приложений могут сопротивляться этому сильнее остальных
              –1
              Есть два подхода к проектированию ПО. Один из них называется «outside-in approach», описанный в частности в книге “Growing
              Object Oriented Software”. В нем предлагается начать разработку с UI и двигаться в сторону бизнес-логики. Есть и другой подход, который называется «inside-out», в котором проектирование начинается с бизнес-логики и уже от нее двигаться в сторону UI и базы данных.
              ___
              Т.е. все то у чего нету UI ПО не является чтоли?
                +4
                Смотря что под UI понимать. Иногда это набор опций командной строки, формат конфигурационного файла, или даже формат ввода/вывода. Не могу вспомнить какое-либо прикладное ПО вообще без интерфейса.
                  0
                  Рано или поздно UI на каком ни будь из концов всплывает, другое дело, что это может быть за пределами разрабатываемой системы, тогда речь идет о программных интерфейсах.

                  Это разновидность функционального подхода — определяем что на входе и на выходе как более или менее данность, выводим из этого то, что в центре.
                  0
                  Вариант «outside-in approach» хорошо описан в Getting Real от 37signals и он отлично работает для проектов с тонкой моделью. Но для продуктов со сложной бизнес-логикой его использовать затруднительно.
                  0
                  «где главными компонентами выступают GUI и БД» — это справедливо почти для любого веб-приложения. А они вполне хорошо проектируется и покрываются тестами (как модульными, так и функциональными), особенно если используют MVC.
                  0
                  В каком-то софтварном журнале читал такое определение архитектуры: это то, что мы хотим сделать правильно с самого начала.
                  Т.е. по идее это такие вещи, которые после начала кодирования изменить сложно.
                  Итого, чтобы получить близкую к идеальной систему, надо хорошо продумать базовые компоненты (используемые фреймворки, интернализацию, логирования, слои) и на базе этих компонентов с помощью TDD строить оставшееся. Таким образом сложно изменяемые части скорее всего будут сделаны правильно от начала, а остальное через TDD с большей вероятностью будет сделано также качественно.
                    +6
                    Полностью согласен с применением тестов в качестве «лакмусовой бумажки». Это прямой путь к защите слоев архитектуры от взаимопроникновения и к минимизации связности компонентов системы.
                    Лично мне досталась в наследство некая система с интегрированным скриптовым языком. Так вот, она была настолько монолитна, что бизнес-логика, которая по задумке должна была быть реализована скриптами, дублировалась. Фактически, было написано две параллельные системы, в скриптах и в бинарнике, именно из-за жесткой связки бизнес-логики, логики работы с БД и GUI. За три года поддержки я смог «расцепить» UI и бизнес лишь частично, и то благодаря тем самым бесконечным тестам.
                    В то же время новый функционал подмешивался в виде автономных (ну, почти) модулей, которые безо всяких изменений могли работать и как отдельное тестовое приложение, и как часть системы. При разработке тестируемость была основными критерием.
                    Итог: подход работает. В новой части изменения проходят легко и непринужденно, в старой — каждый раз кровавое месиво. Конечно, в моем случае имеет место постепенное перепроектирование системы, а не разработка с нуля, но не думаю, что это принципиально.
                    А по поводу разработки архитектуры имею удовольствие доложить: очень многое, чуть ли не всё, зависит от хорошо продуманных программных интерфейсов. Даже самую мерзкую пакость можно обернуть приятным API и жить спокойно, при условии автономности оной пакости, разумеется.
                      +1
                      В таких случаях рекомендуют потратить время и зафиксировать поведение хотя бы наиболее критичных участков системы функциональными тестами. Это позволит сделать месиво менее кровавым :)
                      +3
                      Ещё бы добавил, что «правильный подход к проектированию» даёт ещё один значимый бонус, о котором часто забывают: масштабируемость процесса разработки. Т.е. грубо говоря — возможность сократить срок разработки за счёт добавления к нему людей. Это происходит за счёт простой вещи: наше ПО это 100500 микро-ситуаций «клиент»->«сервис». И если «сервис» будет не конкретным классом а интерфейсом, то разделить работу между 2 сотрудниками: одному разрабатывать «клиент»и тестироваться с помощью мока сервиса, а другому — разрабатывать реализацию сервиса и тестировать его тестом на основ контракта. Ясно что скорость увеличится не в 2 раза из-за необходимости дополнительно создать 2 комплекта для тестирования, но если в команде и так пишутся автотесты — то не так важно. Так же очевидно, что эффективность такого подхода тем выше, чем ближе наш клиент->сервис к ситуации взаимодействия 2х крупных компонент.
                        0
                        Хороший подход, на мой взгляд, а двух компектов тестов для приложений с пользовательским интерфейсом все равно не избежать.
                        +2
                        Вставлю свои 5 копеек. Основанные на личных фантазиях в меру далекого от программирования человека.

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

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

                        Тоесть, если риск ошибки (человеческой, программной etc) в одном модуле равен 0.1% (мало, правда),
                        то в случае если у нас 100 таких модулей, то риск всей системы возрастет до 10%.

                        Чем сложней система, тем большая вероятность катастрофы.
                          +1
                          У МакКоннела вся книга Code Complete, которая, на мой взгляд, является наиболее практически ценной из всех книг по программированию, строится вокруг изоляции и снижения сложности, и это выносится как основной принцип и императив.
                            +1
                            Вот, умные идеи они витают в воздухе. И распространяются на все сферы жизни.

                            Я к этому пришел через абсолютно экономические науки. Даже напишу статью об этом, так как оно затрагивает очень много чего в мире IT. Такое проектирование систем это частное применение одной большей современной эконом. теории.

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

                            * философски *

                            Как-то объясняет порядок законов во вселенной. Так же как много физических явления завязаны на логарифмическую константу е, так же и другие вещи абсолютно казалось бы не связанные между собой, абсолютно связаны.
                              +1
                              Ну, я социолог по образованию, так что тоже не могу отделаться от профессионального взгляда на разработку, во всяком случае социология науки и деятельности объясняют очень многое из получающихся результатов.

                              Достаточно много авторов уже писали в этом ключе: Дейкстра, Брукс, Грэм и т.д., но, слава богу, еще много неохваченных тем и есть что сказать.
                                0
                                Интересно. Ищу только где на вас можно подписаться (:
                          +1
                          В свое время я писал достаточно обширную заметку по типам связанности, и всяких тонких нюансах, например в нетипизованных языках любой параметр при вызове метода стоит воспринимать как более широкий канал для образования связанности. Отдельная тема — мониторинг связанности через глобалы и god-классы. В таком случае все, например, может прекрасно тестироваться пока не был затронут god-класс или глобалсы, а после их изменения вылететь в трубу.

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

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

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

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое