Теперь самое время объединить изученные нами правила и принципы создания архитектур и применить их для реализации учебного примера. Пример будет коротким и простым, но его будет вполне достаточно, чтобы продемонстрировать процесс, которому следует хороший архитектор, и принимаемые им решения.
Продукт
Для этого примера я выбрал продукт, с которым довольно близко знаком: программное обеспечение для веб-сайта, продающего видеоматериалы. Да, он напоминает cleancoders.com — сайт, где я продаю свои обучающие видеофильмы.
Основная идея проста. У нас есть партия видеофильмов, которые требуется продать. Мы продаем их через Веб физическим лицам и компаниям. Физические лица могут заплатить одну, более низкую, цену за потоковое видео, или другую, более высокую, за загрузку копий и их постоянное использование. Бизнес-лицензии распространяются только на потоковую доставку и приобретаются партиями, что позволяет предоставлять скидки.
Физические лица обычно выступают в роли зрителей или покупателей. В компаниях, напротив, есть люди, которые покупают видеофильмы для просмотра другими.
Авторы видеофильмов должны предоставлять свои видеофайлы, сопровождая их описанием и вспомогательными файлами с контрольными вопросами, решениями, исходным кодом и другими материалами.
Администраторам необходимо добавлять новые видеоролики, добавлять видеоролики в серию и удалять их, а также устанавливать цены на различные лицензии.
Администраторы должны добавлять коллекции видеофильмов, удалять коллекции и устанавливать цены на различные лицензии.
Наш первый шаг в определении начальной архитектуры системы — выявление действующих лиц и вариантов использования.
Анализ вариантов использования
На рис. 33.1 изображена схема типичного анализа вариантов использования.
Рис. 33.1. Типичный анализ вариантов использования
На этой схеме изображены четыре действующих лица. В соответствии с принципом единственной ответственности, эти четыре действующих лица станут основными источниками изменений для системы. Добавление любых новых возможностей или изменение существующих будет выполняться ради обслуживания одного из действующих лиц. Поэтому мы должны разбить систему так, чтобы изменения, предназначенные для одного действующего лица, не затрагивали других.
На рис. 33.1 изображены не все варианты использования. Например, на схеме отсутствуют варианты, отвечающие за вход и выход. Это сделано специально, чтобы упростить задачу и ограничить ее размер. Если бы я включил все возможные варианты использования, эта глава могла бы превратиться в самостоятельную книгу.
Обратите внимание на варианты использования в центре на рис. 33.1, заключенные в пунктирные рамки. Это абстрактные[62] варианты использования. Абстрактным называется такой вариант использования, который определяет общую политику для других вариантов использования. Как видите, абстрактный вариант использования Просмотр каталога наследуют варианты Просматривает каталог как зритель и Просматривает каталог как покупатель.
С одной стороны, эту абстракцию можно было не создавать. Я мог бы убрать абстрактный вариант использования из схемы без ущерба для каких-либо особенностей продукта. С другой стороны, эти два варианта использования настолько похожи, что я счел разумным признать сходство и найти способ унифицировать его с самого начала.
Компонентная архитектура
Теперь, когда известны действующие лица и варианты использования, можно создать предварительную компонентную архитектуру (рис. 33.2).
Рис. 33.2. Предварительная компонентная архитектура
Двойными линиями, как обычно, изображены архитектурные границы. Как видите, это типичное деление на представления, презентаторы, интеракторы и контроллеры. Также отметьте, что каждую из этих категорий я разбил на подкатегории по соответствующим им действующим лицам.
Каждый компонент на рис. 33.2 потенциально представляет файл .jar или .dll и каждый будет содержать соответствующие представления, презентаторы, интеракторы и контроллеры.
Обратите внимание на особые компоненты Представление каталога и Презентатор каталога. Именно так предполагается реализовать абстрактный вариант использования Просмотр каталога. Я предполагаю, что такие представления и презентаторы будут иметь вид абстрактных классов, которые будут наследовать конкретные классы представлений и презентаторов в производных компонентах.
Стал бы я действительно разбивать систему на все эти компоненты и организовывать их в файлы .jar или .dll? И да и нет. Конечно, я мог бы помудрить над компиляцией и настроить окружение так, чтобы действительно можно было создавать компоненты, развертываемые независимо. Также я мог бы оставить за собой право объединить компоненты, чтобы при необходимости получить меньшее количество единиц развертывания. Например, компоненты, изображенные на рис. 33.2, легко можно объединить в пять файлов .jar — по одному для представлений, презентаторов, интеракторов, контроллеров и утилит соответственно. В этом случае я мог развертывать компоненты, которые, скорее всего, будут меняться независимо друг от друга.
Еще один возможный вариант группировки: объединить представления и презентаторы в один файл .jar, а интеракторы, контроллеры и утилиты поместить в свои, отдельные файлы. Другой, еще более простой вариант группировки: создать два файла .jar — один с представлениями и презентаторами и другой со всем остальным.
Оставив эти возможности открытыми, позже мы сможем изменить способ развертывания системы, исходя из особенностей ее развития.
Управление зависимостями
Поток управления на рис. 33.2 движется справа налево. Входные данные поступают в контроллеры и затем обрабатываются интеракторами. Презентаторы форматируют результаты обработки и передают их представлениям для отображения.
Обратите внимание, что не все стрелки направлены справа налево. Фактически большинство из них направлено слева направо. Это объясняется тем, что архитектура следует правилу зависимости. Все зависимости пересекают архитектурные границы в одном направлении и всегда направлены в сторону компонентов, содержащих политики более высокого уровня.
Также отметьте, что отношения использования (открытые стрелки) совпадают с направлением потока управления, а отношения наследования (закрытые стрелки) направлены против потока управления. Это отражает использование принципа открытости/закрытости, который требует, чтобы зависимости были направлены в правильном направлении и изменения в низкоуровневых деталях не затрагивали высокоуровневые политики.
Заключение
Диаграмма архитектуры на рис. 33.2 включает разделение по двум измерениям. Первое — разделение на основе действующих лиц согласно принципу единственной ответственности; второе соответствует правилу зависимости. Цель обоих — разделить компоненты, изменяющиеся по разным причинам и с разной скоростью. Причины в данном случае соответствуют действующим лицам, а скорости — разным уровням политик.
После структуризации кода таким способом появляется возможность смешивать и распределять его по единицам развертывания как угодно. Вы сможете группировать компоненты в любые единицы развертывания, имеющие смысл, и менять правила группировки с изменением условий.
Глава 34. Недостающая глава
Все советы, которые вы прочитали к настоящему моменту, безусловно, помогут вам проектировать замечательные приложения, состоящие из классов и компонентов с четко определенными границами, понятными обязанностями и управляемыми зависимостями. Но как всегда, дьявол кроется в деталях реализации, и действительно, очень легко споткнуться о последнее препятствие, если не уделить ему должного внимания.
Представим, что мы строим книжный онлайн-магазин и один из вариантов использования, который нам предлагается внедрить, — возможность просмотра клиентами состояния своих заказов. Пример, следующий ниже, описывается с позиции языка Java, однако принципы в равной степени применимы к другим языкам программирования. Отложим пока чистую архитектуру в сторону и рассмотрим несколько подходов к проектированию и организации кода.
Упаковка по уровням
Первый и самый простой, пожалуй, подход — организация традиционной многоуровневой архитектуры, в которой код разделяется по функциональному признаку. Этот подход часто называют «упаковкой по уровням». На рис. 34.1 показано, как могла бы выглядеть соответствующая UML-диаграмма классов.
Рис. 34.1. Упаковка по уровням
В такой типичной многоуровневой архитектуре один уровень выделяется для веб-кода, один уровень — для «бизнес-логики» и один уровень — для работы с хранилищем данных. Иными словами, горизонтальные уровни используются как способ группировки по подобию. В «строгой многоуровневой архитектуре» уровни должны зависеть только от следующего смежного уровня. В Java уровни обычно реализуются в виде пакетов. Как показано на рис. 34.1, все зависимости между уровнями (пакетами) направлены вниз. В этом примере у нас имеются следующие Java-типы:
• OrdersController: веб-контроллер, иногда что-то вроде MVC-контроллера из Spring, обрабатывающего запросы из Веб.
• OrdersService: интерфейс, определяющий «бизнес-логику», связанную с заказами.
• OrdersServiceImpl: реализация службы заказов[63].
• OrdersRepository: интерфейс, определяющий порядок доступа к информации о заказах в хранилище.
• JdbcOrdersRepository: реализация интерфейса хранилища.
В своей статье Presentation Domain Data Layering[64] («Многоуровневая организация: представление, бизнес-логика, данные») Мартин Фаулер написал, что такая трехуровневая организация отлично подходит для начального этапа. И он не одинок. Во многих книгах, руководствах, курсах и примерах кода демонстрируются способы создания многоуровневой архитектуры. Это очень быстрый способ без особых затруднений создать и запустить что-то. Проблема, как указывает Мартин, в том, что с ростом масштаба и сложности программного обеспечения трех больших слоев кода оказывается недостаточно и приходится задумываться о более дробной организации.
Другая проблема, как уже сказал «дядюшка Боб»[65], — многоуровневая архитектура не кричит о своем практическом назначении. Поместите рядом код двух многоуровневых архитектур из разных предметных областей, и они почти наверняка будут выглядеть пугающе похожими: веб-интерфейсы, службы и хранилища. Многоуровневые архитектуры страдают еще от одного большого недостатка, но мы поговорим о нем позже.
Упаковка по особенностям
Другой вариант организации кода — «упаковка по особенностям». Это разделение по вертикали, основанное на объединении связанных особенностей, предметных понятий и общих корней (если использовать терминологию предметно-ориентированного проектирования). В типичных реализациях, которые мне доводилось видеть, все типы помещаются в один Java-пакет, имя которого отражает идею группировки.
При такой организации, изображенной на рис. 34.2, имеются те же интерфейсы и классы, но они помещаются в единый Java-пакет. Ее легко получить простым рефакторингом из «упаковки по уровням», но теперь верхнеуровневая структура кричит о предметной области. Теперь видно, что кодовая база имеет какое-то отношение к заказам, а не к Веб, не к службам и не к хранилищам. Другим преимуществом является относительная простота поиска кода для изменения, например, когда потребуется изменить вариант использования «просмотр заказов». Весь код сосредоточен вместе, а не разбросан по разным Java-пакетам[66].
Рис. 34.2. Упаковка по особенностям
Мне часто встречаются команды разработчиков, испытывающие трудности с разделением на горизонтальные уровни («упаковка по уровням») и переключающиеся на разделение по вертикали («упаковка по особенностям»). По моему мнению, оба подхода не оптимальны. Дочитав книгу до этого места, многие из вас подумают, что можно сделать лучше, и они будут правы.
Порты и адаптеры
Как говорил «дядюшка Боб», подходы, такие как «порты и адаптеры», «гексагональная архитектура», «граница, управление, сущность» и др., придуманы с целью создания архитектур, в которых прикладной/предметный код независим и отделен от технических деталей реализации, таких как фреймворки и базы данных. Такие базы кода часто состоят из двух областей: «внутренняя» (предметная) и «внешняя» (инфраструктура), как показано на рис. 34.3.
Рис. 34.3. Кодовая база с внутренней и внешней областями
«Внутренняя» область включает все предметные понятия, а «внешняя» отвечает за взаимодействия с внешним миром (то есть содержит пользовательские интерфейсы, базы данных, механизмы интеграции со сторонними продуктами). Главное правило при такой организации: «внешняя» область зависит от «внутренней», но никогда наоборот. На рис. 34.4 изображена версия реализации случая использования «просмотр заказов».
Рис. 34.4. Случай использования «просмотр заказов»
Пакет com.mycompany.myapp.domain в этой версии — «внутренний», а другие пакеты — «внешние». Обратите внимание, что зависимости указывают в сторону «внутренней» области. Внимательный читатель наверняка заметит, что класс OrdersRepository переименован в Orders. Это объясняется влиянием правил предметно-ориентированного проектирования, которые требуют всему, что находится «внутри», давать простые имена из словаря «универсального предметного языка». Так, в предметных дискуссиях мы говорим «заказы», а не «хранилище заказов».
Также стоит отметить, что это существенно упрощенная версия UML-диаграммы классов, потому что в ней отсутствуют такие элементы, как интеракторы и объекты для передачи данных через границы зависимостей.
Упаковка по компонентам
В целом я согласен с рассуждениями о принципах SOLID, эквивалентности повторного использования (REP), согласованного изменения (CCP) и совместного повторного использования (CRP) и большинством советов в этой книге, но я пришел к немного другим выводам, касающимся организации кода. Поэтому я представлю еще один вариант, который я называю «упаковка по компонентам». Прежде всего, в своей карьере я много лет посвятил созданию корпоративного программного обеспечения, в основном на Java, во многих прикладных областях. Эти программные системы сильно отличались. Подавляющее их число были основаны на Веб, но имелись также клиент-серверные[67], распределенные, основанные на обмене сообщениями и некоторые другие системы. Хотя технологии отличались, всех их объединяла традиционная многоуровневая архитектура.
Я уже упоминал пару причин, почему многоуровневую архитектуру следует считать неудовлетворительной, но это еще не все. Цель многоуровневой архитектуры — отделить код, выполняющий схожие функции. Веб-интерфейс отделяется от бизнес-логики, которая, в свою очередь, отделяется от механизмов доступа к данным. Как было показано на UML-диаграмме классов, с точки зрения реализации уровень примерно соответствует пакету. С точки зрения доступности кода, чтобы OrdersController мог зависеть от интерфейса OrdersService, последний должен быть объявлен общедоступным, потому что класс и интерфейс находятся в разных пакетах. Аналогично общедоступным должен быть объявлен интерфейс OrdersRepository, так как он используется классом OrdersServiceImpl, находящимся за пределами пакета, определяющего функции доступа к хранилищу.
В настоящей многоуровневой архитектуре стрелки зависимостей всегда должны быть направлены вниз. Уровни должны зависеть только от соседнего, нижележащего уровня. В результате введения некоторых правил, определяющих, как должны зависеть элементы в базе кода, получается красивый, чистый, ациклический граф зависимостей. Большая проблема в том, что мы можем хитрить, вводя некоторые нежелательные зависимости, и при этом получать замечательный ациклический граф зависимостей.
Предположим, что на работу был нанят новый специалист, он присоединился к вашей команде и вы поручаете ему реализовать другой вариант использования, связанный с заказами. Как всякий новичок, этот человек хочет произвести впечатление и реализовать порученное ему задание максимально быстро. Посидев несколько минут с чашкой кофе, новичок замечает существующий класс OrdersController и решает, что это именно тот код, который должен использоваться новой веб-страницей, порученной ему. Но ему нужны некоторые данные о заказах из базы данных. Новичка озаряет: «О, здесь уже есть интерфейс OrdersRepository. Я могу просто внедрить реализацию в мой контроллер. Отлично!» Спустя несколько минут он создает действующую веб-страницу. Но получившаяся UML-диаграмма выглядит, как показано на рис. 34.5.
Стрелки зависимостей все еще направлены вниз, но теперь для некоторых вариантов использования OrdersController минует OrdersService. Такую организацию часто называют нестрогой многоуровневой архитектурой, так как уровням позволено перепрыгивать через смежные, соседние уровни. В некоторых ситуациях это делается намеренно, например, если вы пытаетесь следовать шаблону CQRS[68]. Во многих других случаях нежелательно перепрыгивать через уровень бизнес-логики, особенно если эта бизнес-логика отвечает за авторизацию доступа к отдельным записям, например.
Рис. 34.5. Нестрогая многоуровневая архитектура
Новый вариант использования работает, но наверняка реализован не так, как вам хотелось бы. Нечто подобное я видел во многих командах, которые посещал как консультант, и обычно это проявляется, когда команды начинают выяснять, как выглядит база кода, часто в первый раз.
В такой ситуации необходимо установить правило — архитектурный принцип, — которое гласит, например: «Веб-контроллеры никогда не должны обращаться к хранилищу непосредственно». Проблема, конечно, заключается в исполнении правила. Многие команды, с которыми я встречался, заявляли: «Мы обеспечиваем соблюдение этого принципа строгой дисциплиной и обзорами кода, потому что доверяем нашим разработчикам». Это хорошо, что есть такая уверенность, но все мы знаем, что происходит, когда бюджет и сроки начинают приближаться к концу.
Намного меньше команд сообщали мне, что они используют инструменты статического анализа (например, NDepend, Structure101, Checkstyle) для автоматической проверки и выявления архитектурных нарушений во время сборки. Возможно, вы уже видели такие правила; обычно они имеют форму регулярных выражений или строк с шаблонными символами, которые указывают: «типы в пакете **/web не должны использоваться типами в **/data» и проверяются после этапа компиляции.
Это немного грубоватый подход, но он может помочь, сообщая о нарушениях архитектурных принципов, которые (по вашему мнению) должны помешать вашей команде разработчиков выполнить сборку. Проблема обоих подходов в том, что они чреваты ошибками и цикл обратной связи дольше, чем хотелось бы. Если отключить проверку, в результате код может превратиться в «большой ком грязи»[69]. Лично я хотел бы, чтобы за соблюдением архитектурных принципов следил компилятор.
Это ведет нас к варианту «упаковка по компонентам». Цель этого гибридного подхода, обсуждавшегося до сих пор, — упаковать все обязанности, связанные с одним крупным компонентом, в единый Java-пакет. Речь идет о сервис-ориентированном представлении программной системы, что, собственно, мы наблюдаем в архитектурах микрослужб. Подобно портам и адаптерам, интерпретирующим Веб как всего лишь еще один механизм доставки, методика «упаковка по компонентам» помогает отделить пользовательский интерфейс от этих крупных компонентов. На рис. 34.6 показано, как мог бы выглядеть вариант «просмотр заказов».
По сути, этот подход связывает «бизнес-логику» и код для работы с хранилищем в единое нечто, что мы называем «компонентом». Выше в книге дядюшка Боб дал такое определение компонента:
Компоненты — это единицы развертывания. Они представляют наименьшие сущности, которые можно развертывать в составе системы. В Java — это jar-файлы.
Рис. 34.6. Вариант использования «просмотр заказов»
Мое определение компонента немного отличается: «Группа функциональных возможностей, находящихся за общим чистым интерфейсом, которые постоянно находятся внутри среды выполнения, такой как приложение». Это определение взято из описания модели C4 программной архитектуры («C4 software architecture model»)[70], которая определяет простую иерархическую организацию статических структур программной системы в терминах контейнеров, компонентов и классов (или кода). В этом описании говорится, что программная система состоит из одного или нескольких контейнеров (например, веб-приложений, мобильных приложений, обычных приложений, баз данных, файловых систем), каждый из которых содержит один или несколько компонентов, которые, в свою очередь, реализуются одним или несколькими классами (или кодом). Находится ли каждый компонент в отдельном jar-файле — это уже второстепенный вопрос.
Ключевое преимущество подхода «упаковки по компонентам» заключается в размещении всего кода, например, имеющего отношение к обработке заказов, в одном месте — в компоненте OrdersComponent. Задачи внутри компонента все еще разделены, то есть бизнес-логика отделена от функций доступа к хранилищу, но это разделение является уже деталью реализации компонентов, о которой потребителям знать не обязательно. Это напоминает организацию микрослужб или сервис-ориентированную архитектуру, когда имеется отдельная служба OrdersService, инкапсулирующая все, что связано с обработкой заказов, отличаясь лишь режимом разделения.
Организацию монолитного приложения в виде набора тщательно проработанных компонентов можно рассматривать как шаг в направлении архитектуры микрослужб.
Дьявол в деталях реализации
На первый взгляд кажется, что все четыре подхода представляют собой разные способы организации кода и поэтому могут считаться разными архитектурными стилями. Это ощущение начинает быстро укрепляться, если нет понимания деталей реализации.
Я постоянно наблюдаю чересчур свободное использование модификатора доступа public в таких языках, как Java. Похоже, что разработчики используют ключевое слово public инстинктивно, не думая. Этот инстинкт хранится в нашей мышечной памяти. Если не верите, взгляните на примеры кода в книгах, руководствах и открытых фреймворках на сайте GitHub. Эта тенденция, похоже, никак не связана с архитектурным стилем, используемым для организации кода, будь то горизонтальные уровни, порты и адаптеры или что-то еще.
Объявление всех типов общедоступными означает отказ от возможностей инкапсуляции, предлагаемых языком программирования. Как результат, это открывает возможность любому написать код с реализацией конкретного класса, нарушающий используемый архитектурный стиль.
Организация и инкапсуляция
Взглянем на эту проблему с другой стороны. Если все типы в Java-приложении объявить общедоступными, пакеты превратятся в простой механизм организации (в группировки, как папки), утратив свойства инкапсуляции. Так как общедоступные типы могут беспрепятственно использоваться в любой точке приложения, вы фактически можете игнорировать пакеты, потому что они практически не несут никакой ценности. В результате, если вы игнорируете пакеты (потому что они не имеют никаких средств инкапсуляции и сокрытия), становится совершенно неважно, какой архитектурный стиль вы пытаетесь воплотить. Если все типы объявить общедоступными, пакеты Java на UML-диаграммах, представленных выше, превращаются в ненужную деталь. По сути, при злоупотреблении подобными объявлениями все четыре архитектурных подхода, представленные выше в этой главе, становятся практически неотличимыми друг от друга (рис. 34.7).
Рис. 34.7. Все четыре архитектурных стиля идентичны
Обратите внимание на стрелки между типами на рис. 34.7: они не зависят от архитектурного стиля, который вы пытаетесь воплотить. Концептуально стили очень разные, но синтаксически они идентичны. Кроме того, можно даже утверждать, что после объявления всех типов общедоступными эти четыре стиля превращаются лишь в четыре способа описания традиционной архитектуры с горизонтальными уровнями. Это ловкий фокус, и, конечно же, никто не будет объявлять все свои Java-типы общедоступными. За исключением случаев, когда это действительно делается. И я видел их.
Модификаторы доступа в Java не идеальны[71], но их игнорирование может вызывать проблемы. Порядок распределения типов Java по пакетам фактически может иметь большое значение для доступности (или недоступности) этих типов, когда модификаторы доступа применяются соответственно. Если вернуть пакеты и отметить те типы (сделав их бледнее на диаграмме), которым можно дать более ограничивающий модификатор, картина станет интереснее (рис. 34.8).
Двигаясь слева направо в подходе «упаковка по уровням», интерфейсы OrdersService и OrdersRepository должны быть объявлены общедоступными, потому что имеют входящие зависимости от классов, находящихся за пределами пакета, в котором объявлены эти интерфейсы. Классам с реализациями (OrdersServiceImpl и JdbcOrdersRepository), напротив, можно придать более ограниченную видимость (на уровне пакета). Никто не должен знать об их существовании; они являются деталями реализации.
В подходе «упаковка по особенностям» единственной точкой входа в пакет является OrdersController, поэтому доступ ко всему остальному можно ограничить рамками пакета. Важно отметить, что в такой ситуации никакой другой код, находящийся за пределами этого пакета, не сможет получить никакой информации о заказах в обход контроллера. Иногда это может быть нежелательно.
В подходе с портами и адаптерами интерфейсы OrdersService и Orders имеют входящие зависимости из других пакетов, поэтому они должны быть объявлены общедоступными. И снова, доступность классов реализации
Рис. 34.8. Бледно-серым цветом выделены типы с более ограничивающим модификатором доступа
можно ограничить рамками пакета и внедрять зависимости во время выполнения.
Наконец, в подходе «упаковка по компонентам» интерфейс OrdersComponent имеет входящую зависимость от контроллера, но доступ ко всему остальному можно ограничить рамками пакета. Чем меньше общедоступных типов, тем меньше число потенциальных зависимостей. В данном случае код за пределами этого пакета не имеет возможности[72] напрямую использовать интерфейс OrdersRepository или его реализацию, поэтому соблюдение архитектурного принципа можно переложить на компилятор. То же самое можно проделать в .NET с помощью ключевого слова internal, но при этом придется создать отдельные сборки для всех компонентов.
Для большей ясности отмечу, что все, о чем рассказывалось здесь, относится к монолитному приложению, когда весь код находится в едином дереве исходных текстов. Если вы создаете такое приложение, я рекомендовал бы проводить в жизнь архитектурные принципы, опираясь на компилятор, а не полагаться на самодисциплину и инструменты, выполняющиеся после компиляции.
Другие режимы разделения
Кроме средств, поддерживаемых языком программирования, часто существуют другие способы разделения зависимостей в исходном коде. Для Java имеются свои инфраструктуры, такие как OSGi, и новейшая система модулей в Java 9. При правильном использовании системы модулей позволяют разделить типы, объявленные как public, и публикуемые (published) типы. Например, можно создать модуль Orders, в котором все типы объявлены как public, но опубликовать только ограниченное число этих типов для внешних потребителей. Это долгожданное нововведение, и я с энтузиазмом воспринимаю появление новой системы модулей Java 9, которая даст нам еще один инструмент для создания хорошего программного обеспечения и вновь разбудит в людях интерес к дизайнерскому мышлению.
Другая возможность разделить зависимости на уровне исходного кода — создать несколько разных деревьев с исходным кодом. Для портов и адаптеров, например, можно было бы создать три таких дерева:
• Исходный код с предметной и бизнес-логикой (то есть все, что не зависит от выбора технологии и фреймворков): OrdersService, OrdersServiceImpl и Orders.
• Исходный код веб-интерфейса: OrdersController.
• Исходный код, реализующий хранение данных: JdbcOrdersRepository.
Последние два дерева имеют зависимости времени компиляции от кода с предметной и бизнес-логикой, который сам по себе ничего не знает о веб-интерфейсе и особенностях хранения данных. С точки зрения реализации это можно сделать, настроив отдельные модули или проекты в конфигурации инструмента сборки (например, Maven, Gradle, MSBuild). В идеале этот шаблон можно применить для отделения деревьев с исходным кодом всех компонентов приложения.
Однако это слишком идеалистическое решение, потому что такое разбиение исходного кода влечет за собой проблемы производительности, сложности и сопровождения.
Более простой подход, используемый теми, кто реализует архитектуру портов и адаптеров, заключается в создании двух деревьев с исходным кодом:
• Предметный код («внутренняя» область).
• Инфраструктурный код («внешняя» область).
Его хорошо иллюстрирует диаграмма (рис. 34.9), которую многие используют для обобщения архитектуры портов и адаптеров с ее зависимостью времени компиляции инфраструктурного кода от предметного.
Рис. 34.9. Предметный и инфраструктурный код
Такой подход к организации исходного кода тоже дает положительные результаты, но вы должны помнить о потенциальных компромиссах. Это то, что я называю «окружным антишаблоном портов и адаптеров». В Париже (Франция) имеется окружная автодорога с названием Boulevard Périphérique (бульвар Периферик), позволяющая обогнуть Париж, не утруждая себя сложностями движения внутри города. Включение всего инфраструктурного кода в единое дерево подразумевает потенциальную возможность для кода из любой части приложения (например, веб-контроллера) напрямую вызывать код из другой части приложения (например, функции для работы с базой данных), без пересечения предметной области. Это особенно верно, если забыть применить в том коде соответствующие модификаторы доступа.
Заключение: недостающий совет
Основная цель этой главы — подчеркнуть, что любые, самые лучшие дизайнерские намерения можно уничтожить в мгновение ока, если не учитывать тонкости стратегии реализации. Подумайте, как ваш дизайн должен отображаться в структуру кода, как организовать этот код и какие режимы разделения применять во время выполнения и компиляции. Старайтесь оставлять открытыми любые возможности, но будьте прагматичными и учитывайте численность вашей команды, навыки ее членов, сложность решения и ограничения по времени и бюджету. Также подумайте, как использовать компилятор для принудительного соблюдения выбранного архитектурного стиля, и следите за связями в других областях, таких как модели данных. Дьявол кроется в деталях реализации.