Перевод проекта на Dependency Injection. Путь Ситха

от автора

Внесу и свой вклад в тренд темного программирования.
Многим из вас знакома дилемма: использовать ли DI в своем проекте или нет.
Поводы перехода на DI:

  • создание развитой системы авто-тестов
  • повторное использование кода в различном окружении, в том числе в различных проектах
  • использование 3rd-party библиотек, построенных на DI
  • изучение DI

Доводы не использовать DI:

  • усложнение понимания кода (поначалу)
  • необходимость конфигурирования контекста
  • изучение DI

Допустим, у нас есть большой рабочий проект, принято решение: переводить на DI. Разработчики чувствуют свой потенциал, уровень мидихлориан в крови зашкаливает.

Путь тебя ждет тернистый и долгий, мой юный падаван.

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

С чего начать

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

import org.jetbrains.annotations.Nullable;  public class Jedi {     private long id;     private String name;     @Nullable     private Long masterId;      // fields, constructors, getters/setters, equals, hashCode, toString, etc...      public long getId() {         return id;     }      public String getName() {         return name;     }      @Nullable     public Long getMasterId() {         return masterId;     }      @Nullable     public Jedi getMaster() {         if (masterId == null) {             return null;         }         return DBJedi.getJedi(masterId);     } } 

Порядочный Джедай метод getMaster уберет вообще или же перенесет в другой класс (сервис). В итоге класс Jedi станет просто бином с данными. Если перенос метода по какой-либо причине сейчас невозможен (например, от него зависит код, недоступный для рефакторинга), можно его объявить deprecated и пока что оставить (как вариант – объявить версию, в которой этот метод будет удален, как это делают разработчики Guava).
Теперь разберемся с DBJedi:

public class DBJedi {     public static Jedi getJedi(long id) {         DataSource dataSource = ConnectionPools.getDataSource("jedi");          Jedi jedi;         // magic         return jedi;     } } 

Подобный класс логично переделать в классический singleton, например, так:

import javax.sql.DataSource;  public class DBJedi {     private static final DBJedi instance = new DBJedi();      private final ConnectionPools connectionPools;      private DBJedi() {         this.connectionPools = ConnectionPools.getInstance();     }      public static DBJedi getInstance() {         return instance;     }      public Jedi getJedi(long id) {         DataSource dataSource = connectionPools.getDataSource("jedi");          Jedi jedi;         // magic         return jedi;     } } 

В результате мы получим более стройную и читаемую структуру кода (весьма спорный факт, конечно). Если довести начатое до конца, в целом переход на DI можно сделать по стандартным гайдам.
Но если Вы — Ситх, то наверняка остались классы (в нашем примере – класс Jedi с методом getMaster), которые по-хорошему не переводятся стандартным способом.

Теперь нужно еще раз подумать о целесообразности прикручивания DI. Если желание все же осталось — продолжаем.
Примеры будут преимущественно на Guice, частично продублированы на Spring. Насчет выбора фреймворка — выбирайте тот, который лучше знаете и неважно какого цвета листочек на его лого.

Плохая практика 1 – сохраняем ссылку на Injector


В какой-то момент встанет вопрос – где взять инстанс инжектора, чтобы вытащить синглтоны? Заведем утилитный класс:

import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; import com.google.inject.Injector; // import com.google.common.base.Preconditions; // guava  public class InjectorUtil {     private static volatile Injector injector;      public static void setInjector(@NotNull Injector injector) {         // Preconditions.checkNotNull(injector);         // Preconditions.checkState(InjectorUtil.injector == null, "Injector already initialized");         InjectorUtil.injector = injector;     }      @TestOnly     public static void rewriteInjector(@NotNull Injector injector) {         // Preconditions.checkNotNull(injector);         InjectorUtil.injector = injector;     }      @Deprecated // use fair injection, Sith!     @NotNull     public static Injector getInjector() {         // Preconditions.checkState(InjectorUtil.injector != null, "Injector not initialized");         return InjectorUtil.injector;     } } 

Для Spring код будет аналогичен, только вместо Injector — ApplicationContext. Либо еще один вариант:

Кошмарный сон перфекциониста

import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware;  import javax.inject.Named;  @Named public class ApplicationContextUtil implements ApplicationContextAware { 	private static volatile ApplicationContext applicationContext;  	public void setApplicationContext(ApplicationContext applicationContext) { 		ApplicationContextUtil.applicationContext = applicationContext; 	}  	@Deprecated 	public static ApplicationContext getApplicationContext() { 		// Preconditions.checkState(applicationContext != null); 		return applicationContext; 	} } 

Теперь наши синглтоны можно переписать так:

@javax.inject.Singleton @javax.inject.Named // пригодится для Spring component-scan, в Guice не требуется public class DBJedi {     private final ConnectionPools connectionPools;      @javax.inject.Inject     public DBJedi(ConnectionPools connectionPools) {         this.connectionPools = connectionPools;     }      @Deprecated     public static DBJedi getInstance() {         return InjectorUtil.getInjector().getInstance(DBJedi.class);     }      public Jedi getJedi(long id) {         DataSource dataSource = connectionPools.getDataSource("jedi");          Jedi jedi;         // ...         return jedi;     } } 

Обращаю внимание, что используются аннотации JSR-330, пакет javax.inject. Используя их, можно впоследствии с большей легкостью перейти с одного DI на другой, в идеальном случае — вообще абстрагироваться от конкретного фреймворка (при условии JSR-330-совместимости). Аннотация Named позволит не делать запись bean в spring-context.xml, если в xml-конфигурации все-таки подразумевается такая запись, аннотацию следует убрать.

Плохая практика 2 – Bean Factory


Если класс является бином с данными, но при этом обращается к singleton-объектам, можно сделать класс-фабрику:

public class Jedi {     private long id;     private String name;     @Nullable     private Long masterId;      private final DBJedi dbJedi;      private Jedi(long id, String name, @Nullable masterId, DBJedi dbJedi) {         this.id = id;         this.name = name;         this.masterId = masterId;          this.dbJedi = dbJedi;     }      //...      public long getId() {         return id;     }      public String getName() {         return name;     }      @Nullable     public Long getMasterId() {         return masterId;     }      @Nullable     public Jedi getMaster() {         if (masterId == null) {             return null;         }         return dbJedi.getJedi(masterId);     }      @Singleton     @Named     public static class Factory {         private final DBJedi dbJedi;          @Inject         public Factory(DBJedi dbJedi) {             this.dbJedi = dbJedi;         }          @Deprecated // refactor Jedi class to simple bean, Sith!         public Jedi create(long id, String name, @Nullable masterId) {                 return new Jedi(id, name, masterId, dbJedi);         }     } } 

Плохая практика 3 — циклические зависимости


В нашем примере между классами DBJedi и Jedi.Factory образуется циклическая зависимость. При попытке создать эти объекты в runtime мы получим ошибку DI-контейнера, например, StackOverflowError. Тут на помощь приходит интерфейс Provider:

import javax.inject.Singleton; import javax.inject.Named; import javax.inject.Inject; import javax.inject.Provider; import javax.sql.DataSource;  @Singleton @Named public class DBJedi {     private final ConnectionPools connectionPools;     private final Provider<Jedi.Factory> jediFactoryProvider;      @Inject     public DBJedi(ConnectionPools connectionPools, Provider<Jedi.Factory> jediFactoryProvider) {         this.connectionPools = connectionPools;         this.jediFactoryProvider = jediFactoryProvider;     }      @Deprecated     public static DBJedi getInstance() {         return InjectorUtil.getInjector().getInstance(DBJedi.class);     }      public Jedi getJedi(long id) {         DataSource dataSource = connectionPools.getDataSource("jedi");          // ...          final Jedi.Factory jediFactory = jediFactoryProvider.get();         return jediFactory.create(id, name, masterId);     } } 

Верно отметить, что generic-декларации недоступны посредством Reflection. Что касается Guice и Spring, они оба читают байт-код класса и таким образом получают generic-тип.

Пишем тесты


В testng есть замечательная аннотация Guice, упрощающая тестирование кода. Для Spring — артефакт org.springframework:spring-test.
Сделаем тест для наших классов:

import org.testng.annotations.*; import com.google.inject.Injector; import com.google.inject.AbstractModule;  @Guice(modules = JediTest.JediTestModule.class) public class JediTest {     private static final long JEDI_QUI_GON_ID = 12;     private static final long JEDI_OBI_WAN_KENOBI_ID = 22;      @Inject     private Injector injector;      @Inject     private DBJedi dbJedi;      @BeforeClass     public void setInjector() {         InjectorUtil.rewriteInjector(injector);     }      @Test     public void testJedi() {         final Jedi obiWan = dbJedi.getJedi(JEDI_OBI_WAN_KENOBI_ID);         final Jedi master = obiWan.getMaster();         Assert.assertEquals(master.getId(), JEDI_QUI_GON_ID);     }      public static class JediTestModule extends AbstractModule {         @Override         public void configure() {             // реализация ConnectionPools опущена, т.к. эту цепочку можно продолжать бесконечно             bind(ConnectionPools.class).toInstance(new ConnectionPools("pools.properties"));         }     } } 

А потом-то что?

А потом у нас возможны два исхода. Первый — остановиться на достигнутом. Так случилось в одном из моих проектов, целиком его перевести на честный DI не удалось, в нем было много legacy-кода. Думаю, эта ситуация знакома многим. Можно немного ее улучшить, например, заменив статическое поле в InjectorUtil на ThreadLocal, таким образом решив проблему concurrent-тестирования с разным DI-окружением в одном статическом пространстве.

Подробнее

public class InjectorUtil {     private static final ThreadLocal<Injector> threadLocalInjector =             new InheritableThreadLocal<Injector>();      private InjectorUtil() {     }      /**      * Get thread local injector for current thread      *      * @return      * @throws IllegalStateException if not set      */     @NotNull     public static Injector getInjector() throws IllegalStateException {         final Injector Injector = threadLocalInjector.get();         if (Injector == null) {             throw new IllegalStateException("Injector not set for current thread");         }         return Injector;     }      /**      * Set Injector for current thread      *      * @param Injector      * @throws java.lang.IllegalStateException if already set      */     public static void setInjector(@NotNull Injector injector) throws IllegalStateException {         if (injector == null) {             throw new NullPointerException();         }         if (threadLocalInjector.get() != null) {             throw new IllegalStateException("Injector already set for current thread");         }         threadLocalInjector.set(injector);     }      /**      * Rewrite Injector for current thread, even if already set      *      * @param injector      * @return previous value if was set      */     public static Injector rewriteInjector(@NotNull Injector injector) {         if (injector == null) {             throw new NullPointerException();         }         final Injector prevInjector = threadLocalInjector.get();         threadLocalInjector.set(injector);         return prevInjector;     }      /**      * Remove Injector from thread local      *      * @return Injector if was set, else null      */     public static Injector removeInjector() {         final Injector prevInjector = threadLocalInjector.get();         threadLocalInjector.remove();         return prevInjector;     } } 

Второй — довести дело до конца. В нашем примере сначала избавимся от метода Jedi.getMaster, тогда Jedi превратится в простой bean. После этого убираем класс Jedi.Factory. Исчезнет и циклическая зависимость. В итоге не станет и самого класса InjectorUtil. Проекты без такого класса — реальность. Пожалуй, стоит пройти все эти этапы, чтобы лучше разобраться, как работает DI и прочувствовать все эти нюансы на собственной практике. А тот, кто никогда подобным не занимался, пусть первый бросит в меня печенькой.

На самом деле, и это еще не все. Если проект, который вы переводите на DI — общая библиотека, есть смысл абстрагироваться и от самого DI, об этом в следующей статье.

Ах, да, чуть не забыл

May the —force be with you.

ссылка на оригинал статьи http://habrahabr.ru/post/217523/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *