- •Оглавление
- •Предисловие
- •Введение
- •Часть 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)
- •Список типовых решений
- •Шпаргалка
- •Как управлять сложным потоком функций приложения?
- •Как взаимодействовать с базой данных?
- •Как избежать загрузки в оперативную память всего содержимого базы данных?
- •Как сохранить структуры наследования в реляционной базе данных?
Глава 18 Базовые типовые решения Шлюз (Gateway)
Объект, инкапсулирующий доступ к внешней системе или источнику данных
Программное обеспечение редко функционирует само по себе. Даже самые типичные объектно-ориентированные системы должны взаимодействовать со структурами, не являющимися объектами, например с таблицами реляционных баз данных, транзакциями CICS и документами XML
В большинстве случаев для доступа к внешним источникам применяются интерфейсы API. К сожалению, они довольно сложны, так как учитывают характер источника. Каждый, кто работает с источником, должен понимать и его интерфейс — JDBC или SQL для реляционных баз данных, W3C или JDOM для XML и т.п. Это не только затрудняет понимание программного обеспечения, но и значительно усложняет потенциальную замену источника данных с реляционной СУБД документом XML или наоборот.
Решение данной проблемы настолько просто и очевидно, что его вряд ли стоит озвучивать. Весь специализированный код API помещается в класс-шлюз, интерфейс которого не отличается от интерфейса обычного объекта. После этого, чтобы получить доступ к удаленному источнику, объекты приложения будут обращаться к шлюзу, который преобразует простые вызовы методов в вызовы специализированного API.
Принцип действия
Концепция типового решения шлюз очень проста. Возьмем внешний источник данных. Какие действия должно совершать приложение по отношению к этому источнику? Создайте простой API, наполните его всеми необходимыми методами и воспользуйтесь шлюзом, чтобы преобразовать вызовы его методов в обращения к внешнему источнику.
Одно из главных назначений шлюза — обеспечить основу для реализации фиктивных служб (Service Stub). Иногда для более удобного применения фиктивной службы в структуру шлюза приходится вносить дополнительные изменения. Не бойтесь это делать: удачное размещение фиктивных служб способно значительно облегчить тестирование, а значит, и написание системы.
Не стоит излишне усложнять шлюз — оставьте его настолько простым, насколько это возможно. Реализуя данное типовое решение, сосредоточьте усилия на адаптации внешней службы к особенностям приложения и обеспечении хорошей основы для поддержки фиктивных служб. Шлюз должен обрабатывать указанные задания и одновременно обладать как можно меньшей функциональностью. Вся более сложная логика должна содержаться в клиентах шлюза.
Зачастую для создания шлюзов применяют системы автоматической генерации кода. Описав структуру внешнего источника, вы можете сгенерировать для него соответствующий шлюз. Чтобы сконструировать оболочку для таблицы реляционной базы данных, можно воспользоваться реляционными метаданными, а чтобы сгенерировать шлюз для доступа к документу XML — схемами XML или шаблонами DTD. Разумеется, полученные шлюзы не будут отличаться особым изяществом, однако выполнять свои функции будут безотказно. Все остальные решения обычно оказываются более сложными.
Иногда реализацию шлюза рассматривают в терминах двух объектов — прикладной части (back end) и интерфейсной части (front end). Прикладная часть шлюза представляет собой минимальную оболочку функций доступа к внешнему источнику и вообще не упрощает использование API. В свою очередь, интерфейсная часть преобразует неудобный API в класс, оптимально приспособленный для применения конкретным приложением. Данную стратегию хорошо применять в случаях, когда построение оболочки для доступа к внешней службе и приспособление этой оболочки к нуждам приложения довольно сложны, а потому требуют обработки каждого из этих заданий в отдельных классах. Напротив, если построение оболочки для доступа к внешней службе не отличается особой сложностью, все перечисленные действия может выполнять и один класс.
Назначение
Типовое решение шлюз рекомендуется применять во всех случаях, когда интерфейс доступа к внешнему источнику слишком неудобен. Наличие шлюза позволяет сконцентрировать все неудобные обращения в одном месте вместо того, чтобы распространять их по всей системе. Применение шлюза не несет побочных эффектов, а код приложения становится более читабельным и понятным.
Как уже отмечалось, шлюз значительно облегчает тестирование, поскольку представляет собой потенциальную точку внедрения фиктивных служб. Даже если с интерфейсом внешней системы все в порядке, использование шлюза позволяет сделать первый шаг в направлении реализации фиктивной службы.
Не менее важным преимуществом шлюза является возможность легко переключаться между источниками данных. Дня перехода к другому источнику достаточно просто изменить класс шлюза — оставшейся части системы это не коснется. Таким образом, шлюз представляет собой простое и мощное средство инкапсуляции изменений. Иногда потребность в наличии подобной степени гибкости, а следовательно, и в реализации шлюза кажется спорной. Тем не менее, даже если вы не собираетесь менять источник данных в обозримом будущем, вы несомненно выиграете от простоты написания и тестирования кода, которую обеспечивает данное типовое решение.
В качестве альтернативного варианта изолирования приложений от внешних источников может применяться преобразователь (Mapper). Однако он имеет более сложную структуру, нежели шлюз, а потому я предпочитаю использовать именно последний.
Некоторое время меня одолевали сомнения относительно того, стоит ли выделять данную схему в самостоятельное типовое решение, противоположное существующим типовым решениям интерфейс (Facade), адаптер (Adapter) и медиатор (Mediator) [20]. В конце концов я решил описать шлюз как отдельное типовое решение, поскольку, на мой взгляд, оно обладает рядом существенных отличий.
Типовое решение интерфейс также упрощает работу с интерфейсом API, однако оно создается самим разработчиком внешней службы и предназначено для общего употребления. В свою очередь, шлюз разрабатывается клиентом для использования конкретным приложением. Кроме того, интерфейс доступа, предоставляемый объектом интерфейса, всегда отличается от интерфейса стоящего за ним объекта, в то время как интерфейс шлюза может представлять собой точную копию инкапсулируемого интерфейса (для тестирования или замены источника данных суррогатным объектом).
Типовое решение адаптер изменяет интерфейс некоторого объекта для достижения соответствия с интерфейсом другого объекта. В отличие от него, интерфейс шлюза не нужно подстраивать под какой-либо существующий интерфейс. Вообще говоря, типовое решение адаптер можно применить, чтобы отобразить реализацию некоторого объекта на интерфейс шлюза. В этом случае объект адаптера будет представлять собой составную часть реализации шлюза.
Типовое решение медиатор разделяет множество объектов так, чтобы они не знали о существовании друг друга, но были осведомлены о наличии объекта медиатора. Шлюз разделяет только два объекта, причем источник данных не знает о существовании шлюза.
Пример: создание шлюза к службе отправки сообщений (Java)
Обсуждая концепцию шлюза с моим коллегой Майком Реттигом (Mike Rettig), я узнал, как он использовал данное типовое решение для обработки доступа к внешним интерфейсам в приложениях EAI (Enterprise Application Integration — интеграция корпоративных систем). Мы решили, что опыт Майка может послужить прекрасной основой для рассмотрения примера использования шлюза.
Как всегда, наш пример отличается невероятно простым положением дел. Мы создадим шлюз к интерфейсу, который отсылает сообщение посредством службы отправки сообщений. Сам интерфейс представляет собой единственный метод:
int send(String messageType, Object[] args);
Первый аргумент данного метода — это строка, указывающая на тип сообщения, а второй — перечень аргументов самого сообщения. Система отправки сообщений позволяет отсылать сообщения любого типа, поэтому ей нужен подобный универсальный интерфейс. При настройке системы необходимо указать тип отсылаемых сообщений, а также количество и типы аргументов этих сообщений. Таким образом, можно настроить систему для отправки подтверждающих сообщений, указав в качестве типа сообщения слово "CNFRM", а также задав аргумент orderID ("идентификатор заказа") типа String, аргумент amount ("количество товара") типа Integer и аргумент symbol ("код товара") типа string. Система отправки сообщений проверяет типы передаваемых аргументов и генерирует ошибку при попытке отослать сообщение неправильного типа или сообщение правильного типа с неправильными типами аргументов.
Описанная схема обеспечивает высокую (и, кстати, весьма необходимую) степень гибкости, однако универсальный интерфейс крайне неудобен в использовании. Из определения универсального интерфейса не ясно, какие типы сообщений допускается отсылать, а также сколько и каких аргументов должно быть в каждом сообщении. Вместо этого нам нужен интерфейс с методами наподобие следующего:
public void sendConfirmation(String orderID, int amount, String symbol);
В этом случае для отправки подтверждающего сообщения объектом домена необходимо поступить так, как показано ниже.
class Order...
public void confirm() {
if (isValid()) Environment.getMessageGateway().sendConfirmation(id, amount, symbol);
}
В приведенном фрагменте кода имя метода указывает на тип отсылаемого сообщения, а для всех аргументов метода sendConfirmation явно заданы имена и типы. Разумеется, данный метод вызывать намного легче, чем метод универсального интерфейса. В этом и состоит смысл применения шлюза — обеспечить более удобный интерфейс для доступа к внешней системе. Правда, при каждом добавлении или изменении типа отсылаемых сообщений понадобится внести изменения и в класс шлюза, однако при отсутствии последнего изменения коснулись бы вызывающего кода, поэтому общая сумма изменений остается прежней. В этом плане применение шлюза более предпочтительно, так как позволяет определить клиентов и перехватить потенциальные ошибки еще на этапе компиляции.
Существует и другая проблема. При обнаружении ошибки универсальный интерфейс возвращает ее код. Нуль означает отсутствие ошибок, а любое число, отличное от нуля, указывает на определенный тип ошибки (при этом разным ошибкам соответствуют разные числа). Данная схема уведомления об ошибках вполне естественна для языка С, однако в Java все обстоит иначе. Здесь для обработки ошибок применяются исключения, поэтому методы шлюза должны генерировать исключения, а не возвращать коды ошибок.
Вместо того чтобы рассматривать все возможные типы ошибок, сконцентрируем внимание только на двух из них: отправка сообщения неизвестного типа, а также отправка сообщения, один из аргументов которого равен null. Возвращаемые коды ошибок определены в интерфейсе системы отправки сообщений.
public static final int NULL_PARAMETER = -1;
public static final int UNKNOWN_MESSAGE_TYPE = -2;
public static final int SUCCESS = 0;
Природа первой и второй ошибок неодинакова. Ошибка, связанная с отправкой сообщения неизвестного типа, указывает на неполадки в классе шлюза; клиент не может привести к появлению подобной ошибки, поскольку вызывает только явные методы с заранее заданными типами сообщений. Тем не менее клиент может передать в качестве аргумента значение null и привести к появлению второй ошибки, а именно null_parameter. Данная ошибка не нуждается в собственном исключении, так как является ошибкой программирования, — ситуации, для которых не пишут специальных обработчиков. Вообще говоря, следить за появлением значений null мог бы и сам шлюз, но повторять действия системы отправки сообщений не имеет смысла.
Исходя из этих доводов, реализуемый нами шлюз должен преобразовывать вызовы методов явного интерфейса в таковые универсального интерфейса, а коды ошибок — в соответствующие исключения.
class MessageGateway...
protected static final String CONFIRM = "CNFRM";
private MessageSender sender;
public void sendConfirmation(String orderID, int amount, String symbol) {
Object[] args = new Object[]{orderID, new Integer(amount), symbol};
send(CONFIRM, args);
}
private void send(String msg, Object[] args) {
int returnCode = doSend(msg, args);
if (returnCode == MessageSender.NULL_PARAMETER)
throw new NullPointerException("Null Parameter bassed for msg type: " + msg);
if (returnCode != MessageSender.SUCCESS)
throw new IllegalStateException(
"Unexpected error from messaging system #:" + returnCode);
}
protected int doSend(String msg, Object[] args) {
Assert.notNull(sender);
return sender.send(msg, args);
}
Пока назначение метода doSend весьма туманно, не так ли? Между тем не будем забывать еще об одной не менее важной функции шлюза — облегчении тестирования. При наличии шлюза тестирование объектов можно проводить без обращения к реальной службе отправки сообщений. Для этого нужно создать фиктивную службу. В данном примере мы определим класс MessageGatewayStub, который наследует класс MessageGateway и переопределяет его метод doSend.
class MessageGatewayStub...
protected int doSend(String messageType, Object[] args) {
int returnCode = isMessageValid(messageType, args);
if (returnCode == MessageSender.SUCCESS) {
messagesSent++;
}
return returnCode;
}
private int isMessageValid(String messageType, Object[] args) {
if (shouldFailAllMessages) return -999;
if (!legalMessageTypes().contains(messageType))
return MessageSender.UNKNOWN_MESSAGE_TYPE;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg == null) {
return MessageSender.NULL_PARAMETER;
}
}
return MessageSender.SUCCESS;
}
public static List legalMessageTypes() {
List result = new ArrayList();
result.add(CONFIRM);
return result;
}
private boolean shouldFailAllMessages = false;
public void failAllMessages() {
shouldFailAllMessages = true;
}
public int getNumberOfMessagesSent() {
return messagesSent;
}
Подсчет количества отправленных сообщений нужен для того, чтобы проверить, правильно ли работает шлюз. Для выполнения проверки можно воспользоваться приведенными ниже тестами.
class GatewayTester...
public void testSendNullArg() {
try {
gate().sendConfirmation(null, 5, "US");
fail("Didn't detect null argument");
} catch (NullPointerException expected) {
}
assertEquals(0, gate().getNumberOfMessagesSent());
}
private MessageGatewayStub gate() {
return (MessageGatewayStub) Environment.getMessageGateway();
}
protected void setUp() throws Exception {
Environment.testInit();
}
Как правило, шлюз располагают в таком месте, чтобы другом объектам было легко его найти. В этом примере я воспользовался статическим интерфейсом окружения. Переключение между реальной и фиктивной службами можно осуществить во время настройки системы посредством типового решения дополнительный модуль (Plugin). Кроме того, для активизации фиктивной службы можно воспользоваться методом setup класса GatewayTester, выполняющим инициализацию окружения.
В данном примере для замены реальной службы отправки сообщений я воспользовался классом, производным от класса шлюза. В качестве возможной альтернативы можно рассмотреть создание класса, производного от самого класса службы, или же новую реализацию последней. При выполнении тестирования шлюз подсоединяется к фиктивной службе отправки сообщений; данная схема срабатывает тогда, когда реализовать службу заново не слишком сложно. Следует также отметить, что вместо замены службы можно осуществить замену шлюза. Более того, некоторые разработчики умудряются заменять и шлюз и службу, используя фиктивный шлюз для тестирования клиентов шлюза, а фиктивную службу — для тестирования самого шлюза.
