суббота, 24 июля 2010 г.

Продолжение перевода статьи М. Фаулера "Inversion of Control Containers and the Dependency Injection pattern"

Продолжение перевода статьи Мартина Фаулера "Inversion of Control Containers and the Dependency Injection pattern" оригинал которой доступен здесь. Первая часть доступна здесь.




Использование Service Locator

Главное преимущество Dependency Injector в том, что убирается зависимость класса MovieLiester от конкретной реализации средства поиска MovieFinder. Это позволяет мне дать список фильмов друзьям, которые могут использовать свою реализацию в зависимости от своего окружения. Инъекции - не единственный способ разорвать эту зависимость, другой способ - это использовать service locator.

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

Рисунок 3. Зависимости для сервис локатора.
В этом случае, я буду использовать локатор сервисов, как одиночку Registry. Наш список затем может использовать его, что бы получить средство поиска.
class MovieLister...
    MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
    public static MovieFinder movieFinder() {
        return soleInstance.movieFinder;
    }
    private static ServiceLocator soleInstance;
    private MovieFinder movieFinder;
Как и в случае с использованием внедрений, мы должны настроить локатор сервисов. Здесь я делаю это прямо в коде, но не так уж и трудно использовать механизм, который будет считывать соответствующие данные из конфигурационного файла.
class Tester...
    private void configure() {
        ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt")));
    }
class ServiceLocator...
    public static void load(ServiceLocator arg) {
        soleInstance = arg;
    }

    public ServiceLocator(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
Вот, тестовый код:
class Tester...
    public void testSimple() {
        configure();
        MovieLister lister = new MovieLister();
        Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
        assertEquals("Once Upon a Time in the West", movies[0].getTitle());
    }
Я часто слышу жалобы, что эти локаторы сервисов - плохая штука, потому что они не тестируемые, т.к. невозможно подменить реализацию. Конечно, вы можете спроектировать их так ужасно что бы попасть в эту передрягу, а можете и не попасть. Для этого экземпляр локатора сервисов должен быть простым контейнером данных. Тогда я легко могу создать локатор, с тестовой реализацией для моих сервисов.

Для более изощренных локаторов я могу создать подкласс локатора сервисов  и поместить его в реестр. Я могу изменить статические методы вызвав доступные методы на целевом экземпляре напрямую. Я могу предоставить набор конкретных локаторов  используя набор конкретных хранилищ. Все это может быть сделано без изменения клиентов локатора сервисов.

Задуматься об этой проблеме стоит тогда, когда локатор сервисов это реестр,  но не одиночка. Шаблон одиночка предлагает простой способ реализации реестра, хотя это решение легко изменить.

Использование принципа отделения интерфейса для локатора

Одна из проблем, с подходом, описанным ранее, это то, что MovieLister зависит от целого класса сервис локатора, несмотря на то что использует только один сервис. Мы можем избавиться от этого, используя принцип отделения интерфейса. Таким образом, вместо полного использования интерфейса сервис локатора, наш список может объявить только ту часть, в которой он нуждается.
public interface MovieFinderLocator {
    public MovieFinder movieFinder();
Затем, локатору необходимо реализовать этот интерфейс, что бы обеспечить доступ к средству поиска.

MovieFinderLocator locator = ServiceLocator.locator();
    MovieFinder finder = locator.movieFinder();
    public static ServiceLocator locator() {
        return soleInstance;
    }
    public MovieFinder movieFinder() {
        return movieFinder;
    }
    private static ServiceLocator soleInstance;
    private MovieFinder movieFinder;
Вы наверно заметили, что поскольку мы используем интерфейс, мы больше не можем обращаться к статическим методам для доступа к сервису. Мы должны использовать класс, для получения экземпляра локатора, и только потом, можем использовать его, для получения всего необходимого.

Динамический Service Locator
Приведенный выше пример был статический, так как класс локатора сервисов имеет статические методы для всех необходимых нам служб. Это не единственный способ, вы также можете сделать динамический локатор сервисов, который позволяет размещать любой сервис в себе прямо во время выполнения.

В этом случае, локатор сервисов использует карту вместо статических  полей для каждого сервиса и предоставляет общие методы для получения и загрузки сервисов.
class ServiceLocator...
    private static ServiceLocator soleInstance;
    public static void load(ServiceLocator arg) {
        soleInstance = arg;
    }
    private Map services = new HashMap();
    public static Object getService(String key){
        return soleInstance.services.get(key);
    }
    public void loadService (String key, Object service) {
        services.put(key, service);
    }
Конфигурация включает загрузку сервиса с соответствующим ключем.

class Tester...
    private void configure() {
        ServiceLocator locator = new ServiceLocator();
        locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
        ServiceLocator.load(locator);
    }
Я использую сервис, используя ту же самую ключевую строку.

class MovieLister...
    MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
В целом, мне не нравится такой подход. Хотя, это безусловно гибко, но не очень явно. Единственный способ добраться до сервиса через текстовые ключи. Я предпочитаю явные методы, так как их проще найти, глядя на объявленный интерфейс.

Использование и локатора и инжектирования с Avalon
Dependency Injection и Service Locator не являются взаимозаменяемыми. Хороший пример использования обоих представлен в Avalon фреймворке. Avalon использует локатор сервисов, в то же время он использует инжектирование, что бы сообщить компонентам где найти локатор.

Berin Loritsch прислал мне простой пример с использованием такого подхода.
public class MyMovieLister implements MovieLister, Serviceable {
    private MovieFinder finder;

    public void service( ServiceManager manager ) throws ServiceException {
        finder = (MovieFinder)manager.lookup("finder");
    } 
Метод service - пример интерфейса инжектора, позволяющего контейнеру внедрить в MyMovieLister менеджера сервисов. ServiceManager - пример ServiceLocator'а. В этом примере, список не хранит менеджера у себя, вместо этого он непосредственно использует его для нахождения средства поиска, который и сохраняет у себя.


Решение, какой вариант использовать
До сих пор я сосредотачивался на обзоре, как мне видятся эти паттерны и  их варианты. Теперь я могу начать говорить об их преимуществах и недостатках, что бы помочь выяснить какие использовать и когда.

Service Locator vs Dependency Injection
Основной выбор между Service Locator и Dependency Injection. Первый момент заключается в том, что в обоих случаях код приложения не зависит от конкретных реализаций интерфейса сервиса. Основное различие этих двух решений, в том, как эта реализация предоставляется прикладному классу. В случае локатора сервиса, прикладной класс просит реализацию явно. В случае в внедрением, нет никакого явного запроса, сервис "впрыскивается" в прикладной класс - отсюда инверсия управления.

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

Ключевое различие в том, что с Service Locator каждый пользователь сервиса зависит от локатора. Локатор может скрывать зависимость от других реализаций, но ваша реализация должна знать о локаторе. Таким образом решение между локатором и внедрением сводится к тому, где именно эта зависимость становится проблемой.

Использование внедрения зависимости может облегчить видимость, каковы составляющие зависимости. С внедрением зависимости вы можете просто взглянуть на механизм внедрения, такой как конструктор и увидеть зависимости. С сервис локатором, вы должны отыскать источник вызова локатора. Современные среды разработки могут облегчить нахождение, но это все равно не так просто, как взглянуть на конструктор или set-методы.

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

Другой вариант, если мой список фильмов является компонентом для разработчиков других приложений. В этом случае, я ничего не знаю об API локатора сервисов, который мои клиенты собираются использовать. У каждого клиента могли бы быть свои собственные, не совместимые локаторы сервисов. Я могу обойти часть ограничений посредством отдельного интерфейса. Каждый клиент для своего локатора может написать адаптер, согласно моему интерфейсу, но в любом случае, я нуждаюсь в доступности первого локатора, для поиска моего конкретного интерфейса. И как только появляется адаптер, тогда простота прямого доступа к локатору начинает пробуксовывать.

Так как с инжектором, у вас нет зависимости от компонента-инжектора, то компонент не может получить дополнительные услуги от инжектора, так как он уже был настроен.

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

Конечно, проблему тестирования усугубляют среды, которые достаточно навязчивые, например Java EJB фреймворк. На мой взгляд, подобные фреймворки должны сводить к минимуму свое влияние на прикладной код, и в частности не должны делать вещи, которые приводят к замедлению цикла edit-execute. Использование расширений для замены тяжеловесных компонентов должно способствовать этому процессу, который является жизненно важным для таких практик как разработка через тестирование (TDD).


Так что главная проблема в людях, которые пишут код, который впоследствии используется в приложениях им не подконтрольных. В этом случае даже минимальное предположение о Service Locator это проблема.

Сравнение внедрение посредством конструктора и set-методов
Для объединения сервисов, у вас всегда должно быть какое-либо соглашение. Преимущество инжектирования прежде всего в том, что оно нуждается в простом соглашении, по-крайней мере для инъекции через конструктор и set-методы. Вы не должны делать ничего особенного с вашим компонентом и при этом его достаточно просто настроить.

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

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

Я довольно давно работаю с объектами  и стараюсь по возможности создавать их полностью через конструктор. Этот совет приводит нас к Кент Беку Smalltalk Best Practice Patterns: Constructor Method and Constructor Parameter Method. Конструкторы с параметрами позволяют вам создавать валидные объекты очевидным способом. Если существует несколько способов создания правильного объекта, сделайте множество конструкторов с различной комбинацией параметров.

Еще одно преимущество инициализации объекта в конструкторе это прозрачно проинициализировать скрытые неизменяемые поля без предоставления set-методов. Я думаю это очень важно - если никто не должен изменять эти поля, то это действительно должно быть так. Если вы используете set-методоты то это может привести к беде. (На самом деле, в таких ситуациях, я предпочитаю избежать проблем и инициализировать поля методами подобно initFoo, что бы подчеркнуть что это поле устанавливается только при инициализации объекта.)

Но в любой ситуации есть исключения. Если у вас много параметров в конструкторе это может выглядить пошло, особенно в языках без ключевых параметров. С другой стороны, часто длинные конструкторы свидетельствуют о перегруженности объекта и должны быть разбиты, хотя бывает что это действительно оправдано.

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

Конструкторы так же страдают, если у вас имеются простые параметры, типа строк. С использованием set-методов, вы можете каждому методу дать понятное название, поясняющее что он делает. С конструкторами вы только полагаетесь на позицию, за которой труднее следить.

Если у вас несколько конструкторов и есть наследование, то все может стать довольно запутанным. Для правильно порядка вы вызываете конструкторы суперклассов попутно добавляя новые параметры. Это может привести к еще большему нагромождению конструкторов.

Несмотря на недостатки я предпочитаю внедрять зависимости через конструктор, но будьте готовы перейти к инъекциям через set-методы, как только вышеизложенные проблемы начнут появляться.

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

Настройка в коде или в конфигурационном файле
Отдельный, но часто связанный вопрос заключается в том, использовать ли конфигурационные файлы или код на основе API для связывания сервисов. Для большинства приложений, которые могут быть развернуты где угодно, применение отдельного конфигурационного файла более предпочтительно. Практически всегда это XML-файл, что естественно. Хотя существуют случаи когда для связывания сервисов проще использовать программный код. Один из таких случаев, когда у вас простое приложение, которое не имеет множества различных вариантов развертывания. В этом случае код может быть понятнее, чем отдельный XML файл.

Противоположный вариант, когда связывание довольно сложное, включающее условные шаги. Как только начинаются ограничения связанные с языком XML, то лучше использовать реальный язык, с полным набором синтаксиса. Затем вы можете написать класс сборщик, который делает связывание. Если у вас есть различные сценарии сборки вы можете предоставить несколько классов сборщиков и использовать простой конфигурационный файл для навигации между ними.

Я часто замечаю, что люди рвутся определять конфигурационные файлы. Хотя программный язык делает конфигурирование более мощным и гибким. Современные языки могут компилировать небольшие сборщики, которые могут использоваться для сборки дополнений для больших систем. Если компиляция затратна, тогда есть скриптовые языки, которые так же хороши.

Часто говорят, что конфигурационные файлы не должны использовать языки программирования поскольку они должны быть адаптированы к не программистам. Но как часто такое случается? Вы действительно думаете, что далекие от программирования люди будут изменять server-side приложения? Конфигурации без языковых конструкций работают хорошо пока они просты. Иначе следует серьезно задуматься о применении соответствующего языка программирования.

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


Мой совет, всегда предоставлять способ сделать всю конфигурацию проще, используя программный интерфейс и затем обращаться к отдельной конфигурации, как к дополнительной функции. Вы можете легко встроить обработку файла конфигурации используя программный интерфейс. Если вы пишите компонент, то оставьте право выбора пользователю использовать программный интерфейс, формат вашего конфигурационного файла или написать свой собственный вариант формата конфигурационного файла и подсунуть используя программный интерфейс.

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

Это разделение особенно полезно, когда вы используете внешние элементы, такие как компоненты и сервисы. Первый вопрос - желаете ли в отложить выбор класса реализации в зависимости от конкретного развертывания? Если да, то необходима некая реализация дополнений. Этот конфигурационный механизм может в последствии настроить локатор сервисов или использовать внедрение для настройки объектов напрямую.


Некоторые дополнительные вопросы
В этой статье, я сосредоточился на основных вопросах по настройке сервисов используя Dependency Injection и Service Locator. Есть еще несколько тем, которые так же заслуживают внимания, но я не успел еще разобраться в этих вопросах. В частности, есть проблема поведения жизненного цикла. Некоторые компоненты имеют различный событийный жизненный цикл: остановка и запуск, например. Другая проблема, рост интереса к аспектно-ориентированным идеям с применением этих контейнеров. Хоть я и не рассмотрел этот материал в статье, я надеюсь написать больше об этом, либо расширив данную статью, либо написав другую.

Вы можете узнать больше об этих идеях взглянув на веб-сайты, посвященные этим легким контейнерам. Серфинг, начиная с picocontainer и spring, приведет вас к более подробным обсуждениям и даст направление к некоторым другим вопросам.

Заключительные мысли
В текущем прорыве легких контейнеров лежит общий паттерн - принцип инверсии зависимостей. Dependency Injection полезная альтернатива Service Locator. При проектировании прикладных классов, они оба примерно эквивалентны, но я думаю Service Locator имеет небольшое преимущество, за счет более прозрачного поведения. Однако, если вы строите классы, которые будут использоваться в нескольких приложениях, тогда Dependency Injection лучший выбор.

Если вы используете Dependency Injection, то есть несколько стилей, между которыми можно выбирать.  Я бы посоветовал использовать внедрения через конструктор, и если вы столкнетесь с проблемами, то использовать set-методы. Если вы собираетесь выбрать контейнер, то присмотритесь к тем, которые поддерживают оба подхода.

Выбор между Dependency Injection и Service Locator менее важен, чем принцип отделения настройки сервиса от его использования в рамках приложения.


Благодарности
Выражаю искрении благодарности всем тем людям, которые помогли мне с этой статьей. Rod Johnson, Paul Hammant, Joe Walnes, Aslak Hellesøy, Jon Tirsén и Bill Caputo помогли мне добраться до сути этих понятий и прокомментировали ранние черновики статьи. Berin Loritsch and Hamilton Verissimo de Oliveira предоставили некоторые полезные советы по использованию Avalan. Dave W Smith терзал меня своими вопросами по поводу моей начальной настройки  интерфейса инжектора, что в итоге привело меня к тому, что это действительно было тупо. Gerry Lowry прислала много исправлений опечаток - достаточное, что бы заслужить отдельную благодарность.

Существенные изменения
23 января 2004: Переделал конфигурационный код, примера с интерфейсом инжектора.
16 января 2004: Добавил небольшой пример  использования Avalon в качестве, как локатора, так и инжектора.
14 января 2004: Первая публикация.

4 комментария:

  1. статья отличная, но примеры фаулера просто ужасны. Обрублены, непонятны, нужно вчитываться. Я бы так и не понял чем отличается сервсил локатор от interface injection если бы не статья Александр Бындю.

    кому интересно: blog byndyu ru/2009/12/blog-post.html

    ОтветитьУдалить
  2. Спасибо за перевод интересной статьи

    ОтветитьУдалить