Статья также доступна на украинском (перейти к просмотру).
Оглавление
- Базовые архитектуры приложений
- Основные компоненты архитектуры Explicit
- Инструменты, использующие приложение
- Поры
- Группы адаптеров
- Инверсия управления приложением
- Строение блока Application Core
- Блоки функций или компоненты
- Управление системой
Архитектура MVC Архитектура MVC и другая «классика» не удовлетворяет в полной мере запросам современной веб-разработки. И поэтому поиск совершенной программной архитектуры веб-приложений является на сегодняшний день весьма актуальной темой среди разработчиков. Представляем произвольный перевод работы Herberto Graça, посвященной указанной теме. Автор предлагает свой вариант архитектуры под условным названием Explicit architecture, которая включает в себя только лучшее из того, что было ранее представлено ведущими специалистами в этой области.
Базовые архитектуры приложений
Концепция Explicit architecture е возникло на голом месте, а явилось результатом комбинирования элементов архитектур, уже проверенных временем. Для этого будет целесообразным хотя бы в кратком виде представить характеристики этих базовых архитектур.
Архитектура EBI
Концепция EBI (Entity Boundary Interactor) первые была описана в начале 90-х годов И. Джейкобсоном в своей книге, посвященной объектно-ориентированной разработке программного обеспечения. Но тогда он использовал несколько иную аббревиатуру для обозначения технологии (Entity Interface Control) из соображений избегания путаницы с существующими концепциями. Но принцип остается прежним.
Сущность концепции заключается в четком разделении архитектуры приложения на три условно независимых блока:
- Данные предметной области и набор операций над ними (Entity);
- Набор инструментов, зависящих от системной среды и обеспечивающих связь системы с внешней средой (Interface);
- Набор управляющих операций, не вошедших в другие блоки (Control).
На Рисунке 1. приведена графическая интерпретация указанных блоков, дающая представление о сущности каждого из них.
Рисуонк 1. Графическая интерпретация блоков EBI.
Любое взаимодействие системы с внешним миром возможно только посредством блока Interface / Boundary, по сути, маскирующий от нее объекты внешней среды и способ их доставки. Таким образом, внешним объектом здесь может быть как человек-пользователь, так и приложение или внешнее устройство. И это не отразится на качестве работы системы.
Архитектура Ports & Adapters
Концепция впервые была предложена А.А.Кокберном в начале двухтысячных лет. Ее сущность состоит в том, что приложение может взаимодействовать/управляться в равной степени, как пользователем, так и устройством или любой другой сущностью изолированно от других процессов. То есть приложение одновременно может разрабатываться, тестироваться или с ним могут выполняться какие-либо другие действия независимо от других взаимодействий.
На Рисунку 2. графически отражена сущность указанной концепции.
Рисунок 2. Интерпретация концепции Ports & Adapters.
Согласно этой концепции, архитектура приложения может быть представлена ??в виде трех уровней: представление, бизнес-логика и данные (Рис. 3).
Рисунок 3. Уровни архитектуры Ports & Adapters.
Приложение здесь выступает в роли центрального ядра, использующего только те данные, которые поставляет ему порт. Последняя изолирует ядро ??от внешних инструментов и технологий поставки данных. Ядро не владеет информацией, кто является поставщиком данных и кто их получает. Итак, есть полная изоляция ядра приложения от технологий связи и доставки данных.
Технология DDD
Концепция DDD (Domain-Driven Design) впервые была предложена Э. Эвансом в своей книге, посвященной проектированию приложений на базе знаний о предметной области для разрабатываемого приложения. Ее основная задача – создание обобщенной концепции понимания принципов построения и работы приложения как со стороны разработчиков, так и заказчиков. То есть концепция предполагает максимальную ориентированность разработки на предметную область приложения.
Согласно DDD архитектура приложения должна быть разделена на несколько независимых уровней:
- Пользовательский интерфейс (UI);
- Прикладной уровень (Application);
- Уровень домена (Domain);
- Инфраструктура (Infrastructure).
Каждый из указанных уровней выполняет свои функции и практически полностью изолирован от других уровней, что является признаком технологии.
Архитектура Onion
Концепция была предложена разработчиком Д. Палермо в 2008 году, которая по своей сути максимально приближена к архитектуре. Ports & Adapters, которую мы рассмотрели раньше. Однако, в отличие от последней, к ней добавляется еще один уровень абстракции – разбиение системы на внутреннем и внешнем уровне. Внутренний уровень включает в себя бизнес-логику приложения, а внешний – механизмы доставки и инфраструктуру (см. Рис. 4).
Рисунок 4. Архитектура Onion.
Концепция поддерживает идею изоляции ядра приложения от инфраструктуры с помощью адаптера. Это упрощает замену инструментария и механизмов доставки, не изменяя при этом ядро ??приложения. Таким образом, здесь заметнее, чем в других архитектурах проявляются следующие характеристики:
- Внутренние уровни не знают о строении внешних;
- Наружные зависят от внутренних.
Это означает, что как и в Ports & Adapters направление связи, то есть зависимости направлены извне в центр системы, что является характерным признаком любой независимой объектной модели, в нашем случае – модели предметной области.
Таким образом, архитектура приложения, опирающаяся на Onion, построенная на независимой объектной модели. И это главная характеристика системы.
Основные компоненты архитектуры Explicit
Наша концепция построения архитектуры приложения опирается на те же принципы, которые были использованы авторами технологий EBI и Ports & Adapters, а именно:
- Отсоединение внешнего кода приложения от внутреннего;
- Существование отдельных независимых адаптеров для соединения ядра с внешним миром.
Изложение информации о строении архитектуры будет постепенно с рассмотрением каждого из элементов, начиная от основных концептуальных решений и заканчивая менее значимыми элементами и принципами их взаимодействия.
Выделим три основных блока архитектуры (см. Рис. 5):
- Инструменты запуска и управления пользовательским интерфейсом (User Interface);
- Ядро приложения, включающее системную бизнес-логику (Application Core);
- Инфраструктура, содержащая набор инструментов для соединения ядра с внешними поставщиками данных (Infrastructure).
Рисунок 5. Основные блоки архитектуры Explicit.
Ядро Application Core является основным блоком системы, который может взаимодействовать со многими типами внешних поставщиков команд и данных – мобильные и стационарные веб-приложения, консоли терминала и любые другие программные средства, использующие API.
Как и во многих базовых архитектурах, направление выполнения команд здесь идет от внешнего интерфейса к ядру приложения. Затем от ядра к блоку Infrastructure, который обычно включает в себя различные типы хранилищ данных. Результаты выполнения запроса с Infrastructure передаются в блок Application Core, а оттуда уже передаются во внешний интерфейс, из которого был осуществлен запрос. Графически этот процесс представлен на Рис. 6.
Рисунок 6. Схема взаимодействия главных компонентов архитектуры Explicit.
Инструменты, использующие приложение
Любое веб-приложение при своей работе использует ряд инструментов (Instruments), не относящиеся к коду его ядра. Эти элементы могут находиться как на стороне UI (первая группа), так и на стороне противоположной части архитектуры – в пределах блока Infrastructure (вторая группа).
В первом случае в качестве таких инструментов может выступать пользовательский терминал, веб-сервер и другие источники поступления управляющих команд. Итак, их основная функция – управлять ядром приложения.
Вторая группа инструментов, напротив, ориентирована на выполнение команд ядра приложения. К ней обычно относятся СУБД, поисковые сервисы и другой подобный инструментарий.
Таким образом, при написании кода для соединения ядра приложения с «внешним миром» необходимо учитывать указанную специфику каждой группы инструментов.
Порты
Элемент Port является точкой входа в блок системы Application Core и определяет как последний будет взаимодействовать с элементами группы Instruments. В грубом приближении этот элемент можно считать обычным интерфейсом, обеспечивающим связь ядра с внешними поставщиками данных. Они являются составной частью бизнес-логики и потому создаются только для нужд блока. Application Core.
Группы адаптеров
Элементы архитектуры типа Adapters служат для соединения ядра с представителями каждой из групп элементов Instruments, то есть обеспечивают взаимодействие последних с бизнес-логикой приложения. Следует отметить, что адаптеры не относятся к коду бизнес-логики, а являются внешними элементами.
Так же, как и элементы Instruments, даптеры разделяются на две группы – первичные и вторичные. Первые «рабюотают» с инструментами первой группы (управляющими), вторые – с инструментами второй группы (зависимыми от ядра).
Первичные адаптеры
Эти элементы с помощью портов передают данные и управляющие команды на вход Application Core, предварительно превращая его в вызовы методов (см. Рис. 7).
Рисунок 7. Схема взаимодействия первичных адаптеров с ядром приложения.
Фактически они являются контроллерами или командами, которые интегрируются с кодом ядра в виде объекта, один из реализуемых классов нужный интерфейс для установления связи Это может быть интерфейс службы, репозитория или шины управления, но в любом случае он будет выполнять функции элемента Port, который мы рассматривали выше.
Вторичные адаптеры
Эти элементы реализуют определенный интерфейс и вводятся в Application Core в том месте, где требуется наличие элемента Port (см. Рис. 8).
Рисунок 8. Схема взаимодействия вторых адаптеров с ядром приложения.
На практике это выглядит примерно так. Случается ситуация, когда для работы приложения необходимо изменить тип хранилища данных, например, MySQL на MongoDB. В этом случае мы создаем новый элемент Adapter, который и реализует интерфейс для MongoDB. После этого остается только заменить предыдущий элемент и система готова к работе с новым типом хранилища.
Инверсия управления приложением
Как и большинство базовых архитектур приложений, которые мы рассматривали выше, наша система построена таким образом, что зависимости направлены снаружи в центр, а не наоборот. То есть, имеется инверсия управления на системном уровне. Это проявляется, в частности, в том, что ядро ??приложения зависит только от элементов Port, в то время как адаптеры зависят не только от конкретного элемента Port, но и от соответствующего элемента группы Instrumets (см. Рис. 9).
Рисунок 9. Инверсия управления приложением.
Заметим, что в данной ситуации элементы Port должны создаваться только для нужд центрального блока системы.
Строение блока Application Core
Так же, как это было предложено в архитектуре Onion, мы введем дополнительные уровни абстракции, которые помогут упорядочить внутренний код приложения. При этом инверсия в управлении приложением остается.
Прикладной уровень
Этот уровень, в частности, включает:
- Обработчики запросов и команд;
- Службы приложений;
- Интерфейсы портов, адаптеров, поисковиков и других элементов;
- Услуги инициализации событий приложения.
Первые две группы программ, в частности, включают управляющий процесс и логику развертывания варианта применения. При своей работе они могут использовать репозитории и другие хранилища данных для работы с сущностями.
Обработчики могут включать в себя логику для реализации варианта применения или вызвать ее из службы приложения при получении соответствующей команды. Выбор способа их использования зависит от типа задачи.
События, инициализируемые сервисами инициализации, запускают логику, зависящую от варианта применения. Это, например, могут быть сообщения от программного интерфейса, отправка почтовых сообщений или другие события.
Под вариантами применения здесь понимаются процессы, которые могут быть запущены в центральном компоненте системы с разными типами интерфейсов. Например, для обычных пользователей может использоваться один тип интерфейса, для менеджеров – второй, для терминалов – третий и т. д. Указанные процессы могут инициировать на этом уровне подключение того или иного интерфейса (см. Рис. 10).
Рисунок 10. Схема взаимодействия частей системы на прикладном уровне.
Уровень домена
Уровень включает объекты с данными и операциями по их обработке, отображающие предметную область приложения. Указанные объекты независимы и полностью изолированы от прикладного уровня (см. Рис. 11).
Рисунок 11. Уровень домена.
Сервисы
Как уже отмечалось выше, службы приложений при работе с сущностями могут использовать репозитории и другие хранилища данных. Но иногда могут возникать ситуации, когда логика уровня домена не соответствует типу сущности и потому сама по себе напрашивается необходимость в изолировании логики от сущностей в службе приложений.
Мы предлагаем включить набор сущностей и операций над ними в отдельную службу. Эта служба будет относиться к уровню домена и будет изолирована от прикладного уровня. Однако она сможет использовать другие службы и объекты модели домена.
Модель домена
Модель является центральной частью ядра приложения, не зависящего от каких-либо внешних воздействий. Она содержит различные типы сущностей, объектов и события домена, которые инициализируются при обновлении данных и содержат новые значения свойств. Эти события можно использовать в модуле регистрации событий.
Блоки функций или компоненты
Традиционный подход к дифференциации кода приложения заключается в разбиении его на отдельные слои или уровни (см. Рис. 12).
Рисунок 12. Дифференциация кода по уровням.
Однако, в этом случае архитектура приложения слабо связана с самим приложением и особенностями предметной области, а только с особенностями Фреймворка, с помощью которого он был создан. Для этого мы предлагаем использование подхода, когда код разбивается не на уровне, а на блоки функций. Эта идея уже была высказана ранее, в частности разработчиком Р. Мартином (см. Рис. 13).
Рисунок 13. Дифференциация кода приложения по компонентам.
Указанные на диаграмме блоки функций проходимы для всех слоев кода. Таким блоком может быть пользовательская учетная запись, сам пользователь и любая другая сущность. на Рисунке 14. представлены примеры разделения кода по компонентам.
Рисунок 14. Пример разбиения кода приложения на компоненты.
Разъединение компонентов
Так же, как и для традиционных речевых конструкций для компонентов, важно разработать эффективный механизм их внутреннего взаимодействия по аналогии, например, с механизмом инкапсуляции для классов и объектов. Такая изолированность, как известно, оказывает положительное влияние на работу любой программы.
Однако реализовать указанные механизмы для наших компонентов по ряду причин будет гораздо сложнее, чем для классов. Здесь понадобятся решения на архитектурном уровне, которые, в частности, могут включать в себя общее ядро, события, средства обнаружения сервисов и другие механизмы.
Строение логики
Проблема здесь состоит в том, чтобы взаимодействующие компоненты были максимально независимы друг от друга. Наше предложение – это создание диспетчера событий для отправки сообщений о событиях прослушивающим компонентам, а также общего ядра, которое бы содержало набор функций, доступных для всех компонентов. При этом для описания событий целесообразно использовать универсальное языковое средство, например, такое как JSON.
Такой подход подойдет для приложений любого типа, в том числе и распределенных, например службы микросервисов. Однако при асинхронном типе связи невозможно получить мгновенную реакцию компонента на событие. Для решения этого вопроса необходимо создать еще один дополнительный программный элемент – службу обнаружения. При формировании запроса к ней от компонента служба передаст его соответствующему сервису, который сгенерирует ответ компонента. При этом компоненты не будут связаны между собой, а только со службой обнаружения. Это и будет являться решением вопроса полного разделения компонентов между собой.
Использование данных
Модель обращения данных между компонентами предполагает существование двух способов их использования:
- Общее хранилище;
- Отдельное хранилище для каждого компонента.
В первом случае любой компонент может приглашать любые данные и использовать их без изменения. То есть они есть только «для чтения».
Второй случай предполагает, что любой из компонентов может иметь отдельный набор данных, которые он может изменять. Кроме того, в таком хранилище могут находиться данные, которые являются копией данных других компонентов и могут использоваться владельцем хранилища. Они должны обновляться сразу после их изменения в компоненте, которому они принадлежат.
Обновление данных реализуется путем генерирования события обладателем обновляемых данных. Все остальные компоненты прослушивают события с помощью соответствующей службы и при поступлении сообщения об обновлении обновляют свою локальную копию данных.
Управление системой
Как уже отмечалось выше, направление взаимодействия основных блоков системы является следующим:
UI > Application Core > Infrastructure > Application Core > UI. Но мы еще не выяснили степень зависимости между блоками, то есть «кто является главным» и как осуществляется управление. Для объяснения этого воспользуемся UML-схемам, приведенным ниже.
Для случая отсутствия управляющей шины контроллер могут зависеть от объектов Query или службы веб-приложения (см. Рис. 15).
Рисунок 15. Организация системы управления Explicit при отсутствии шины управления.
Здесь использован интерфейс службы веб-приложений, хотя вполне можно обойтись и без него, поскольку служба входит в состав кода приложения. Она содержит логику вариантов применения, которая активируется при ответных действиях пользователя. Служба зависит от репозиториев, возвращающих активные объекты для их инициализации. Она может зависеть от диспетчера событий в случае необходимости инициализации события для всей системы.
Объект Query имеет в своем составе запрос на возвращение необработанных данных в DTO. Эти данные будут представлены пользователю системы.
В случае наличия управляющей шины вышеприведенная схема мало изменяется за исключением того, что контроллер теперь будет зависеть от команд, которые будут передаваться по шине в виде отдельных экземпляров данных. Для них затем будет подобран подходящий обработчик.
На Рисунку 16. показан обработчик, использующий службу приложений. Но это необязательно должно быть, поскольку обычно он имеет логику варианта применения, которая передается при необходимости в отдельную службу приложений.
Рисунок 16. Организация системы управления Explicit при наличии шины управления.
Обращаем внимание на то обстоятельство, что все элементы независимы друг от друга. Это касается шины, команд, запросов и обработчиков. Это соответствует одному из принципов построения нашей системы – полная независимость составных частей. Можно сконфигурировать способ взаимодействия шины с определенным обработчиком команды или запроса.
Кроме того, нами выдержано главное правило для любой современной программной архитектуры – направление зависимостей идет извне внутрь, но не наоборот. (см. Рис. 17).
Рисунок 17. Диаграмма зависимости для архитектуры Explicit.
Подписывайтесь на наш телеграмм-канал https://t.me/freehostua, чтобы быть в курсе новых полезных материалов
Смотрите наш канал Youtube на https://www.youtube.com/freehostua.
Мы в чем ошиблись, или что-то пропустили?
Напишите об этом в комментариях, мы с удовольствием ответим и обсуждаем Ваши замечания и предложения.
Дата: 03.04.2024 Автор: Александр Ровник
|
|
Авторам статьи важно Ваше мнение. Будем рады его обсудить с Вами:
comments powered by Disqus