Использование инверсии управления (IoC) в сигнатурах методов

IoC - не только для компонентов

Шаблон Инверсии Управления (Inversion of Control (IoC)) обычно используется для компонентов. В этой статье описывается, как использовать шаблон для сигнатур методов, чтобы уменьшить связь между компонентами и повысить производительность. Консультант IBM Global Business Services Андре Фахат (André Fachat) использует два примера, что показать гибкость этого подхода.

Андре Фахат, IT-архитектор, IBM

Андре Фахат (Andre Fachat) является IT-архитектором сообщества Enterprise Java, входящего в состав IBM Global Business Services в Германии. Хотя он до сих пор наизусть помнит машинный язык своего первого компьютера, его области знаний сегодня включают архитектуры приложений Web и Enterprise Java, SOA и Web-сервисы, распределённые вычисления и моделирование. Он имеет степень доктора в теоретической физике, полученную в техническом университете в Хемнице, в Германии, где он занимался исследованием алгоритмов стохастической оптимизации на параллельных компьютерах. Он пришёл в IBM в 1999, и с того времени руководил различными проектами, включая рекомендации по производству. В IBM он также работал в таких областях, как построение архитектуры решений, разработка приложений и консалтинг.



10.04.2007

Инверсия Управления (IoC) и Ввод Зависимостей (DI) - шаблоны, которые привлекают много внимания (см. Ресурсы). В основном они используются в так называемых контейнерах IoC, которые вводят зависимости в компонент в форме других компонентов. Однако шаблоны не определяют конструкцию этих методов компонентов зависимости. При классическом построении объекты данных или объекты передачи данных в этих методах используются как параметры методов и возвращают значения, когда требуются комплексные объекты.

Эта статья показывает вам, что вы также можете использовать IoC на сигнатурах методов для того, чтобы разделить методы и объекты-значения. Вы сделаете это, заменив объекты-значения в сигнатурах методов на интерфейсы. Я покажу вам сценарии, при которых данный подход может быть полезен. Я часто использую этот шаблон и нахожу, что он помогает мне лучше разделять интересы между компонентами. А в период выполнения он сокращает усилия, затрачиваемые на создание объектов и копирование.

Объекты-значения как параметры метода

Об IoC было много написано, поэтому я опишу только его общий принцип: компонент использует "аутсорсинг" для конфигурирования, локализации и аспектов, связанных с жизненным циклом компонентов, которые он использует. Например, компонент доступа к данным вместо поиска источника данных JDBC (конфигурирование и локализация) и, возможно, поддержки пулинга соединений (жизненный цикл) "получает" откуда-нибудь соединение JDBC и просто использует его. В конфигурации с IoC, эти аспекты обычно обрабатываются контейнером IoC, который вводит зависимости в компонент, например, вызывая метод установки.

IoC сосредоточен на управлении жизненным циклом компонентов. В центре внимания этой статьи не компонент, а параметры методов тех операций, которые компонент предоставляет.

Посмотрите на схему зависимостей для типичной конфигурации компонента с двумя зависимостями и используемые параметры методов (см. Рисунок 1). Эти параметры методов задаются как объекты-значения, то есть, объекты без логики, которые содержат только значения данных.

Рисунок 1. Схема зависимостей для компонентов с объектами-значениями в роли параметров методов
Схема зависимостей с объектами-значениями

На рисунке 1 Component1 зависит от Component2 и Component3 (в терминах IoC) и вызывает method2 и method3, соответственно. В данный момент неважно, "знает" ли Component1 о Component2 или о Component3 напрямую или только через интерфейсы, которые реализуют Component2 и Component3. Однако, существенно, что в норме параметры методов - это объекты, а не интерфейсы.

В этой конфигурации, когда Component1 вызывает method2, он должен создавать экземпляр Value Object 2 и заполнять его значениями. Аналогично, если Component1 вызывает method3, то он должен создать экземпляр Value Object 3 и наполнить его значениями.

Теперь предположим, что Component1 надо вызвать и method2 и method3 с одинаковыми входными данными, чтобы получить разные данные на выходе. Например, Component1 мог бы быть компонентом для подготовки заказов, method2 - методом, определяющим время выполнения, а method3 - методом, определяющим цены. Обоим методам нужны одинаковые входные данные, а на выходе они предоставят разные данные.

В этом случае использование объектов-значений в роли параметров методов требует, чтобы Component1 создал объекты-значения для каждого вызова метода и активно копировал требуемые значения в эти объекты-значения. Также, от каждого из объектов-значений должен быть создан экземпляр, что уже не так дорого, как это было с ранними версиями Java™, но всё же требует определённых ресурсов. Все это снижает производительность. Последующие разделы покажут, как вы вместо этого можете оптимизировать производительность.


Интерфейсы для спасения

Цель заключается в том, чтобы избежать копирования значений между различными объектами-значениями. Вы можетет сделать это, описав параметры метода как интерфейсы. При таком подходе вызывающий компонент может использовать любой объект как параметр метода, если объект реализует интерфейс.

Рисунок 2 показывает новую схему зависимостей:

Рисунок 2. Схема зависимостей для компонентов с интерфейсами в качестве параметров методов
Схема зависимостей с интерфейсами

Зависимые методы описывают параметры метода как интерфейсы. Вызывающий метод создает экземпляр объекта, умышленно не называемый объектом-значением, который реализует эти интерфейсы и использует этот объект как параметр метода в обоих вызовах методов.

Следующий пример освещает некоторые преимущества этого подхода.


Пример: Цены и время выполнения

Вновь давайте предположим, что method1 определяет время выполнения для заказа, а method2 определяет цены. Простое описание этих компонентов и методов могло бы быть таким, как показано в Листинге 1:

Листинг 1. Образцы компонентов, использующие интерфейсы в качестве параметров метода
interface LeadtimeComponent {
   void getLeadtimes(List<LeadtimeItem> items) throws LeadtimeException;
}
interface LeadtimeItem {
   Long getArticleId();
   BigDecimal getQuantity();
   String getQuantityUnit();

   void setLeadtimeInDays(Integer leadtime);
}

interface PricingComponent {
   void getPrices(List<PriceItem> items) throws PricingException;
}
interface PriceItem {
   Long getArticleId();
   BigDecimal getQuantity();
   String getQuantityUnit();

   void setPrice(BigDecimal price);
   void setPriceUnit(String currency);
}

Заметьте, что оба интерфейса описывают одни и те же сигнатуры методов для извлечения данных изделия: getArticleId(), getQuantity() и getQuantityUnit(). Также обратите внимание, что методы компонентов не имеют возвращаемых значений; они модифицируют указанные объекты "на месте", вызывая методы настройки для объектов-параметров (то есть, интерфейсов), чтобы установить цены и времена выполнения.

Этот подход упрощает реализацию конвейерного шаблона (см. Ресурсы), где данные "передаются" от одного компонента к другому, и где одна ступень конвейера (компонент) использует данные, предоставленные предыдущими ступенями в конвейере. Рисунок 3 показывает диаграмму последовательностей, которая использует конвейерный шаблон:

Рисунок 3. Диаграмма последовательностей для подготовки заказов с использованием конвейерного шаблона
Диаграмма последовательностей для подготовки заказов

См. здесь полный рисунок.

В этом примере процесс подготовки заказа сначала считывает ID изделий, находящихся в потребительской тележке. Затем он добавляет дополнительную информацию из самой базы данных каталога, извлекает времена выполнения и цены (там, где ценам необходимо время выполнения) и сохраняет дополнительную информацию в тележке так, чтобы использовать её при финализации заказа. Если объекты, возвращённые после считывания данных потребительской тележки, реализуют интерфейсы, требуемые каталогу, а также методы для времени выполнения и цены, то копирование в этом процессе вовсе не требуется.


Фабричные методы

Вас мог удивить компонент OrderDB на Рисунке 3 и его метод readCart(). В самом деле, это - особый случай. В предыдущем примере все объекты, которые были модифицированы зависящим методом (таким как getPrices(...)), уже передавались как параметры методов. Это невозможно, когда компонент считывает данные из базы данных, поскольку в этом случае число элементов в потребительской тележке неизвестно до того, как оно будет прочитано.

Решением здесь является предоставление параметра метода с фабричным методом для чтения элементов, как показано в Листинге 2:

Листинг 2: Использование фабричных методов в параметре метода
interface OrderDBComponent {
   void readCart(Cart cart) throws OrderDBException;
}
interface Cart {
   Long getCartId();

   CartItem newItem();
   void addItem(CartItem item);
}
interface CartItem {
   void setArticleId(Long articleId);
   ...
}

При таком объявлении компонент OrderDB прочитывает элементы из базы данных и для каждого элемента, который он прочитывает, получает новый объект элемента (CartItem) от объекта Cart при помощи метода newItem(). После заполнения значениями, прочитанными из базы данных, CartItem добавляется к тележке с помощью метода addItem(). Заметьте, что добавление элемента к тележке только после его заполнения значениями сохраняет тележку корректной в любое время.


Несовпадение импеданса

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

Однако даже если несовместимости существуют, не все потеряно! Объекты-адаптеры могут трансформировать объект так, чтобы он реализовывал требуемый интерфейс, не копируя все вокруг данные. Хотя при таком подходе вы должны создать экземпляр объекта-адаптера, он всё же избегает копирования значений данных.


Другой пример

Вот другой пример, который показывает гибкость данного подхода. Я написал редактор для определённой объектной модели, но хотел хранить реализацию модели отдельно от реализации редактора. Поэтому я позволил редактору определить интерфейсы для модели, которую он может редактировать. Затем фактическая реализация модели реализовала интерфейс в Листинге 3:

Листинг 3. Пример интерфейса редактора модели
interface ModelEditor {
   void edit(Model model);
}
interface Model {
   ModelElement newElement();
   ModelElement addElement(ModelElement element);
}

В этом (весьма) упрощённом определении вы можете видеть, что метод addElement() в модели не только принимает ModelElement как параметр, но и возвращает экземпляр ModelElement. Возвращённый ModelElement является элементом модели, который заменяется вновь добавленным элементом модели или NULL, если ни один из элементов не заменяется. Возвращённое значение затем сохраняется в команде undo (отмена) так, что модель может быть легко восстановлена путём повторного вызова addElement(). Также, метод addElement() реализует проверки связности модели и отклоняет недопустимые изменения.


Заключение

Эта статья показала особую форму IoC, которая применяется к параметрам методов компонентов, а не к компонентам. Использование интерфейсов в роли параметров методов является (в терминах IoC) формой контекстной IoC, применённой к зависящим от вызывающего. Так же, как зависимый компонент (такой как PriceComponent) вводится в вызывающий компонент (такой как OrderPrepareComponent), вызывающий компонент вводит свой зависимый объект (реализацию параметра-интерфейса метода) в метод зависимого компонента. Поскольку вызванный компонент ограничен методами, опредёленными в интерфейсе-параметре, то интерфейс может гарантировать, что объект, заданный как параметр, поддерживается логически связным. Аккуратно минимизация интерфейс до функционально необходимых методов уменьшает связь между компонентами.

Ресурсы

Научиться

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


  • Bluemix

    Узнайте больше информации о платформе IBM Bluemix, создавайте приложения, используя готовые решения!

  • Библиотека документов

    Более трех тысяч статей, обзоров, руководств и других полезных материалов.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=208471
ArticleTitle=Использование инверсии управления (IoC) в сигнатурах методов
publish-date=04102007