レガシープロジェクトの依存性注入への翻訳。 シスの道

また、 暗いプログラミングのトレンドにも貢献します。

あなたの多くは、プロジェクトでDIを使用するかどうかというジレンマに精通しています。

DIに切り替える理由: DIを使用しない理由:

大規模な作業ドラフトがある場合、DIに変換するという決定が下されたとします。 開発者は自分の可能性、血中のミディクロリアンのレベルを感じます。



私の若いパダワン、とげのある長い道があなたを待っています。



プロジェクトが大きく、多くの開発者がいる場合、1回のコミットで1つのリファクタリングが可能になることはほとんどありません。 したがって、いくつかの悪い慣行を使用して、移行を単純化し、それらを取り除きます。



どこから始めるか
DIには、コード内で構造的な枠を開くという素晴らしい機能があるため、準備作業を実行することは理にかなっています。 DIの概念では、すべてのクラスを条件付きで2つのカテゴリに分けることができます-それらをサービスとビンと呼びます。 前者は通常、コンテキスト内の単一のコピーに存在し、それに関連付けられています。 後者は処理されたデータ自体を保存し、サービスではなく他のBeanを参照できます。 時々、さまざまなバリエーションがあります:
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); } }
      
      









適切なJedi getMasterメソッドは完全に削除されるか、別のクラス(サービス)に転送されます。 その結果、Jediクラスは単なるデータビンになります。 何らかの理由でメソッドを転送できない場合(たとえば、リファクタリングに使用できないコードはそれに依存します)、非推奨として宣言し、今のところそのままにしておくことができます(オプションとして、Guava開発者が行うように、このメソッドが削除されるバージョンを宣言します) )

それでは、DBJediに対処しましょう。
 public class DBJedi { public static Jedi getJedi(long id) { DataSource dataSource = ConnectionPools.getDataSource("jedi"); Jedi jedi; // magic return jedi; } }
      
      



たとえば、次のように、そのようなクラスを古典的なシングルトンに作り直すことは論理的です。
 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への移行は標準ガイドを使用して行うことができます。

しかし、あなたがシスである場合、おそらくクラス(この例ではgetMasterメソッドを備えたJediクラス)があり、良い方法では標準的な方法で翻訳されていません。



ここで、DIをねじ込むことの妥当性について再度考える必要があります。 欲望がまだ残っている場合-続行します。

例は主にGuiceで、Springで部分的に複製されます。 フレームワークの選択に関しては、最もよく知っているものを選択してください。



悪い練習1-インジェクターへの静的リンクを維持する


ある時点で、質問は次のようになります-インジェクタインスタンスを取得して、シングルトーンを引き出す場所はどこですか? ユーティリティクラスを取得しましょう。
 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から別のDIに簡単に切り替えることができます。理想的な場合は、特定のフレームワークから完全に切り離されます(JSR-330互換性の対象)。 名前付き注釈では、spring-context.xmlでBeanエントリを作成できません。xml構成でこのエントリがまだ暗示されている場合は、注釈を削除する必要があります。



悪い練習2-Bean Factory


クラスがデータビンであるが、シングルトンオブジェクトに同時にアクセスする場合、ファクトリクラスを作成できます。
 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クラスの間に循環依存関係が形成されます。 ランタイムでこれらのオブジェクトを作成しようとすると、DIコンテナエラー(StackOverflowErrorなど)が発生します。 プロバイダーインターフェイスが助けになります。
 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); } }
      
      



Reflectionを介して一般的な宣言を使用できないことに注意してください。 GuiceとSpringについては、両方ともクラスのバイトコードを読み取り、ジェネリック型を取得します。



テストを書く


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")); } } }
      
      





結果は何ですか
そして、最終的に、2つの可能な結果があります。 最初はそこで停止することです。 これは私のプロジェクトの1つで起こりました。正直なDIに完全に変換することはできませんでした。多くのレガシーコードがありました。 この状況は多くの人によく知られていると思います。 たとえば、InjectorUtilの静的フィールドをThreadLocalに置き換えて、同じ静的空間内の異なる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に翻訳しているプロジェクトが共有ライブラリである場合、DI自体から抽象化することは理にかなっていますが、これは別の投稿のトピックです。



最後まで読んだ人へ






-力があなたと共にありますように。



All Articles