Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Shablony_korporativnykh_prilozheniy_Fauler_M.docx
Скачиваний:
1
Добавлен:
01.07.2025
Размер:
3.82 Mб
Скачать

Глава 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, выполняющим инициализацию окружения.

В данном примере для замены реальной службы отправки сообщений я воспользовал­ся классом, производным от класса шлюза. В качестве возможной альтернативы можно рассмотреть создание класса, производного от самого класса службы, или же новую реа­лизацию последней. При выполнении тестирования шлюз подсоединяется к фиктивной службе отправки сообщений; данная схема срабатывает тогда, когда реализовать службу заново не слишком сложно. Следует также отметить, что вместо замены службы можно осуществить замену шлюза. Более того, некоторые разработчики умудряются заменять и шлюз и службу, используя фиктивный шлюз для тестирования клиентов шлюза, а фик­тивную службу — для тестирования самого шлюза.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]