📜 ⬆️ ⬇️

Translation of legacy project to Dependency Injection. Sith Way

I will also contribute to the trend of dark programming .
Many of you are familiar with the dilemma: whether to use DI in your project or not.
Reasons for switching to DI:Arguments not to use DI:
Suppose we have a large working draft, the decision was made: transfer to DI. Developers feel their potential level midichlorian in the blood rolls over.

The path is waiting for you thorny and long, my young Padawan.

If the project is large and there are a lot of developers, it is unlikely to be able to do this refactoring with one commit. Therefore, we use several bad practices, simplifying the transition, and then get rid of them.

Where to begin
DI has a remarkable feature to open architectural jambs in the code, so it makes sense to carry out the preparatory work. In the concept of DI, all classes can be conditionally divided into two categories - let's call them services and bins. The first exist, as a rule, in a single copy within the context and are attached to it. The second ones store the processed data themselves and can refer to other bins, but not to services. Sometimes there are mixed variations:
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); } } 



The decent Jedi getMaster method will remove it altogether or transfer it to another class (service). As a result, the Jedi class will become just a bin with data. If the transfer of the method for any reason is now impossible (for example, code that cannot be refactored depends on it), you can declare it deprecated and leave something for now (alternatively, declare the version in which this method will be removed, as the Guava developers do) ).
Now let's deal with DBJedi:
 public class DBJedi { public static Jedi getJedi(long id) { DataSource dataSource = ConnectionPools.getDataSource("jedi"); Jedi jedi; // magic return jedi; } } 
It is logical to convert such a class into a classic singleton, for example, like this:
 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; } } 
As a result, we get a more coherent and readable code structure (a very controversial fact, of course). If you finish what has been started, in general, the transition to DI can be done using standard guidelines.
But if you are a Sith, then surely there are classes left (in our example, the Jedi class with the getMaster method), which are not translated in a standard way.
')
Now you need to think again about the advisability of screwing DI. If the desire is still left - continue.
Examples will be mainly on Guice, partially duplicated on Spring. As for choosing a framework, choose the one you know best.

Bad practice 1 - save static link to Injector

At some point, the question arises - where to get the injector instance to pull out the singletones? Let's get a utility class:
 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; } } 
For Spring, the code will be similar, but instead of Injector, the ApplicationContext. Or another option:
Perfectionist nightmare
 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; } } 

Now our singletons can be rewritten like this:
 @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; } } 
Please note that the JSR-330 annotations are used, the javax.inject package. Using them, you can later more easily switch from one DI to another, ideally abstracting from a specific framework altogether (assuming JSR-330 compatibility). The Named annotation will allow not to record the bean in the spring-context.xml, if the xml configuration still implies such an entry, the annotation should be removed.

Bad Practice 2 - Bean Factory

If the class is a bean with data, but at the same time refers to singleton objects, you can make a factory class:
 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); } } } 

Bad practice 3 - cyclic dependencies

In our example, a cyclical relationship is formed between the DBJedi and Jedi.Factory classes. When we try to create these objects at runtime, we get a DI-container error, for example, StackOverflowError. Here the Provider interface comes to the rescue:
 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); } } 
It is true to note that generic declarations are not available through Reflection. As for Guice and Spring, they both read the bytecode of the class and thus get a generic type.

We write tests

There is a great Guice annotation in testng that simplifies code testing. For Spring - the artifact org.springframework: spring-test.
Let's make a test for our classes:
 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")); } } } 

What is the result
As a result, we have two possible outcomes. The first is to stay on top. It happened in one of my projects, it was not possible to translate it entirely into an honest DI, there was a lot of legacy code in it. I think this situation is familiar to many. You can improve it a bit, for example, replacing the static field in InjectorUtil with ThreadLocal, thus solving the problem of concurrent testing with different DI-environments in the same static space.
Read more
 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; } } 
The second is to finish the job. In our example, we’ll first get rid of the Jedi.getMaster method, then Jedi will turn into a simple bean. After that, remove the class Jedi.Factory. Cyclic dependence will also disappear. As a result, the InjectorUtil class itself will not be. Projects without such a class are a reality. It is not necessary to go through all these stages, but let me remind you that we are talking about the situation of the legacy project, in the new project such a problem can be avoided from the very beginning.



In fact, this is not all. If the project that you are translating to DI is a general library, it makes sense to abstract from the DI itself, but this is already a topic for a separate post.

Those who read to the end

May the --force be with you.

Source: https://habr.com/ru/post/217523/


All Articles