Edit Page

Dependency inversion


Попытки разобраться с принципом ООП Инверсия зависимостей (Dependency Inversion Principle) привели к статье Мартина Фаулера "Inversion of Control Containers and the Dependency Injection pattern" оригинал которой доступен здесь. Перевод на русский язык можно найти здесь. В данном разделе будут представлены выдержки из этого перевода соответствие с реалиями фреймворка LeanES.

Inversion of Control Containers and the Dependency Injection pattern.

В сообществе Java-разработчиков был пик легких контейнеров, которые помогали собирать компоненты из множества проектов в единое приложение. В основе этих контейнеров лежит паттерн, известный также под общим названием "Inversion of Control" (Инверсия управления). В этой статье я разберусь как этот паттерн работает, под более конкретным названием "Dependency Injection" (Внедрение зависимости), а так же сравню его с альтернативой в виде Service Locator. Выбор между ними менее важен чем принцип разделения конфигурирования от использования.

Одна из замечательных вещей в мире корпоративной Java-разработки это стремление к разработке открытых альтернатив господствующим технологиям J2EE. Многие эти решения являются реакцией на тяжеловесные решения J2EE, также значительная часть направлена на альтернативные решения, что способствует генерации творческих идей. Общая проблема в том, как связать вместе различные элементы: как связать вместе эту веб-архитектуру с этим интерфейсом базы данных, которые были разработаны разными командами с плохим знанием друг о друге. Несколько фреймворков приняли удар на себя и расширились, что бы предоставить единую возможность сборки компонентов из различных слоев. Они часто упоминаются как легкие контейнеры, примерами могут быть PicoContainer и Spring. В основе этих контейнеров лежит ряд интересных принципов проектирования, вещи которые выходят за рамки этих двух конкретных контейнеров и даже за рамки платформы Java в целом. Примеры будут написаны на Java, но, принципы применимы и к другим ОО средам.

Компоненты и Сервисы

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

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

Сервис похож на компонент в том, что он используется другими приложениями. Главное отличие в том, что компонент используется локально (например jar-файл, dll и т.д). Сервис используется удаленно, через некоторый удаленный интерфейс в синхронном или асинхронном режиме (например веб-сервис, система обмена сообщениями, RPC или сокет).

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

Простой пример

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

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

class MovieLister...
    public Movie[] moviesDirectedBy(String arg) {
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext();) {
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }

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

Основная цель этой статьи это объект поиска, точнее как мы связываем объект список с этим объектом поиска. Причина почему это интересно в том, что мы хотим чтобы прекрасный метод moviesDirectedBy был полностью независим от того, как этот список хранится. Итак, все что метод делает, это ссылается на объект поиска, в свою очередь все что делает объект поиска - это знает как реагировать на метод findAll. Мы можем вынести эту зависимость из метода путем определения интерфейса для поиска.

public interface MovieFinder {
    List findAll();
}

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

class MovieLister...  
  private MovieFinder finder;  
  public MovieLister() {  
    finder = new ColonDelimitedMovieFinder("movies1.txt");  
  }

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

Итак, если мы используем этот класс только для себя, то все замечательно. Но что случится когда наши друзья проникнутся этой великолепной функциональностью и попытаются скопировать это решение к себе? Если они хранят списки фильмов в текстовом файле с разделителями под именем "movies1.txt" то все нормально. Если у них просто файл называется по другому, то все что нужно вынести имя файла в свойства файла. Но что если они имеют совершенно другую форму хранения списка фильмов: SQL-база, XML-файл, веб-сервис или какой другой формат текстового файла? В этом случае мы нуждаемся в другом классе для захвата данных. Теперь, поскольку мы выделили MovieFinder интерфейс, это не изменит нашего метода moviesDirectedBy, но мы все равно нуждаемся в каком-либо способе получить экземпляр нужного класса, реализующего объект поиска.

Зависимости, с использованием простого создания в классе списка.

Рисунок показывает зависимости для этой ситуации. Класс MovieLister зависит как от интерфейса, так и от реализации. Было бы лучше, если бы зависимость была только от интерфейса, но тогда как заставить работать реализацию?

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

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

Итак, ключевая проблема как собрать эти плагины в приложении? Это одна из главных проблем которую эти легковесные контейнеры решают через универсальные механизмы, используя Inversion of Control.

Inversion of Control (Инверсия управления)

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

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

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

В результате, мы нуждаемся в более конкретном названии для этого шаблона. Inversion of Control слишком общий термин, который может сбить людей с толку. Как результат, в следствии продолжительных дискуссий со сторонниками IoC мы остановились на названии Dependency Injection (Внедрение зависимости).

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

Формы Dependency Injection

Основная идея Dependency Injection заключается в наличии отдельного объекта - сборщика (Assembler), который подставляет в поле в классе списка реализацию согласно интерфейсу поиска, результат зависимостей показан на рисунке

Зависимости для Dependency Injector

Можно выделить три основных стиля внедрения зависимости под следующими названиями: Constructor Injection, Setter Injection, и Interface Injection. Если в ходе текущей дискуссии вы читали материал об Inversion of Control они упоминаются как тип 1 IoC (interface injection), тип 2 IoC (setter injection) и тип 3 IoC (constructor injection).

Constructor Injection в PicoContainer

Начнем описание того, как это внедрение происходит с помощью легкого контейнера под названием PicoContainer.

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

class MovieLister...
    public MovieLister(MovieFinder finder) {
        this.finder = finder;       
    }

Средство поиска также будет управляться PicoConctainer'ом, и таким же образом будет внедряться название текстового файла с данными.

class ColonMovieFinder...
    public ColonMovieFinder(String filename) {
        this.filename = filename;
    }

Затем, необходимо сказать PicoContainer'у какой класс реализации связать с каждым интерфейсом, а также какую строку внедрить в класс средства поиска.

private MutablePicoContainer configureContainer() {      
    MutablePicoContainer pico = new DefaultPicoContainer();
    Parameter[] finderParams =  {new ConstantParameter("movies1.txt")};
    pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
    pico.registerComponentImplementation(MovieLister.class);
    return pico;
}

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

public void testWithPico() {
    MutablePicoContainer pico = configureContainer();
    MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

Несотря на то, что в этом примере мы использовали Construction Injection, PicoContainer так же поддерживает Setter Injection, хотя разработчиками этого контейнера предпочтение отдается именно иньякциям с помощью конструктора.

Setter Injection в Spring

Spring framework это всеобъемлющий фреймворк в мире корпоративной разработки на Java. Он включает в себя слои абстаркций для транзакции, сохранения, разработки веб-приложения и JDBC. Подобно PicoContainer'у он поддерживает как инъекции с помощью конструктора, так и с помощью set-методов, правда его разработчики предпочитают setter-инъекции, что делает его подходящим выбором для данного примера.

Что бы принять инъекцию в наш список фильмов, мы определяем set-метод:

class MovieLister...
    private MovieFinder finder;
    public void setFinder(MovieFinder finder) {
      this.finder = finder;
    }

Аналогично, мы определяем set-метод для имени файла:

class ColonMovieFinder...
    public void setFilename(String filename) {
        this.filename = filename;
    }

Третий шаг - создание конфигурационного файла. Spring поддерживает конфигурирование как через XML-файлы, так и через код, правда предпочтительней делать это через XML.

<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>

Протестировать все это можно следующим образом:

public void testWithSpring() throws Exception {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

Interface Injection

Третья техника внедрения определяет и использует интерфейс. Фреймворк Avalon и спользует как раз такой подход.

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

Вот сам интерфейс для внедрения средства поиска фильмов в объект.

public interface InjectFinder {
    void injectFinder(MovieFinder finder);
}

Этот интерфейс будет объявляться теми, кто поддерживает интерфейс MovieFinder. Также интерфейс должен быть реализован всеми классами, которые хотят использовать средство поиска фильмов, например наш список.

class MovieLister implements InjectFinder...
    public void injectFinder(MovieFinder finder) {
        this.finder = finder;
    }

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

public interface InjectFinderFilename {
    void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFinderFilename......
    public void injectFilename(String filename) {
        this.filename = filename;
    }

Затем, как обычно, нам требуется настроить окружение, что бы связать реализации. Для простоты мы сделаем это прямо в коде.

class Tester...
    private Container container;

     private void configureContainer() {
       container = new Container();
       registerComponents();
       registerInjectors();
       container.start();
    }

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

class Tester...
  private void registerComponents() {
    container.registerComponent("MovieLister", MovieLister.class);
    container.registerComponent("MovieFinder", ColonMovieFinder.class);
  }

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

class Tester...
  private void registerInjectors() {
    container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
    container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
  }

Каждый объект-инжектора реализует интерфейс инжектора.

public interface Injector {
  public void inject(Object target);
}

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

class ColonMovieFinder implements Injector......
  public void inject(Object target) {
    ((InjectFinder) target).injectFinder(this);        
  }
class Tester...
  public static class FinderFilenameInjector implements Injector {
    public void inject(Object target) {
      ((InjectFinderFilename)target).injectFilename("movies1.txt");      
    }
  }

Пример когда используем контейнер.

class IfaceTester...
    public void testIface() {
      configureContainer();
      MovieLister lister = (MovieLister)container.lookup("MovieLister");
      Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
      assertEquals("Once Upon a Time in the West", movies[0].getTitle());
    }

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

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

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

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

Зависимости для сервис локатора

В этом случае, мы будем использовать локатор сервисов, как одиночку 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");

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

Использование и локатора и инжектирования

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

Простой пример с использованием такого подхода.

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-методоты то это может привести к беде.

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

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

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

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

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

Настройка в коде или в конфигурационном файле

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

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

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

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

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

Разделение конфигурирования от использования

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

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

Некоторые дополнительные вопросы

В данном разделе мы рассмотрели основные вопросы организации Инверсии управления через Инверсию зависимости (инъекцию) и Сервис локатор. Это в большей степени теоретическая глава, примеры кода даны только чтобы раскрыть идею. В следующей главе мы рассмотрим более практичные вопросы применительно нашего фреймворка LeanES.