Содержание


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

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

Comments

Инверсия Управления (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), вызывающий компонент вводит свой зависимый объект (реализацию параметра-интерфейса метода) в метод зависимого компонента. Поскольку вызванный компонент ограничен методами, опредёленными в интерфейсе-параметре, то интерфейс может гарантировать, что объект, заданный как параметр, поддерживается логически связным. Аккуратно минимизация интерфейс до функционально необходимых методов уменьшает связь между компонентами.


Ресурсы для скачивания


Похожие темы


Комментарии

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

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