- •Оглавление
- •Предисловие
- •Введение
- •Часть I Обзор Глава 1 "Расслоение" системы
- •Развитие модели слоев в корпоративных программных приложениях
- •Три основных слоя
- •Где должны функционировать слои
- •Глава 2 Организация бизнес-логики
- •Выбор типового решения
- •Глава 3 Объектные модели и реляционные базы данных
- •Архитектурные решения
- •Функциональные проблемы
- •Считывание данных
- •Взаимное отображение объектов и реляционных структур
- •Отображение связей
- •Наследование
- •Реализация отображения
- •Двойное отображение
- •Использование метаданных
- •Соединение с базой данных
- •Другие проблемы
- •Дополнительные источники информации
- •Глава 4 Представление данных в Web
- •Типовые решения представлений
- •Типовые решения входных контроллеров
- •Дополнительные источники информации
- •Глава 5 Управление параллельными заданиями
- •Проблемы параллелизма
- •Контексты выполнения
- •Изолированность и устойчивость данных
- •Стратегии блокирования
- •Предотвращение возможности несогласованного чтения данных
- •Разрешение взаимоблокировок
- •Транзакции
- •Типовые решения задачи обеспечения автономного параллелизма
- •Параллельные операции и серверы приложений
- •Дополнительные источники информации
- •Глава 6 Сеансы и состояния
- •В чем преимущество отсутствия "состояния"
- •Состояние сеанса
- •Глава 7 Стратегии распределенных вычислений
- •Соблазны модели распределенных объектов
- •Интерфейсы локального и удаленного вызова
- •Когда без распределения не обойтись
- •Сужение границ распределения
- •Интерфейсы распределения
- •Глава 8 Общая картина
- •Предметная область
- •Источник данных
- •Платформы и инструменты
- •Другие модели слоев
- •Часть II Типовые решения Глава 9 Представление бизнес-логики Сценарий транзакции (Transaction Script)
- •Модель предметной области (Domain Model)
- •Модуль таблицы (Table Module)
- •Слой служб (Service Layer)
- •Глава 10 Архитектурные типовые решения источников данных Шлюз таблицы данных (Table Data Gateway)
- •Шлюз записи данных (Row Data Gateway)
- •Активная запись (Active Record)
- •Преобразователь данных (Data Mapper)
- •Глава 11 Объектно-реляционные типовые решения, предназначенные для моделирования поведения Единица работы (Unit of Work)
- •Коллекция объектов (Identity Map)
- •Загрузка по требованию (Lazy Load)
- •Глава 12 Объектно-реляционные типовые решения, предназначенные для моделирования структуры Поле идентификации (Identity Field)
- •Отображение внешних ключей (Foreign Key Mapping)
- •Отображение с помощью таблицы ассоциаций (Association Table Mapping)
- •Отображение зависимых объектов (Dependent Mapping)
- •Внедренное значение (Embedded Value)
- •Сериализованный крупный объект (Serialized lob)
- •Наследование с одной таблицей (Single Table Inheritance)
- •Наследование с таблицами для каждого класса (Class Table Inheritance)
- •Наследование с таблицами для каждого конкретного класса (Concrete Table Inheritance)
- •Преобразователи наследования (Inheritance Mappers)
- •Глава 13 Типовые решения объектно-реляционного отображения с использованием метаданных Отображение метаданных (Metadata Mapping)
- •Объект запроса (Query Object)
- •Хранилище (Repository)
- •Глава 14 Типовые решения, предназначенные для представления данных в Web Модель-представление-контроллер (Model View Controller)
- •Контроллер страниц (Page Controller)
- •Контроллер запросов (Front Controller)
- •Представление по шаблону (Template View)
- •Представление с преобразованием (Transform View)
- •Двухэтапное представление (Two Step View)
- •Контроллер приложения (Application Controller)
- •Глава 15 Типовые решения распределенной обработки данных Интерфейс удаленного доступа (Remote Facade)
- •Объект переноса данных (Data Transfer Object)
- •Глава 16 Типовые решения для обработки задач автономного параллелизма Оптимистическая автономная блокировка (Optimistic Offline Lock)
- •Пессимистическая автономная блокировка (Pessimistic Offline Lock)
- •Блокировка с низкой степенью детализации (Coarse-Grained Lock)
- •Неявная блокировка (Implicit Lock)
- •Глава 17 Типовые решения для хранения состояния сеанса Сохранение состояния сеанса на стороне клиента (Client Session State)
- •Сохранение состояния сеанса на стороне сервера (Server Session State)
- •Сохранение состояния сеанса в базе данных (Database Session State)
- •Глава 18 Базовые типовые решения Шлюз (Gateway)
- •Преобразователь (Mapper)
- •Супертип слоя (Layer Supertype)
- •Отделенный интерфейс (Separated Interface)
- •Реестр (Registry)
- •Объект-значение (Value Object)
- •Деньги (Money)
- •Частный случай (Special Case)
- •Дополнительный модуль (Plugin)
- •Фиктивная служба (Service Stub)
- •Множество записей (Record Set)
- •Список типовых решений
- •Шпаргалка
- •Как управлять сложным потоком функций приложения?
- •Как взаимодействовать с базой данных?
- •Как избежать загрузки в оперативную память всего содержимого базы данных?
- •Как сохранить структуры наследования в реляционной базе данных?
Слой служб (Service Layer)
Рэнди Стаффорд
Схема определения границ приложения посредством слоя служб, который устанавливает множество доступных действий и координирует отклик приложения на каждое действие
Корпоративные приложения обычно подразумевают применение разного рода интерфейсов к хранимым данным и реализуемой логике — загрузчиков данных, интерфейсов пользователя, шлюзов интеграции и т.д. Несмотря на различия в назначении, подобные интерфейсы часто нуждаются в одних и тех же функциях взаимодействия с приложением для манипулирования данными и выполнения бизнес-логики. Функции могут быть весьма сложными и способны включать транзакции, охватывающие многочисленные ресурсы, а также операции по координации реакций на действия. Описание логики взаимодействия в каждом отдельно взятом интерфейсе сопряжено с многократным повторением одних и тех же фрагментов кода.
Слой служб определяет границы приложения [12] и множество операций, предоставляемых им для интерфейсных клиентских слоев кода. Он инкапсулирует бизнес-логику приложения, управляет транзакциями и координирует реакции на действия.
Принцип действия
Слой служб может быть реализован несколькими способами, удовлетворяющими всем упомянутым выше условиям. Различия проявляются в методах распределения ответственности вне интерфейса слоя служб. Прежде чем окунуться в особенности альтернативных реализаций, позвольте осветить некоторые основополагающие аспекты.
Разновидности "бизнес-логики"
Подобно сценарию транзакции (Transaction Script) и модели предметной области (Domain Model), слой служб представляет собой типовое решение по организации бизнес-логики. Многие проектировщики, и я в том числе, любят разносить бизнес-логику по двум категориям: логика домена (domain logic) имеет дело только с предметной областью как таковой (примером могут служить стратегии вычисления зачтенного дохода по контракту), а логика приложения (application logic) описывает сферу ответственности приложения [11] (скажем, уведомляет пользователей и сторонние приложения о протекании процесса вычисления доходов). Логику приложения часто называют также "логикой рабочего процесса", несмотря на то что под "рабочим процессом" часто понимаются совершенно разные вещи.
Модель предметной области более предпочтительна в сравнении со сценарием транзакции, поскольку исключает возможность дублирования бизнес-логики и позволяет бороться со сложностью с помощью классических проектных решений. Но размещение логики приложения в "чистых" классах домена чревато нежелательными последствиями. Во-первых, классы домена допускают меньшую вероятность повторного использования, если они реализуют специфическую логику приложения и зависят от тех или иных прикладных инструментальных пакетов. Во-вторых, смешивание логики обеих категорий в контексте одних и тех же классов затрудняет возможность новой реализации логики приложения с помощью специфических инструментальных средств, если необходимость такого шага становится очевидной. По этим причинам слой служб предусматривает распределение "разной" логики по отдельным слоям, что обеспечивает традиционные преимущества расслоения, а также большую степень свободы применения классов домена в разных приложениях.
Варианты реализации
Двумя базовыми вариантами реализации слоя служб являются создание интерфейса доступа к домену (domain facade) и конструирование сценария операции (operation script). При использовании подхода, связанного с интерфейсом доступа к домену, слой служб реализуется как набор "тонких" интерфейсов, размещенных "поверх" модели предметной области. В классах, реализующих интерфейсы, никакая бизнес-логика отражения не находит — она сосредоточена исключительно в контексте модели предметной области. Тонкие интерфейсы устанавливают границы и определяют множество операций, посредством которых клиентские слои взаимодействуют с приложением, обнаруживая тем самым характерные свойства слоя служб.
Создавая сценарий операции, вы реализуете слой служб как множество более "толстых" классов, которые непосредственно воплощают в себе логику приложения, но за бизнес-логикой обращаются к классам домена. Операции, предоставляемые клиентам слоя служб, реализуются в виде сценариев, создаваемых группами в контексте классов, каждый из которых определяет некоторый фрагмент соответствующей логики. Подобные классы, расширяющие супертип слоя (Layer Supertype) и уточняющие объявленные в нем абстрактные характеристики поведения и сферы ответственности, формируют "службы" приложения (в названиях служебных типов принято употреблять суффикс "Service"). Слой служб и заключает в себе эти прикладные классы.
Быть или не быть удаленному доступу
Интерфейс любого класса слоя служб обладает низкой степенью детализации почти по определению, поскольку в нем объявляется набор прикладных операций, открытых для внешних интерфейсных клиентских слоев. Поэтому классы слоя служб хорошо приспособлены для удаленного доступа.
За удаленные вызовы приходится платить, применяя технологии распределения объектов. Чтобы обеспечить возможность совмещения методов слоя служб с контекстом объектов переноса данных (Data Transfer Object), требуется приложить немалые усилия. Не стоит недооценивать затраты на выполнение этой работы, особенно при использовании сложной модели предметной области и насыщенных интерфейсов редактирования данных для "навороченных" вариантов использования! Это весьма трудоемкое и хлопотное дело — по крайней мере второе по степени сложности после объектно-реляционного отображения. Да, и не забывайте о Первом Законе Распределения Объектов (см. главу 7, с.113).
Советую начинать со слоя служб, адресуемого в локальном режиме, и включать возможность удаленного вызова только при необходимости (если таковая вообще возникнет), размещая "поверх" слоя служб соответствующие интерфейсы удаленного доступа (Remote Facade) или снабжая нужными интерфейсами объекты слоя служб как таковые. Если приложение обладает графическим Web-интерфейсом или использует шлюзы для интеграции, основанные на Web-службах, я не могут назвать правило, которое заставляло бы располагать бизнес-логику в контексте процесса, отделенного от страниц сервера или Web-служб. Отдавая предпочтение режиму совместного выполнения, вы сэкономите много сил и времени, не утратив возможность масштабирования.
Определение необходимых служб и операций
Установить, какие операции должны быть размещены в слое служб, отнюдь не сложно. Это определяется нуждами клиентов слоя служб, первой (и наиболее важной) из которых обычно является пользовательский интерфейс. Как известно, он предназначен для поддержки вариантов использования приложения, поэтому в качестве отправной точки для определения набора операций слоя служб должны рассматриваться модель вариантов использования и пользовательский интерфейс приложения.
Как это ни грустно, большинство вариантов использования корпоративных приложений составляют банальные операции CRUD (create, read, update, delete — создать, считать, обновить, удалить) над объектами домена: создать экземпляр того, считать коллекцию этих или обновить что-нибудь еще. На практике варианты использования CRUD практически всегда полностью соответствуют операциям слоя служб.
Несмотря на сказанное выше, обязанности приложения по обработке вариантов использования никак нельзя назвать банальными. Создание, обновление или удаление в приложении объекта домена, не говоря уже о проверке правильности его содержимого, требует отправки уведомлений пользователям или другим интегрированным приложениям. Координацией откликов и отправлением их в рамках атомарных транзакций и занимаются операции слоя служб.
К сожалению, определить, в какие абстракции слоя служб необходимо сгруппировать родственные операции, далеко не просто. Универсального рецепта для решения этой проблемы не существует. Небольшому приложению вполне может хватить одной абстракции, названной по имени самого приложения. Более крупные приложения обычно разбиваются на несколько подсистем, каждая из которых включает в себя вертикальный срез всех имеющихся архитектурных слоев. В этом случае я предпочитаю создавать по одной одноименной абстракции для каждой подсистемы. В качестве альтернатив можно предложить создание абстракций, отражающих основные составляющие модели предметной области, если таковые отличаются от подсистем (например, ContractService, ProductsService) или абстракций, названных в соответствии с типами поведения (например, RecognitionService).
Особенности Java-реализации
Оба подхода, упомянутых в предыдущем разделе, — создание интерфейса доступа к домену и построение сценария операции — допускают реализацию класса слоя служб в виде объекта POJO или же компонента сеанса, не имеющего состояний. К сожалению, какое бы решение вы не выбрали, вам обязательно придется чем-то пожертвовать — либо простотой тестирования, либо легкостью управления транзакциями. Так, объекты POJO легче тестировать, поскольку для их функционирования не нужны EJB-контейнеры. С другой стороны, слою служб, реализованному в виде объекта POJO, будет труднее работать со службами распределенных транзакций, управляемых на уровне контейнера, особенно при обмене данными между разными службами. Компоненты сеанса, напротив, хорошо справляются с распределенными транзакциями, управляемыми на уровне контейнера, однако могут быть протестированы и запущены только при наличии EJB-контейнера. Проще говоря, из двух зол приходится выбирать меньшее.
Я предпочитаю реализовать слой служб с использованием локальных интерфейсов и сценария операции в виде компонентов сеанса EJB 2.0, обращающихся за логикой домена к классам объектов домена POJO. Что ни говори, а слой служб очень удобно реализовать в виде компонента сеанса, не меняющего состояния, потому что в EJB предусмотрена поддержка распределенных транзакций, управляемых на уровне контейнера. Кроме того, с помощью локальных интерфейсов, появившихся в EJB 2.0, слой служб может использовать весьма ценные службы транзакций и в то же время избежать всех неприятностей, связанных с вопросами распределения объектов.
Говоря о Java, хочу обратить ваше внимание на отличие слоя служб от интерфейса доступа посредством компонентов сеанса (Session Facade) — типового решения для J2EE, описанного в [3, 28]. Разработчики интерфейса доступа посредством компонентов сеанса намеревались избежать падения производительности, связанной с чрезмерным количеством удаленных вызовов компонентов сущностей; поэтому в данном типовом решении доступ к компонентам сущностей происходит с помощью компонентов сеанса. В отличие от этого, слой служб направлен на определение как можно большего количества общих операций, для того чтобы избежать дублирования кода и обеспечить возможность повторного использования; это архитектурное решение, которое выходит за рамки технологии. Действительно, типовое решение граница приложения (Application Boundary), описанное в [12] и ставшее прототипом слоя служб, появилось примерно на три года раньше, чем EJB. Интерфейс доступа посредством компонентов сеанса имеет много общего со слоем служб, однако на данный момент его имя, назначение и позиционирование принципиально иные.
Назначение
Преимуществом использования слоя служб является возможность определения набора общих операций, доступных для применения многими категориями клиентов, и координация откликов приложения на выполнение каждой операции. В сложных случаях отклики могут включать в себя логику приложения, передаваемую в рамках атомарных транзакций с использованием нескольких ресурсов. Таким образом, если у бизнес-логики приложения есть более одной категории клиентов, а отклики на варианты использования передаются через несколько ресурсов транзакций, использование слоя служб с транзакциями, управляемыми на уровне контейнера, становится просто необходимым, даже если архитектура приложения не является распределенной.
Гораздо легче ответить на вопрос, когда слой служб не нужно использовать. Скорее всего, вам не понадобится слой служб, если у логики приложения есть только одна категория клиентов, например пользовательский интерфейс, отклики которого на варианты использования не охватывают несколько ресурсов транзакций. В этом случае управление транзакциями и выбор откликов можно возложить на контроллеры страниц (Page Controller), которые будут обращаться непосредственно к слою источника данных.
Тем не менее, как только у вас появится вторая категория клиентов или начнет использоваться второй ресурс транзакции, вам неизбежно придется ввести слой служб, что потребует полной переработки приложения.
Дополнительные источники информации
Слой служб имеет не так уж много прототипов. В его основу легло типовое решение граница приложения (Application Boundary), описанное Алистером Кокберном (Alistair Cockburn) в [12]. В источнике [2], посвященном удаленным службам, обсуждается роль интерфейсов доступа в распределенных системах. Сравните эти концепции с описаниями типового решения интерфейс доступа посредством компонентов сеанса (Session Facade), содержащимися в [3, 28]. Рассмотрение вариантов использования в соответствии с типами поведения, приведенное в справочнике [11], может помочь в определении обязанностей приложения, координацию которых должен проводить слой служб. Похожие идеи под названием "системных операций" представлены в более раннем справочнике [13], посвященном технологии Fusion.
Пример: определение зачтенного дохода (Java)
Продолжим рассмотрение примера определения зачтенного дохода, который был начат в разделах, посвященных типовым решениям сценарий транзакции и модель предметной области. На сей раз я продемонстрирую использование слоя служб, реализовав его операцию в виде сценария, который инкапсулирует в себе логику приложения и обращается за логикой домена к классам домена. Для реализации слоя служб (вначале с помощью объектов POJO, а затем компонентов EJB) будет использован подход сценария операции.
Чтобы продемонстрировать работу слоя служб, расширим нашу задачу, добавив в нее немного логики приложения. Пусть варианты использования нашего приложения предполагают, что при вычислении зачтенного дохода по контракту приложение должно отослать отчет администратору контракта и опубликовать сообщение посредством промежуточного программного обеспечения для уведомления интефированных приложений.
Для начала немного изменим класс RecognitionService ИЗ примера определения зачтенного дохода для сценария транзакции. Расширим его супертип слоя и добавим несколько шлюзов (Gateway), инкапсулирующих в себе логику приложения. Полученная схема классов показана на рис. 9.7. Класс RecognitionService станет РОЮ-реализацией службы из слоя служб, а его методы будут представлять две операции этого слоя.
Рис. 9.7. Схема связей POJO-класса RecognitionService
Методы класса RecognitionService представляют логику приложения в виде сценариев, обращающихся за логикой домена к классам объектов домена, описанным в примере для модели предметной области.
public class ApplicationService {
protected EmailGateway getEmailGateway() {
//return an instance of EmailGateway
}
protected IntegrationGateway getIntegrationGateway() {
//return an instance of IntegrationGateway
}
}
public interface EmailGateway {
void sendEmailMessage(String toAddress, String subject, String body);
}
public interface IntegrationGateway {
void publishRevenueRecognitionCalculation(Contract contract);
}
public class RecognitionService
extends ApplicationService {
public void calculateRevenueRecognitions(long contractNumber) {
Contract contract = Contract.readForUpdate(contractNumber);
contract.calculateRecognitions();
getEmailGateway().sendEmailMessage(
contract.getAdministratorEmailAddress(),
"RE: Contract #" + contractNumber,
contract + " has had revenue recognitions calculated.");
getIntegrationGateway().publishRevenueRecognitionCalculation(contract);
}
public Money recognizedRevenue(long contractNumber, Date asOf) {
return Contract.read(contractNumber).recognizedRevenue(asOf);
}
}
Для упрощения примера я опустил детали хранения данных. Достаточно сказать, что класс contract реализует статические методы, предназначенные для считывания из источника данных контрактов с заданными номерами. Один из этих методов (а именно readForUpdate) считывает контракты для последующего обновления, что позволяет преобразователю данных (Data Mapper), используемому в приложении, регистрировать считываемый объект или объекты, скажем, в единице работы (Unit of Work).
В этом примере были опущены и детали управления транзакциями. Метод calculateRevenueRecognitions() по своей сути является транзакционным, поскольку во время его выполнения обновляются постоянные объекты контракта путем добавления к ним значения зачтенного дохода, ставятся в очередь публикуемые сообщения и по электронной почте отправляются отчеты. Все эти отклики должны быть переданы посредством атомарных транзакций, потому что отправку отчета и публикацию сообщений следует выполнять только в том случае, если обновление объектов базы данных прошло успешно.
В среде J2EE EJB-контейнеры могут управлять распределенными транзакциями, реализуя службы приложений (и шлюзы) в виде компонентов сеанса, не меняющих своего состояния и использующих ресурсы транзакций. На рис. 9.8 изображена схема реализации класса RecognitionService, использующего локальные интерфейсы EJB2.0 и идиому "бизнес-интерфейса". В этой реализации также используется супертип слоя, обеспечивающий стандартную реализацию методов классов компонентов, необходимых EJB-контейнерам, в дополнение к методам, специфичным для данного приложения. Кроме того, если рассматривать интерфейсы EmailGateway И IntegrationGateway как бизнес-интерфейсы соответствующих компонентов сеанса, то управление распределенными транзакциями может выполняться путем присвоения методам calculateRevenue Recognitions, sendEmailMessage И publishRevenueRecognitionCalculation статуса транзакционных. В этом случае методы класса RecognitionService, рассмотренные в примере с реализацией объекта POJO, "плавно переходят" в класс RecognitionServiceBeanImpl без каких-либо изменений.
Важно отметить, что для координации транзакционных откликов на выполнение операций слоя служб используются и сценарии операции, и классы объектов домена. Рассмотренный ранее метод calculateRevenueRecognitions реализует сценарий логики приложения для выполнения откликов на варианты использования, однако обращается за логикой предметной области к классам домена. Здесь также продемонстрировано несколько приемов, представляющих собой попытку избежать дублирования логики в сценариях операций слоя служб. Обязанности по выполнению откликов выносятся в различные объекты (например, в шлюзы), которые могут быть повторно использованы посредством делегирования. Доступ к этим объектам удобно получать через супертип слоя.
Puc. 9.8. Схема связей EJB-класса RecognitionService
Некоторые могут не согласиться с моим выбором, предложив реализовать сценарий операции посредством типового решения наблюдатель (Observer), представленного в [20]. Конечно, это более эффектное решение, однако реализовать его в многопоточном, не имеющем состояний слое служб было бы весьма затруднительно. Как мне кажется, открытый код сценария операции гораздо понятнее и проще в использовании.
Можно было бы поспорить и о размещении логики приложения. Думаю, некоторые предпочли бы реализовать ее в методах объектов домена, таких, как Contract. calculateRevenueRecognitions(), ИЛИ вообще В слое источника данных, ЧТО позволило бы обойтись без отдельного слоя служб. Тем не менее подобное размещение логики приложения кажется мне весьма нежелательным, и вот почему. Во-первых, классы объектов домена, которые реализуют логику, специфичную для приложения (и зависят от шлюзов и других объектов, специфичных для приложения), менее подходят для повторного использования другими приложениями. Это должны быть модели частей предметной области, представляющих интерес для данного приложения, поэтому подобные объекты вовсе не обязаны описывать возможные отклики на все варианты использования приложения. Во-вторых, инкапсуляция логики приложения на более высоком уровне (каковым не является слой источника данных) облегчает изменение реализации этого слоя, возможно, посредством некоторых специальных инструментальных средств.
Слой служб как типовое решение для организации слоя логики корпоративного приложения сочетает в себе применение сценариев и классов объектов домена, используя преимущества тех и других. Реализация слоя служб допускает некоторые варианты, например использование интерфейсов доступа к домену или сценариев операций, объектов POJO или компонентов сеанса либо сочетание их обоих. Кроме того, слой служб может быть предназначен для локальных вызовов, удаленных вызовов или и тех и других. Вне зависимости от способа реализации, и это наиболее важно, данное типовое решение служит основой для инкапсулированной реализации бизнес-логики приложения и последовательных обращений к этой логике ее многочисленными клиентами.
