Внедрение зависимостей с помощью Guice

Хорошо тестируемый код с минимумом служебных частей

Guice - это разработанная Google инфраструктура с открытым кодом для внедрения зависимостей при разработке на Java™. Она позволяет улучшить тестируемость и модульность кода, избавляя программиста от необходимости написания собственных фабрик. Николас Лесецки предлагает вам познакомиться с наиболее важными концепциями Guice и подготовиться к применению Guice в ваших собственных приложениях.

Николас Лесецки, разработчик ПО, Google

Николас Лесецки пишет, говорит и размышляет об улучшении программного обеспечения с 2000 г. В то время, когда он не служит троном для своего кота, Николас разрабатывает код для инженерного офиса Google в Сиэтле.



24.04.2009

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

Для обзора, а не для споров

Guice - далеко не первая DI-инфраструктура. Есть целое множество замечательных инфраструктур (В разделе Ресурсы есть ссылка на страничку сайта PicoContainer, - одной из таких инфраструктур, - где рассказывается об истории развития и взаимоотношениях различных инфраструктур. Позднее появление Guice породило дискуссии о том, какая из существующих инфраструктур лучше и о том, нужна ли вообще еще одна DI-инфраструктура. Как при любом выборе технологии, каждая библиотека имеет свои плюсы и минусы. На мой взгляд, Guice содержит важные нововведения, но в этой статье я не буду ввязываться в споры, а сделаю обзор возможностей Guice. (В Интернете можно найти множество оживленных дискуссий, поискав по словам "guice vs spring".)

Guice 2.0 beta

На момент написания этой статьи, команда Guice работает над версией 2.0 и планирует выпустить ее до конца 2008 года. Бета-версия доступна для скачивания на сайте Google Code (см. ссылку в разделе Ресурсы). Это хорошая новость, потому что команда Guice добавила в эту версию новые возможности, которые позволяют сделать ваш Guice-код более простым для использования и понимания. Несмотря на то, что в бета-версии отсутствуют некоторые возможности, которые будут включены в финальную версию, все-таки это стабильная и высококачественная версия. Фактически Google уже использует эту бета-версию в своих работающих продуктах. Я советую вам сделать также. Я написал эту статью именно для Guice 2.0, рассказав о некоторых новых возможностях, и умолчав о возможностях версии 1.0, не рекомендуемых для использования в 2.0. Разработчики Guice заверили меня, что функциональность, о которой я расскажу, не будет изменяться в финальной версии по сравнению с бета-версией.

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

Пример использования DI

Я начну с примера. Скажем, я пишу игровое приложение с супергероем и реализую героя с именем Frog Man. Листинг 1 содержит код приложения и мой первый тест. (Я надеюсь, мне не нужно убеждать вас в пользе написания юнит-тестов.)

Листинг 1. Простой герой и его тест
public class FrogMan {
  private FrogMobile vehicle = new FrogMobile();
  public FrogMan() {}
  // здесь герой борется с преступностью...
}

public class FrogManTest extends TestCase {
 public void testFrogManFightsCrime() {
    FrogMan hero = new FrogMan();
    hero.fightCrime();
    //делаем несколько проверок...
  }
}

Казалось бы, все хорошо, но при запуске теста я получаю исключение, показанное в листинге 2:

Листинг 2. Зависимости могут доставлять неприятности
java.lang.RuntimeException: Refinery startup failure.
  at HeavyWaterRefinery.<init>(HeavyWaterRefinery.java:6)
  at FrogMobile.<init>(FrogMobile.java:5)
  at FrogMan.<init>(FrogMan.java:8)
  at FrogManTest.testFrogManFightsCrime(FrogManTest.java:10)

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

Введение в DI

Чтобы избежать этой проблемы, можно создать интерфейс (например, Vehicle) и заставить конструктор вашего класса FrogMan принимать этот интерфейс в качестве аргумента, как показано в листинге 3:

Листинг 3. Передаем зависимость через интерфейс
public class FrogMan {
  private Vehicle vehicle;

  public FrogMan(Vehicle vehicle) {
    this.vehicle = vehicle;
  }
  // здесь герой борется с преступностью...
}

Такой стиль является сущностью концепции DI - пусть ваши классы принимают зависимости в виде ссылок на интерфейсы, вместо того чтобы создавать их самим или использовать статические ссылки. Листинг 4 показывает, как DI упрощает тест:

Листинг 4. В тесте можно использовать заглушки вместо сложных зависимостей
static class MockVehicle implements Vehicle {
  boolean didZoom;

  public String zoom() {
    this.didZoom = true;
    return "Mock Vehicle Zoomed.";
  }
}

public void testFrogManFightsCrime() {
  MockVehicle mockVehicle = new MockVehicle();

  FrogMan hero = new FrogMan(mockVehicle);
  hero.fightCrime();

  assertTrue(mockVehicle.didZoom);
  // другие проверки
}

Этот тест использует вместо FrogMobile самодельный объект-заглушку. Благодаря DI, теперь тесту не нужно создавать сложный объект FrogMobile, ему о нем вообще ничего не надо знать! Все, что нужно знать тесту, - это интерфейс Vehicle. DI, кроме того, что делает тесты проще, также улучшает модульность и сопровождаемость вашего кода. Теперь, если вы захотите поменять FrogMobile на объект другого типа, например FrogBarge, вы можете это сделать не меняя FrogMan. Единственное от чего зависит FrogMan, - это интерфейс.

Однако здесь есть нюанс. Как и я, читая первый раз про Guice, вы можете подумать: "Отлично, теперь все, кто вызывают FrogMan, должны знать о FrogMobile (о его устройстве, зависимостях и т.д.)." Но если бы это было так, концепция DI никогда не стала бы популярной. Чтобы упростить всем жизнь, вы можете создавать фабрики, которые будут управлять созданием объектов и их зависимостей.

Именно при работе с фабриками хороши инфраструктуры. Фабрики требуют много скучного, повторяющегося кода. В лучшем случае, они просто раздражают автора кода (и его читателей), а в худшем случае их вообще не пишут из-за их неудобства. Guice и другие инфраструктуры с DI выполняют роль "суперфабрик", которые вы можете гибко настраивать для конструирования ваших объектов. Конфигурирование инфраструктуры гораздо проще написания собственной фабрики. Как результат, программисту удается написать больше кода в стиле DI. В итоге, мы имеем больше тестов, более качественный код и довольных программистов.


Простое внедрение зависимостей с помощью Guice

Надеюсь, мне удалось убедить вас, что DI делает проектирование более качественным и что использование инфраструктуры значительно облегчит вам жизнь. Давайте теперь приступим к изучению Guice, начав с аннотации @Inject и модулей.

Внедряем зависимость в класс с помощью аннотации @Inject

Единственная разница между предыдущим классом FrogMan и классом FrogMan с Guice - это строка @Inject. Листинг 5 показывает конструктор класса FrogMan с аннотацией:

Листинг 5. Внедрение в класс FrogMan с помощью @Inject
@Inject
public FrogMan(Vehicle vehicle) {
  this.vehicle = vehicle;
}

Некоторым разработчикам не нравится идея добавления аннотации @Inject в свой класс. Они предпочитают, чтобы класс ничего не знал об инфраструктуре DI. Это разумный аргумент, но все-таки он меня не убеждает. При большом количестве зависимостей использование аннотаций довольно удобно. Тег @Inject имеет значение только когда вы просите Guice сконструировать объект вашего класса. Если вы не просите Guice создать объект FrogMan, эта аннотация никак не влияет на поведение кода, - аннотация просто подсказывает, что Guice будет принимать участие в создании класса. Однако для использования аннотаций необходим доступ к исходному коду. Если аннотации вам не нравятся или вы используете Guice для создания объектов к исходному коду которых вы не имеете доступа, в Guice есть альтернативный механизм (см. врезку Другие варианты использования методов провайдера ниже).

Указываем Guice, какую именно зависимость мы хотим внедрить

Теперь, когда Guice знает, что вашему герою нужен некий Vehicle, ему нужно знать, какой конкретно Vehicle ему надо предоставить. Листинг 6 содержит Module: специальный класс, который используется для того, чтобы сообщить Guice какие реализации есть у ваших интерфейсов:

Листинг 6. HeroModule привязывает Vehicle к FrogMobile
public class HeroModule implements Module {
  public void configure(Binder binder) {
    binder.bind(Vehicle.class).to(FrogMobile.class);
  }
}

Модуль - это интерфейс с одним методом. Guice передает в ваш модуль связыватель Binder, с помощью которого вы можете сообщить Guice как вы хотите конструировать ваши объекты. Связыватель имеет API, который представляет собой предметно-ориентированный язык (см. Ресурсы). Этот миниязык позволяет писать "говорящий" код, например bind(X).to(Y).in(Z). Далее в статье вы увидите и другие примеры возможностей связывателя. Каждый вызов bind создает связывание и именно на основе связываний Guice выполняет запросы на внедрение зависимостей.

Создание процедур загрузки в Guice с помощью с помощью Инжектора

Затем мы выполняем начальную загрузку (bootstrap) Guice, используя объект класса Injector. Как правило, инжектор создают в самом начале программы, чтобы позволить Guice создать за вас большинство ваших объектов. Листинг 7 содержит пример основной программы, которая запускает приключение нашего героя с помощью класса Injector:

Листинг 7 Улучшение вашего приложения с помощью Injector
public class Adventure {
  public static void main(String[] args){
    Injector injector = Guice.createInjector(new HeroModule());
    FrogMan hero = injector.getInstance(FrogMan.class);
    hero.fightCrime();
  }
}

Чтобы получить инжектор, нужно вызвать метод createInjector класса Guice. Методу createInjector передают список модулей, которые он должен использовать для своей настройки. (В этом примере присутствует только один модуль, но можно было бы кроме модуля героев добавить, например, модуль злодеев VillainModule.) Теперь, имея инжектор, можно создавать объекты различных классов с помощью его метода getInstance, передавая ему класс в качестве аргумента. (Проницательные читатели заметят, что вам не нужно сообщать Guice о классе FrogMan. Оказывается, если класс имеет конструктор с аннотацией @Inject или открытый конструктор без аргументов, Guice может создавать объекты этого класса без вызова bind.)

Итак, первый способ создания ваших объектов с помощью Guice - делать каждый раз явный запрос. Однако вряд ли вы будете его использовать за пределами процедуры загрузки. Лучший и более простой способ - переложить на Guice внедрение всех зависимостей, включая зависимости этих зависимостей и так далее (по принципу «черепаха на черепахе на черепахе» - "it's turtles all the way down"; см. Ресурсы). Сначала такой подход может показаться непонятным, но через некоторое время вы к нему привыкнете. В качестве примера в листинге 8 показан FrogMobile с внедренным FuelSource:

Листинг 8. FrogMobile принимает параметр FuelSource
@Inject
public FrogMobile(FuelSource fuelSource){
  this.fuelSource = fuelSource;
}

Это означает, что когда вы запрашиваете объект класса FrogMan, Guice сначала создает объекты FuelSource, FrogMobile, а только потом объект FrogMan, даже если ваше приложение взаимодействовало с инжектором только единожды.

Конечно, вы не всегда имеете доступ к функции main вашего приложения. Например, многие Web-инфраструктуры "автомагически" конструируют "действия", "шаблоны" или что-то еще, что служит стартовой точкой в программе. Обычно все-таки удается найти место, чтобы "втиснуть" Guice, либо с помощью дополнительного модуля (плагина) к инфраструктуре, либо с помощью вашего собственного кода. (Например, проект Guice выпустил плагин к инфраструктуре Struts 2, который позволяет Guice конфигурировать ваши "действия" в Struts; см. Ресурсы.)


Другие формы внедрения

До сих пор мы видели аннотации @Inject, относящиеся к конструкторам. Guice, обнаружив аннотацию, пробегается по аргументам конструктора и для каждого из них пытается найти сконфигурированное связывание. Такой способ называется внедрением через конструктор. Согласно руководству по рекомендованным методикам Guice, внедрение через конструктор - это предпочтительный способ внедрения зависимостей. Но не единственный. В листинге 9 показан другой способ конфигурирования класса FrogMan:

Листинг 9. Внедрение через методы
public class FrogMan{
  private Vehicle vehicle;

  @Inject
  public void setVehicle(Vehicle vehicle) {
    this.vehicle = vehicle;
  }
//и т.д. ...

Заметьте, что здесь нет инъекции в конструкторе, но зато появился метод с аннотацией @Inject. Guice вызывает этот метод сразу после создания объекта героя. Любители инфраструктуры Spring могут представлять себе это как "инъекцию установщика". Однако для Guice важна только аннотация @Inject; ваш метод может называться как угодно и может принимать несколько аргументов. Также метод может быть защищенным на уровне пакета или объявлен частным.

Если решение Guice о доступе к частным методам кажется вам небезопасным, взгляните на листинг 10, в котором FrogMan использует внедрение через поле:

Листинг 10. Внедрение через поле
public class FrogMan {
  @Inject private Vehicle vehicle;
  public FrogMan(){}
//etc. ...

Еще раз: единственное, о чем заботится Guice - это аннотация @Inject. Guice ищет аннотированные поля и пытается внедрить в них соответствующую зависимость.

Какой способ лучше?

Все три версии класса FrogMan ведут себя одинаково: Guice внедряет соответствующий Vehicle при создании объекта. Однако я, как и создатели Guice, предпочитаю внедрение через конструктор. Вот краткий анализ этих трех стилей:

  • Внедрение через конструктор является простым и понятным. Занимается Guice созданием ваших объектов или нет, вы можете быть уверены, что объекты к вам попадут уже инициализированными, так как вызов конструктора гарантируется самой технологией Java. Также, вы можете пометить ваши поля как final.
  • Внедрение через поле ухудшает тестируемость кода, особенно если вы помечаете поля как private. Это противоречит одной из основных целей концепции DI. Внедрение через поле следует применять очень ограниченно.
  • Внедрение через метод может быть полезно, если вы не можете управлять созданием объектов класса. Также его можно использовать в случае, если у вас есть суперкласс с зависимостями. (В этом случае применение внедрения через конструктор затруднительно.)

Выбираем нужную вам реализацию

Предположим, в вашем приложении есть несколько реализаций Vehicle. Другой персонаж - героическая девушка Weasel Girl - не может использовать FrogMobile, а использует WeaselCopter, но вы не хотите жестко кодировать эту зависимость. Guice решает эту проблему, предоставляя вам возможность аннотировать зависимости. Листинг 11 показывает, как Weasel Girl запрашивает более быстрый вид транспорта:

Листинг 11. Используем аннотации для получения конкретной реализации
@Inject
public WeaselGirl(@Fast Vehicle vehicle) {
  this.vehicle = vehicle;
}

В листинге 12 модуль HeroModule использует связыватель, чтобы сообщить Guice, что WeaselCopter - это "быстрый" транспорт:

Листинг 12. Сообщите Guice об аннотации в вашем модуле
public class HeroModule implements Module {
 public void configure(Binder binder) {
    binder.bind(Vehicle.class).to(FrogMobile.class);
    binder.bind(Vehicle.class).annotatedWith(Fast.class).to(WeaselCopter.class);
  }
}

Заметьте, что я выбрал аннотацию, которая абстрактно описывает тип транспорта (@Fast), вместо аннотации, тесно связанной с реализацией (@WeaselCopter). Если использовать аннотации, которые точно описывают реализацию, это будет создавать неявную зависимость между ними в головах читателей кода. Если вы используете аннотацию @WeaselCopter, а Weasel Girl будет использовать транспорт Wombat Rocket, это может повергнуть в недоумение программистов, читающих или отлаживающих ваш код.

Чтобы создать аннотацию @Fast, скопируйте шаблон из листинга 13:

Листинг 13. Скопируйте этот код для создания связывающей аннотации
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface Fast {}

Если много применять BindingAnnotations, это приведет к тому, что у вас накопится много таких маленьких файлов, различающихся лишь именем аннотации. Если вам это досаждает или вы хотите быстро создать какой-нибудь прототип, вам стоит взглянуть на встроенную аннотацию @Named, которую предлагает Guice. Эта аннотация принимает строковый атрибут. Этот альтернативный подход иллюстрирует листинг 14:

Листинг 14. Использование аннотации @Named вместо пользовательских аннотаций
// в WeaselGirl
@Inject
public WeaselGirl(@Named("Fast") Vehicle vehicle) {
  //...
}

// в HeroModule
binder.bind(Vehicle.class)
  .annotatedWith(Names.named("Fast")).to(WeaselCopter.class);

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

Но что, если вы вовсе не хотите использовать аннотации? Ведь добавление @Fast или @Named("Fast") делает ваш код частично ответственным за конфигурирование самого себя. Если вы так считаете, читайте далее.


Методы провайдера

Другие варианты использования методов провайдера

Если вам не нравится использование аннотаций в Guice или вы не можете их использовать (например, если вы создаете объект стороннего класса), вам помогут элегантно решить проблему методы провайдера. Так как метод провайдера находится внутри вашего модуля, вы можете аннотировать его, не беспокоясь, что эти аннотации будут видны из других участков кода. Например, листинг 16 содержит метод провайдера для стороннего героя BadgerBoy:

Листинг 16. Метод провайдера позволяет вам не аннотировать исходный код
@Provides @Inject
private BadgerBoy provideBadgerBoy(WeaselCopter copter) {
return new BadgerBoy(copter);
}

Здесь, применяя метод провайдера, вы можете использовать Guice для конфигурирования BadgerBoy, и даже для выбора того, какой именно Vehicle ему нужен. Все это можно сделать, совершенно не меняя класс BadgerBoy. Это значит, что если вы не желаете использовать аннотацию @Inject или связывающую аннотацию, такую как @Fast, вы можете обойтись без них. Вместо этого вы можете использовать метод провайдера для выбора нужной зависимости.

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

Предположим, вам надоело посылать Frog Man на каждое задание, и теперь вы хотите выбирать героя для каждого нового приключения случайным образом. Однако стандартный API, поставляемый с Guice, не имеет вызова вроде "при каждом обращении связывать класс Hero с произвольной реализацией." Но вы можете заставить Guice использовать специальный метод для создания каждого нового объекта Hero. В листинге 15 показан новый метод, добавленный в модуль HeroModule, помеченный специальной аннотацией @Provides:

Листинг 15. Использование провайдера для создания особой логики конструирования объектов
@Provides
private Hero provideHero(FrogMan frogMan, WeaselGirl weaselGirl) {
  if (Math.random() > .5) {
    return frogMan;
  }
  return weaselGirl;
}

Guice автоматически распознает в ваших модулях все методы, имеющие аннотацию @Provides. Так как метод provideHero возвращает объект класса Hero, теперь каждый раз для создания героя по вашему запросу, Guice будет вызывать этот метод. Вы можете заполнять методы провайдера какой угодно логикой создания объектов. Можете выбирать тип объекта случайно, брать объект из кэша и т.д. Методы провайдера - это отличный способ для интеграции сторонних библиотек в ваш модуль Guice. Методы провайдера появились впервые в Guice 2.0. (В Guice 1.0 нужно было писать собственные классы провайдера, что было менее аккуратно и сжато по сравнению с методами провайдера. Если вы решите использовать Guice 1.0, в руководстве пользователя есть описание этого способа; также вы можете ознакомиться с классом провайдера в примерах кода, прилагаемых к этой статье.)

В листинге 15 Guice автоматически передает в метод провайдера правильные аргументы. Это значит, что Guice сам находит классы WeaselGirl и FrogMan в своем списке связываний, и вам не нужно самостоятельно генерировать их внутри метода провайдера. Это иллюстрирует вышеупомянутый принцип "черепахи на черепахе на черепахе", - вы можете полагаться на Guice при создании необходимых зависимостей даже когда речь идет о конфигурировании самого модуля Guice.

Использование провайдера вместо зависимостей

Предположим, мы создаем большое приключение – Сагу – и хотим использовать в нем множество героев. Если попросить Guice внедрить объект Hero, будет создан всего один герой. Но если вы прикажете Guice создать "провайдера героев", как в листинге 17, вы сможете создать столько героев, сколько пожелаете:

Листинг 17. Внедряем провайдера для получения контроля над созданием объектов
public class Saga {
  private final Provider<Hero> heroProvider;

  @Inject
  public Saga(Provider<Hero> heroProvider) {
    this.heroProvider = heroProvider;
  }

  public void start() throws IOException {
    for (int i = 0; i < 3; i++) {
      Hero hero = heroProvider.get();
      hero.fightCrime();
    }
  }
}

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

Интерфейс Provider имеет единственный метод: get<T>. Чтобы получить доступ к объекту через интерфейс, нужно просто вызвать этот метод. Будет или нет создаваться каждый раз новый объект, как этот объект будет сконфигурирован, - это зависит от настроек Guice. В следующей секции мы увидим как настроить Guice для работы с синглтонами и другими долгоживущими объектами. А в этом случае Guice использует метод @Provides так как здесь это зарегистрированный способ создания новых объектов Hero. В результате для нашей саги будет случайно выбрано 3 героя.

Не следует путать провайдеров и методы провайдера. (В Guice 1.0 о них было довольно сложно говорить по отдельности). Хотя класс Saga получает объекты своих героев с помощью вашего метода @Provides, вы можете работать через провайдера с любой созданной Guice зависимостью. Если вы хотите, можно переписать конструктор класса FrogMan в соответствии с листингом 18:

Листинг 18. Вы можете запросить провайдер вместо зависимости
@Inject
public FrogMan(Provider<Vehicle> vehicleProvider) {
  this.vehicle = vehicleProvider.get();
}

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


Области видимости

По умолчанию, Guice создает по каждому запросу новый экземпляр зависимости. Если ваши объекты не требуют много ресурсов, то такой подход вполне приемлем. Однако если издержки на создание вашей зависимости велики, стоит рассмотреть возможность организации общего доступа к одному экземпляру зависимости нескольким клиентам. В листинге 19 модуль HeroModule осуществляет связывание с зависимостью HeavyWaterRefinery как с синглтоном:

Листинг 19. Связывание с HeavyWaterRefinery как с синглтоном
public class HeroModule implements Module {
  public void configure(Binder binder) {
    //...
    binder.bind(FuelSource.class)
      .to(HeavyWaterRefinery.class).in(Scopes.SINGLETON);
  }
}

Синглтоны - это зло?

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

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

Guice предлагает вам возможность выбрать способ указания области видимости. Вы можете указать ее с помощью связывателя или же аннотировать зависимость напрямую, как показано в Листинге 20:

Листинг 20. Выбираем область видимости с помощью аннотации
@Singleton
public class HeavyWaterRefinery implements FuelSource {...}

Guice не только предлагает готовую область видимости Singleton, но и дает вам возможность при желании определить свои собственные области видимости. Например, пакет Guice по работе с сервлетами предлагает две дополнительные области видимости: Request и Session, которые обеспечивают уникальность экземпляра вашего класса в рамках запроса сервлета и сессии сервлета соответственно.


Постоянное связывание и конфигурация модуля

Предположим, что классу HeavyWaterRefinery для запуска нужен лицензионный ключ. Оказывается, Guice может связывать как объекты классов, так и значения констант. Взгляните на листинг 21:

Листинг 21. Связывание со значением константы
public class HeavyWaterRefinery implements FuelSource {
  @Inject
  public HeavyWaterRefinery(@Named("LicenseKey") String key) {...}
}

// in HeroModule:
binder.bind(String.class)
  .annotatedWith(Names.named("LicenseKey")).toInstance("QWERTY");

Связывающая аннотация здесь необходима, так как иначе Guice не смог бы различать между собой разные объекты String.

Заметьте, что здесь я выбрал аннотацию @Named несмотря на то, что ранее я рекомендовал не использовать ее. Я сделал так для того, чтобы показать код в листинге 22:

Листинг 22. Конфигурирование модуля с помощью файла свойств
//В HeroModule:
private void loadProperties(Binder binder) {
  InputStream stream =
    HeroModule.class.getResourceAsStream("/app.properties");
  Properties appProperties = new Properties();
  try {
    appProperties.load(stream);
    Names.bindProperties(binder, appProperties);
  } catch (IOException e) {
    // Это предпочтительный способ сообщения Guice о том, что что-то пошло не так
    binder.addError(e);
  }
}

//В файле app.properties:
LicenseKey=QWERTY1234

Этот код использует функцию Guice Names.bindProperties для связывания каждого свойства в файле app.properties и константы с соответствующей аннотацией @Named . Это удобно само по себе, и кроме того, это показывает, как можно сделать модуль сколь угодно сложным. При желании можно загружать связывающую информацию из базы данных или XML файла. Модули представляют собой чистый Java-код, и это дает вам очень большую гибкость.


Что дальше?

Подытожим основные идеи Guice:

  • Вы запрашиваете зависимость с помощью аннотации @Inject.
  • Вы связываете зависимости и реализации с помощью интерфейса module.
  • Вы реализуете загрузочные процедуры с помощью Injector.
  • Вы можете увеличивать гибкость с помощью методов, аннотированных @Provides.

Конечно, можно еще много рассказать о Guice, но, понимая темы, раскрытые в этой статье, вы можете сами начать работу и учиться дальше самостоятельно. Я рекомендую вам загрузить Guice, а также код примеров из этой статьи и попробовать их запустить. А еще лучше - создать небольшое приложение самостоятельно. Полезно поэкспериментировать с концепциями, не заботясь о работоспособности кода. Если вы хотите узнать больше о продвинутых возможностях Guice (например, поддержке аспектно-ориентированного программирования), рекомендую вам изучить ссылки в разделе Ресурсы.

Если говорить о коде в реальных продуктах, то здесь минусом DI является то, что работа Guice может быть похожа на распространение вируса. Внедрение зависимости в один класс влечет за собой внедрение зависимости в еще один, и так далее. Это хорошо, так как DI улучшает код. Но, с другой стороны, это может привести к большому рефакторингу существующего кода. Чтобы сохранить контроль, вы можете хранить Guice Injector где-нибудь отдельно и вызывать его явно. Относитесь к этому как к "костылю" - на данный момент это необходимо, но в будущем от этого надо избавляться.

Скоро выйдет Guice 2.0. Некоторые из его возможностей, о которых я не рассказал, облегчат конфигурирование модулей и поддержку более сложных и хитроумных конфигурационных схем. Пройдя по ссылкам в разделе Ресурсы, вы можете изучить эти новые возможности.

Я надеюсь, вы подумаете о возможности добавить Guice в ваш инструментарий. По моему опыту, DI очень важна для гибкости и тестируемости кода. Guice делает работу с DI легкой и даже приятной. Что может быть лучше гибкого, тестируемого кода, который к тому же приятно писать?


Загрузка

ОписаниеИмяРазмер
Файлы Java для этой статьиj-guice.zip19 КБ

Ресурсы

Научиться

Получить продукты и технологии

  • Guice (EN): загрузите бета-версию Guice 2.
  • Плагин Guice (EN): получите плагин Guice для Struts.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


  • Bluemix

    Узнайте больше информации о платформе IBM Bluemix, создавайте приложения, используя готовые решения!

  • developerWorks Premium

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

  • Библиотека документов

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=385362
ArticleTitle=Внедрение зависимостей с помощью Guice
publish-date=04242009