Многим из вас знакома дилемма: использовать ли 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/
Добавить комментарий