Инверсия Управления (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), вызывающий компонент вводит свой зависимый объект (реализацию параметра-интерфейса метода) в метод зависимого компонента. Поскольку вызванный компонент ограничен методами, опредёленными в интерфейсе-параметре, то интерфейс может гарантировать, что объект, заданный как параметр, поддерживается логически связным. Аккуратно минимизация интерфейс до функционально необходимых методов уменьшает связь между компонентами.
Научиться
- Оригинал статьи: Use Inversion of Control in method signatures.
-
"Контейнеры Инверсии Управления и шаблон Ввода Зависимостей (Inversion of Control Containers and the Dependency Injection pattern)" (Мартин Фаулер, martinfowler.com, январь 2004 г.): Фаулер разбирает, как работает шаблон IoC и сопоставляет его с альтернативой Service Locator alternative.
-
Apache Excalibur: Проект Excalibur от Apache Software Foundation основан на IoC.
-
"Секреты легковесного успеха разработок, Часть 4: Сравнение легковесных контейнеров (Secrets of lightweight development success, Part 4: A comparison of lightweight containers)" (Брюс Тейт, developerWorks, август 2005 г.): Узнайте о трех легковесных контейнерах, которые могут резко ослабить связь между основными компонентами вашей системы.
-
Контейнеры Инверсии Управления с открытым кодом: Просмотрите этот комплексный список контейнеров IoC с открытым кодом.
-
Объект-значение: Прочитайте больше об объектах-значениях.
-
Объект передачи данных: Каталог шаблонов архитектур приложений Мартина Фаулера описывает объекты передачи данных.
-
Шаблон разработки PipelineProcessing: Читайте о конвейерном шаблоне.
-
Раздел Java-технологий: Сотни статей обо всех аспектах Java-программирования.
Обсудить
- Примите участие в обсуждении материала на форуме.
-
Посещайте блоги developerWorks
и вступайте в сообщество developerWorks.
Андре Фахат (Andre Fachat) является IT-архитектором сообщества Enterprise Java, входящего в состав IBM Global Business Services в Германии. Хотя он до сих пор наизусть помнит машинный язык своего первого компьютера, его области знаний сегодня включают архитектуры приложений Web и Enterprise Java, SOA и Web-сервисы, распределённые вычисления и моделирование. Он имеет степень доктора в теоретической физике, полученную в техническом университете в Хемнице, в Германии, где он занимался исследованием алгоритмов стохастической оптимизации на параллельных компьютерах. Он пришёл в IBM в 1999, и с того времени руководил различными проектами, включая рекомендации по производству. В IBM он также работал в таких областях, как построение архитектуры решений, разработка приложений и консалтинг.