Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 2.06.2014 | нет комментариев
Внесу и свой взнос в тренд темного программирования.
Многим из вас знакома дилемма: применять ли 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"));
        }
    }
}
А потом-то что?

А потом у нас допустимы два исхода. 1-й — остановиться на достигнутом. Так случилось в одном из моих планов, целиком его перевести на Добросовестный 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;
    }
}

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

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

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

May the –force be with you.

 Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB