среда, 21 июля 2010 г.

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

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


Inversion of Control Containers and the Dependency Injection pattern. Martin Fowler

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

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


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

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

Сервис похож на компонент в том, что он используется другими приложениями. Главное отличие в том, что компонент используется локально (например 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, но я все равно нуждаюсь в каком-либо способе получить экземпляр нужного класса, реализующего объект поиска.

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

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

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

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

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


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), который подставляет в поле в классе списка реализацию согласно интерфейсу поиска, результат зависимостей показан на рисунке 2.


Рисунок 2. Зависимости для 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. Я начну прежде всего с него, так как некоторые мои коллеги по ThoughtWorks активно участвуют в его разработке (да, это своего рода корпоративная семейственность).

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());
    }
Контейнер использует объявленные интерфейсы инъекции что бы выяснить зависимости, а так же инжекторы, что бы внедрить правильные зависимости. (Реализация указанного контейнера, которую я сделал, не так важна для этой техники, поэтому я не буду ее демонстрировать, тем более увидев ее вы рассмеетесь.)

Продолжение перевода статьи

13 комментариев:

  1. Спасибо за перевод! Считаю, это лучшая статья по теме

    ОтветитьУдалить
  2. http://chtoby.narod.ru/chtoby/

    ОтветитьУдалить
  3. Перевод хороший, но очень много орфографических ошибок.

    ОтветитьУдалить
  4. Спасибо за работу!
    Позволю комментарий: в переводе звучит "Реализация этой функции примитивна, она просит объект поиска (до которого мы доберемся через мгновение) вернуть список всех известных ему фильмов." - здесь "объект поиска" лучше было записать как "объект finder" - так сразу понятно о чем речь, а "объект поиска" сбивает с толку (т.к. нет прямой привязки к коду) и заставляет смотреть в оригинал.

    ОтветитьУдалить
    Ответы
    1. Автор исправь пожалуйста это. Я тоже долго тупил, что за объект поиска.

      Удалить
  5. Спасибо за перевод!

    ОтветитьУдалить
  6. "Под компонентом я имею ввиду программное обеспечение, которое предполагается для использования как есть, т.е. без возможности изменения, т.к. приложение написано сторонними лицами и не подлежит моему контролю".
    Всё-таки в оригинале у этого предложения немного другой смысл... Это скорее:
    "Под компонентом я имею в виду часть программного обеспечения, которая предполагается для использования как есть, т.е. без возможности изменения, приложением, которое написано сторонними лицами и находится вне контроля разработчика компонента. " Т.к. тут лишнее)

    ОтветитьУдалить
  7. И ещё следующие предложения:

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

    можно перевести немного по-другому (возможно, я ошибаюсь, но по-моему, так будет ближе по смыслу к оригиналу):

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

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

    мне кажется, лучше заменить на:

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

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