Марк Ричардс

Паттерны архитектуры

программного обеспечения

Общие шаблоны архитектуры и способы их применения


Исходный текст, 2015 / Русский перевод, 2023


Оглавление

Глава 1
Слоистая архитектура
(Layered Architecture)
Описание паттерна
Рисунок 1-1. Паттерн слоистой архитектуры
Компоненты в рамках модели слоистой архитектуры организованы в горизонтальные слои, каждый из которых выполняет определённую роль в приложении (например, логика представления или бизнес-логика).

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

В некоторых случаях слой бизнес-логики и слой доступа к данным объединяются в один слой бизнес-логики, особенно когда логика доступа к данным (например, SQL или HSQL) встроена в компоненты бизнес-логики. Таким образом, небольшие приложения могут иметь только три слоя, в то время как более крупные и сложные бизнес-приложения могут содержать пять или более слоёв.
Каждый слой паттерна слоистой архитектуры имеет определённую роль и ответственность в приложении. Например, слой представления отвечает за работу с пользовательским интерфейсом и логику взаимодействия с браузером, в то время как бизнес-слой будет отвечать за выполнение конкретных бизнес-правил, связанных с запросом. Каждый слой в архитектуре формирует абстракцию вокруг работы, которую необходимо выполнить для удовлетворения конкретного бизнес-запроса.

Например, слою представления не нужно знать или беспокоиться о том, как получить данные о клиенте; ему нужно только отобразить эту информацию на экране в определённом формате. Аналогично, слой бизнес-логики не должен быть озадачен тем, как форматировать данные клиента для отображения на экране, или даже тем, откуда поступают данные клиента. Ему нужно только получить данные из слоя доступа к данным, применить бизнес-логику на основе этих данных (например, вычислить значения или агрегировать данные) и передать эту информацию слою представления.
Ключевые особенности
Одной из сильных сторон слоистой архитектуры является разделение задач (separation of concerns) между компонентами. Компоненты определённого слоя работают только с логикой, относящейся к этому слою. Например, компоненты в слое представления имеют дело только с логикой представления, а компоненты, расположенные в бизнес-слое — только с бизнес-логикой. Такого рода классификация компонентов упрощает построение эффективных моделей ролей и ответственности в архитектуре, а также упрощает разработку, тестирование, надзор и обслуживание приложений с использованием этого паттерна архитектуры, благодаря чётко определённым интерфейсам компонентов и ограниченному границам компонентов.
Обратите внимание на Рисунок 1-2. Каждый из слоёв в архитектуре помечен, как закрытый. Это очень важная концепция паттерна слоистой архитектуры. Закрытый слой означает, что по мере того, как запрос перемещается от уровня к уровню, он должен проходить через слой прямо под ним, чтобы попасть на следующий. Например, запрос, исходящий от слоя представления, должен сначала пройти через слой бизнес-логики, а затем в слой доступа к данным перед окончательным попаданием в слой базы данных.

Рисунок 1-2. Закрытые уровни и запрос доступа

Так почему бы не предоставить слою представления прямой доступ к слою доступа к данным или к базе данных? В конце концов, прямой доступ к базе данных с слоя представления гораздо быстрее, чем прохождение через кучу ненужных слоев только для получения или сохранения информации из базы данных. Ответ на этот вопрос кроется в ключевой концепции паттерна, известной как уровни изоляции (layers of isolation).
Концепция изолированных слоев означает, что изменения, внесенные в один слой архитектуры, обычно не влияют на компоненты других слоев. Изменения изолированы для компонентов этого слоя и, возможно, другого связанного слоя (например, слоя доступа к данным, содержащий SQL). Если вы дадите слою представления прямой доступ к слою доступа к данным, то изменения, внесённые в SQL в пределах слоя доступа к данным будут влиять как на слой бизнес-логики, так и на слой представления, тем самым создавая очень тесно связанное приложение с большим количеством взаимозависимостей между компонентами. Такой тип архитектуры становится очень трудно и дорого изменять.
Концепция изолированных слоев также означает, что каждый слой является независимым от других. Таким образом, он практически не знает о внутренней работе других слоев архитектуры. Чтобы понять силу и важность данной концепции, рассмотрим работу по рефакторингу для преобразования структуры представления с JSP (Java Server Pages) на JSF (Java Server Faces). Если предположить, что контракты (например, модель), используемые между слоем представления и слоем бизнес-логики, остаются неизменными, то слой бизнес-логики не будет затронут рефакторингом и останется полностью независимым от типа фреймворка пользовательского интерфейса, используемого слоем представления.
Хотя закрытые слои способствуют изоляции и помогают изолировать изменения в архитектуре, бывают случаи, когда имеет смысл сделать некоторые слои открытыми. Например, предположим, что требуется добавить слой общих сервисов в архитектуру, содержащую общие компоненты услуг, к которым обращаются компоненты слоя бизнес-логики (например, классы служебных данных и строк или классы аудита и логирования). Создание нового слоя «Сервисы» в таком случае является хорошей идеей, поскольку архитектурно это ограничивает доступ к общим службам только слоем бизнес-логики (а не слоем представления). Без отдельного слоя нет ничего, что архитектурно ограничивало бы доступ слоя представления к этим общим сервисам, что затрудняет управление этим ограничением доступа.
В описанном выше примере новый слой «сервисы», скорее всего будет располагаться ниже слоя бизнес-логики, чтобы показать, что компоненты этого слоя сервисов недоступны из уровня представления. Однако, это создаёт проблему, так как запрос из слоя бизнес-логики теперь должен пройти через слой сервисов, чтобы добраться до слоя доступа к данным, что совершенно бессмысленно. Это извечная проблема слоистой архитектуры, которая решается путем создания открытых слоев в архитектуре.
Как показано на Рисунке 1-3, слой сервисов в данном случае является открытым, что означает, что запросы могут обходить этот открытый слой и переходить непосредственно к слою, расположенному ниже. В данном примере показано, что слой «Сервисы» открыт, поэтому слою бизнес-логики разрешено обходить его и переходить непосредственно к слою доступа к данным.

Рисунок 1-3. Открытые уровни и поток исполнения запроса

Использование концепции открытых и закрытых слоев помогает определить взаимосвязь между слоями архитектуры и потоками запросов, а также предоставляет проектировщикам и разработчикам необходимую информацию для понимания различных ограничений доступа к слоям в архитектуре. Если вы не умеете документировать или правильно информировать о том, какие слои в архитектуре являются открытыми и закрытыми (и почему), то это обычно приводит к тесно связанным и хрупким архитектурам, которые очень трудно тестировать, поддерживать и развёртывать.
Пример применения шаблона
Чтобы показать работу слоистой архитектуры, рассмотрим запрос от пользователя на получение информации о конкретном клиенте, как показано на Рисунке 1-4. Чёрные стрелки показывают запрос, идущий к базе данных с целью получения данных о клиенте. Красные стрелки показывают ответ, идущий обратно вверх к экрану пользователя для отображения данных. В этом примере информация о клиенте содержит данные о клиенте и его заказах.
Рисунок 1-4. Пример слоистой архитектуры
Экран клиента (Customer Screen) отвечает за принятие запроса и отображение информации о клиенте. Экран клиента не знает, где находятся данные, как они извлекаются и сколько таблиц базы данных необходимо запросить для получения данных.

Когда экран клиента получает запрос на получение информации о клиенте для конкретного человека, он передает данный запрос модулю делегирования клиента (Customer Delegate).

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

Модуль «Customer Object» в слое бизнес-логики отвечает за сбор всей информации, необходимой для бизнес запроса (в данном случае, для получения информации о клиенте).

Этот модуль обращается к модулю «Customer dao» (DAO: data access object, объект доступа к данным) в слой доступа к данным для получения данных о клиенте, а также к модулю «Order dao» для получения информации о заказе.

Эти модули, в свою очередь, выполняют SQL-запросы для получения соответствующих данных и передают их обратно модулю «Customer object» в слой бизнес-логики. После того, как модуль «Customer object» получает данные, он агрегирует их и передает информацию в модуль «Customer Delegate», который в свою очередь передает эти данные в модуль «Customer Screen». В результате мы имеем данные, которые отображаются на экране пользователя.
С технологической точки зрения существуют десятки способов реализации этих модулей. Например, на Java платформе экраном клиента может выступать JSF (Java Server Faces), соединенный с модулем «Customer Delegate» в управляемый компонент Managed Bean (Java-бин, управляемый средой JSF). Модуль «Customer Object» в слое бизнес-логики может выступать Local Spring Bean или приложение с дистанционным доступом — EJB3 bean. Объекты доступа к данным, показанные в предыдущем примере, могут реализованы как простые POJO (Plain Old Java Objects), файлы Mybatis XML Mapper, или даже объектов, инкапсулирующих необработанные вызовы JDBC или запросы Hibernate. С точки зрения платформы Microsoft экраном клиента может выступать модуль ASP (Active Server Pages), использующий фреймворк .NET для доступа к модулям C# на уровне бизнес-логики. Для модулей доступа к данным клиента и заказа может быть использован интерфейс программирования приложений — ADO (ActiveX Data Objects).
Рекомендации
Паттерн слоистой архитектуры является надёжным шаблоном общего назначения, что делает его хорошей отправной точкой для большинства приложений, особенно когда вы не уверены, какой архитектурный паттерн лучше всего подойдет для вашего приложения. Однако, при выборе этого паттерна есть несколько нюансов, которые следует учитывать при выборе архитектуры.
Первое, чего стоит остерегаться — это антипаттерн архитектурной воронки (architecture sinkhole anti-pattern). Данный антипаттерн описывает ситуацию, при которой запросы проходят через несколько слоёв архитектуры, как простая сквозная обработка с минимальной логикой или вообще без логики, выполняемой на каждом слое. Например, предположим, что слой представления отвечает на запрос пользователя на получение данных о клиенте. Слой представления передает запрос в слой бизнес-логики, который в свою очередь просто передает запрос дальше в слой доступа к данным, который затем выполняет простой SQL-вызов в слое базы данных для получения данных клиента. Затем данные передаются обратно вверх по стеку без каких-либо операций или бизнес-логики для агрегации, вычисления или преобразования данных.
В каждой слоистой архитектуре есть, как минимум несколько сценариев, которые попадают в антипаттерн «архитектурной воронки». Ключевым моментом является анализ вероятности запросов, попадающих в эту категорию. Правило Парето 80/20 является хорошей практикой для определения, столкнулись ли вы с антипаттерном «архитектурной воронки». Как правило, около 20% запросов являются простой сквозной обработкой, а 80% запросов имеют определённую бизнес-логику, связанную с запросом. Однако, если вы обнаружите, что данное соотношение изменилось и большинство ваших запросов являются простой сквозной передачей, то надо рассмотреть возможность сделать открытыми некоторые из уровней. Помните о том, что контроль за изменениями будет осуществляться сложнее из-за отсутствия изоляции уровней.
Ещё один момент, на который стоит обратить внимание, заключается в том, что слоистая архитектура склонна к созданию монолитных приложений, даже если вы разделите слой представления и слой бизнес-логики на отдельные развёртываемые единицы. Хотя для некоторых из приложений это не является проблемой, но это создаёт потенциальные проблемы с точки зрения развёртывания, стабильности и надежности, производительности и масштабируемости.
Анализ паттерна
Далее приведены оценки и общий анализ характеристик паттерна слоистой архитектуры. Оценка для каждой из характеристик основана на естественном проявлении данной характеристики, как способности, качества (архитектуры), проявляющейся при реализации паттерна, а также на том, чем в целом известен этот паттерн. Для сопоставительного сравнения и получения представления о том, как этот паттерн соотносится с остальными паттернами нашего обзора, обратитесь к Приложению А в конце книги.
Общая адаптивность (Overall agility)
Оценка: Низкая
Анализ: Общая адаптивность — это способность быстро реагировать на постоянно меняющуюся среду. Хотя, изменения могут быть изолированы, с помощью концепции изолированных слоев, внесение изменений в архитектурном паттерне является очень трудоёмким из-за монолитности реализаций, а также тесной связи компонентов.
Простота развёртывания (Ease of deployment)
Оценка: Низкая
Анализ: В зависимости от способа реализации данного паттерна развёртывание может быть проблематичным, особенно для больших приложений. Одно небольшое изменение компонента может потребовать повторного развёртывания всего приложения (или большей его части). В данном случае это приведет к необходимости планирования и выполнения развёртывания приложения в нерабочее время или в выходные дни. Поэтому слоистая архитектура не очень легко поддаётся такому подходу к разработке ПО, как непрерывная доставка, то есть Continuous delivery (CD), что ещё больше снижает общую оценку развёртывания.
Тестируемость

Оценка: Высокая

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

Оценка: Низкая

Анализ: Несмотря на то, что некоторые слоистые архитектуры могут хорошо выполнять свою работу, этот паттерн не эффективен для высокопроизводительных приложений. Это связано с необходимостью проходить через несколько слоёв архитектуры для выполнения бизнес-запроса.
Масштабируемость

Оценка: Низкая

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

Оценка: Высокая

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

Глава 2
Событийно-ориентированная архитектура (Event-Driven Architecture)