オタクタイム-デザインパターンの美しさファクトリーパターン(パート2):依存関係インジェクションフレームワークをデザインおよび実装する方法は?

オブジェクトの作成が「大きなプロジェクト」である場合、通常、ファクトリパターンを使用して、オブジェクトの複雑な作成プロセスをカプセル化し、オブジェクトの作成と使用を分離し、コードをより明確にすることを選択します。では、「大きなプロジェクト」とは何ですか?前回のレッスンでは、2つの状況について説明しました。1つは、作成プロセスに複雑なif-elseブランチの判断が含まれること、もう1つは、オブジェクトの作成に他のタイプの複数のオブジェクトのアセンブリまたは複雑な初期化プロセスが必要なことです。

オブジェクト、依存関係インジェクションフレームワーク、または依存関係インジェクションコンテナ、または略してDIコンテナを作成するための「大きなプロジェクト」について話しましょう。今日の説明では、次の質問を明確にします。DIコンテナと先ほど説明したファクトリモデルの違いと関係は何ですか。DIコンテナのコア機能と、単純なDIコンテナを実装する方法は何ですか?

ファクトリモードとDIコンテナの違いは何ですか?

実際、DIコンテナの下部にある最も基本的な設計アイデアは、工場のパターンに基づいています。DIコンテナは、プログラムの起動時に構成(作成するクラスオブジェクトとクラスオブジェクトごとに作成する必要がある他のクラスオブジェクト)に応じてオブジェクトを事前に作成する大規模なファクトリクラスに相当します。アプリケーションが特定のクラスオブジェクトを使用する必要がある場合、それはコンテナから直接取得できます。このフレームワークが「コンテナ」と呼ばれるのは、オブジェクトの束を保持しているからです。

前のレッスンで説明したファクトリパターンの例と比較すると、DIコンテナはより大きなオブジェクト作成プロジェクトを処理します。前のレッスンで説明したファクトリモデルでは、ファクトリクラスは、特定のクラスオブジェクトまたは関連するクラスオブジェクトの特定のグループ(同じ抽象クラスまたはインターフェイスサブクラスから継承)の作成のみを担当し、DIコンテナはアプリケーション全体のすべてのアプリケーションを担当します。クラスオブジェクトの作成。

さらに、DIコンテナは、単純なファクトリモデル以上の役割を果たします。たとえば、構成分析やオブジェクトのライフサイクル管理も含まれます。次に、単純なDIコンテナに含める必要のあるコア機能について詳しく説明します。

DIコンテナのコア機能は何ですか?

要約すると、単純なDIコンテナには、一般に、構成分析、オブジェクト作成、およびオブジェクトライフサイクル管理の3つのコア機能があります。

まず、構成分析を見てみましょう。

前のレッスンで説明したファクトリパターンでは、ファクトリクラスが作成するクラスオブジェクトは事前に決定され、ファクトリクラスコードにハードコードされています。一般的なフレームワークとして、フレームワークコードとアプリケーションコードは高度に分離する必要があります。DIコンテナは、アプリケーションが作成するオブジェクトを事前に認識しておらず、アプリケーションによって作成されたオブジェクトをフレームワークコードに書き込むことはできません。したがって、作成するオブジェクトをDIコンテナに指示するアプリケーションのフォームが必要です。このフォームは、これから説明する構成です。

DIコンテナによって作成されたクラスオブジェクトと、クラスオブジェクトを作成するために必要な情報(使用されるコンストラクタと対応するコンストラクタパラメータなど)を構成ファイルに作成する必要があります。コンテナは構成ファイルを読み取り、構成ファイルによって提供された情報に基づいてオブジェクトを作成します。

以下は、典型的なSpringコンテナ構成ファイルです。Springコンテナは、この構成ファイルを読み取り、作成される2つのオブジェクト(rateLimiterとredisCounter)を解析し、2つの間の依存関係を取得します。rateLimiterはredisCounterに依存します。


public class RateLimiter {
    
    
  private RedisCounter redisCounter;
  public RateLimiter(RedisCounter redisCounter) {
    
    
    this.redisCounter = redisCounter;
  }
  public void test() {
    
    
    System.out.println("Hello World!");
  }
  //...
}

public class RedisCounter {
    
    
  private String ipAddress;
  private int port;
  public RedisCounter(String ipAddress, int port) {
    
    
    this.ipAddress = ipAddress;
    this.port = port;
  }
  //...
}

配置文件beans.xml:
<beans>
   <bean id="rateLimiter" class="com.xzg.RateLimiter">
      <constructor-arg ref="redisCounter"/>
   </bean>
 
   <bean id="redisCounter" class="com.xzg.redisCounter">
     <constructor-arg type="String" value="127.0.0.1">
     <constructor-arg type="int" value=1234>
   </bean>
</beans>

次に、オブジェクトの作成を見てみましょう

DIコンテナでは、クラスごとにファクトリクラスを作成すると、プロジェクト内のクラスの数が指数関数的に増加し、コードのメンテナンスコストが増加します。この問題を解決することは難しくありません。BeansFactoryなどのファクトリクラスにすべてのクラスオブジェクトを作成するだけで済みます。

作成するクラスオブジェクトがたくさんある場合、BeansFactoryのコードは直線的に拡張されますか(コードの量は作成されたオブジェクトの数に比例します)?実際にはありません。DIコンテナの特定の実装について説明するときは、事前にコードを書き留めることなく、プログラムの実行中にクラスを動的にロードしてオブジェクトを作成できる「リフレクション」メカニズムについて説明します。オブジェクト。したがって、1つのオブジェクトを作成する場合でも10個のオブジェクトを作成する場合でも、BeansFactoryファクトリクラスコードは同じです。

最後に、オブジェクトのライフサイクル管理を見てみましょう。

単純なファクトリパターンを実装する方法は2つあります。1つは毎回新しく作成されたオブジェクトを返す方法、もう1つは毎回同じ事前に作成されたオブジェクト(いわゆるシングルトンオブジェクト)を返す方法です。Springフレームワークでは、scopeプロパティを構成することにより、これら2つの異なるタイプのオブジェクトを区別できます。scope = protocolは、新しく作成されたオブジェクトを返すことを意味し、scope = singletonは、シングルトンオブジェクトを返すことを意味します。

さらに、オブジェクトがレイジーロードをサポートするかどうかを構成することもできます。lazy-init = trueの場合、オブジェクトは実際に使用されたときに作成されます(例:BeansFactory.getBean( "userService"))。lazy-init = falseの場合、オブジェクトはアプリケーションの起動時に事前に作成されます。

それだけでなく、init-method = loadProperties()、destroy-method = updateConfigFile()など、オブジェクトのinit-methodメソッドとdestroy-methodメソッドを構成することもできます。DIコンテナはオブジェクトを作成した後、init-method属性で指定されたメソッドをアクティブに呼び出して、オブジェクトを初期化します。オブジェクトが最終的に破棄される前に、DIコンテナーは、destroy-method属性で指定されたメソッドをアクティブに呼び出して、データベース接続プールの解放やファイルのクローズなどのクリーンアップ作業を実行します。

単純なDIコンテナを実装する方法は?

実際、Java言語で単純なDIコンテナを実装するには、コアロジックに、構成ファイルの解析と、「リフレクション」構文による構成ファイルに基づくオブジェクトの作成の2つの部分を含めるだけで済みます。

1.最小限のプロトタイプ設計

主に設計パターンを説明しているため、本日の説明では、DIコンテナの最小限のプロトタイプのみを実装します。SpringフレームワークなどのDIコンテナでサポートされる構成形式は、非常に柔軟で複雑です。コードの実装を簡素化し、原則に焦点を合わせるために、最小限のプロトタイプでは、次の構成ファイルに含まれる構成構文のみをサポートします。


配置文件beans.xml
<beans>
   <bean id="rateLimiter" class="com.xzg.RateLimiter">
      <constructor-arg ref="redisCounter"/>
   </bean>
 
   <bean id="redisCounter" class="com.xzg.redisCounter" scope="singleton" lazy-init="true">
     <constructor-arg type="String" value="127.0.0.1">
     <constructor-arg type="int" value=1234>
   </bean>
</bean

最小限のプロトタイプの使用は、Springフレームワークと非常によく似ています。サンプルコードは次のとおりです。


public class Demo {
    
    
  public static void main(String[] args) {
    
    
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
            "beans.xml");
    RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
    rateLimiter.test();
    //...
  }
}
2.実行エントリを提供します

前述したように、オブジェクト指向の設計の最後のステップは、クラスをアセンブルし、実行するためのエントリポイントを提供することです。ここで、実行エントリは、外部使用に公開されるインターフェイスとクラスのセットです。

最小プロトタイプのサンプルコードを使用すると、実行エントリがApplicationContextとClassPathXmlApplicationContextの2つの部分で構成されていることがわかります。その中で、ApplicationContextはインターフェースであり、ClassPathXmlApplicationContextはインターフェースの実装クラスです。2つのクラスの具体的な実装は次のとおりです。


public interface ApplicationContext {
    
    
  Object getBean(String beanId);
}

public class ClassPathXmlApplicationContext implements ApplicationContext {
    
    
  private BeansFactory beansFactory;
  private BeanConfigParser beanConfigParser;

  public ClassPathXmlApplicationContext(String configLocation) {
    
    
    this.beansFactory = new BeansFactory();
    this.beanConfigParser = new XmlBeanConfigParser();
    loadBeanDefinitions(configLocation);
  }

  private void loadBeanDefinitions(String configLocation) {
    
    
    InputStream in = null;
    try {
    
    
      in = this.getClass().getResourceAsStream("/" + configLocation);
      if (in == null) {
    
    
        throw new RuntimeException("Can not find config file: " + configLocation);
      }
      List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
      beansFactory.addBeanDefinitions(beanDefinitions);
    } finally {
    
    
      if (in != null) {
    
    
        try {
    
    
          in.close();
        } catch (IOException e) {
    
    
          // TODO: log error
        }
      }
    }
  }

  @Override
  public Object getBean(String beanId) {
    
    
    return beansFactory.getBean(beanId);
  }
}

上記のコードから、ClassPathXmlApplicationContextがBeansFactoryとBeanConfigParserの2つのクラスをアセンブルし、実行プロセスをシリアル化する役割を果たしていることがわかります。クラスパスからXML形式で構成ファイルをロードし、BeanConfigParserを介して統一されたBeanDefinition形式に解析し、次にBeanDefinitionに従ってBeansFactoryを解析します。オブジェクトを作成します。

3.構成ファイルの分析

構成ファイルの分析には、主にBeanConfigParserインターフェイスとXmlBeanConfigParser実装クラスが含まれます。XmlBeanConfigParser実装クラスは、構成ファイルをBeanDefinition構造に解析して、BeansFactoryがこの構造に基づいてオブジェクトを作成できるようにします。

構成ファイルの分析は比較的面倒であり、このコラムで説明する理論的な知識は含まれず、説明の焦点でもありません。そこで、ここでは、特定の実装コードを指定せずに、2つのクラスの一般的な設計アイデアのみを示します。興味があれば、自分で完成させることができます。具体的なコードフレームワークは次のとおりです。


public interface BeanConfigParser {
    
    
  List<BeanDefinition> parse(InputStream inputStream);
  List<BeanDefinition> parse(String configContent);
}

public class XmlBeanConfigParser implements BeanConfigParser {
    
    

  @Override
  public List<BeanDefinition> parse(InputStream inputStream) {
    
    
    String content = null;
    // TODO:...
    return parse(content);
  }

  @Override
  public List<BeanDefinition> parse(String configContent) {
    
    
    List<BeanDefinition> beanDefinitions = new ArrayList<>();
    // TODO:...
    return beanDefinitions;
  }

}

public class BeanDefinition {
    
    
  private String id;
  private String className;
  private List<ConstructorArg> constructorArgs = new ArrayList<>();
  private Scope scope = Scope.SINGLETON;
  private boolean lazyInit = false;
  // 省略必要的getter/setter/constructors
 
  public boolean isSingleton() {
    
    
    return scope.equals(Scope.SINGLETON);
  }


  public static enum Scope {
    
    
    SINGLETON,
    PROTOTYPE
  }
  
  public static class ConstructorArg {
    
    
    private boolean isRef;
    private Class type;
    private Object arg;
    // 省略必要的getter/setter/constructors
  }
}
4.コアファクトリーデザイン

最後に、BeansFactoryがどのように設計および実装されているかを見てみましょう。これは、DIコンテナのコアクラスでもあります。構成ファイルから解析されたBeanDefinitionに基づいてオブジェクトを作成します。

オブジェクトのスコープ属性がシングルトンの場合、オブジェクトは作成後にsingletonObjectsのマップにキャッシュされます。次にこのオブジェクトが要求されると、オブジェクトは再作成されずにマップから直接取得されて返されます。オブジェクトのscopeプロパティがプロトタイプの場合、オブジェクトが要求されるたびに、BeansFactoryは新しいオブジェクトを作成して返します。

実際、BeansFactoryがオブジェクトを作成するために使用する主な技術的ポイントは、Javaのリフレクション構文です。これは、クラスを動的にロードしてオブジェクトを作成するためのメカニズムです。JVMは、起動時にコードに基づいてクラスを自動的にロードし、オブジェクトを作成することがわかっています。ロードするクラスと作成するオブジェクトについては、これらはすべてコードにハードコードされているか、事前に記述されています。ただし、オブジェクトの作成がコードに記述されておらず、構成ファイルに配置されている場合、プログラムの実行中にクラスを動的にロードし、構成ファイルに従ってオブジェクトを作成する必要があります。そうすると、作業のこの部分は機能しません。 JVMに自動的に実行させてください。コードを自分で作成するには、Javaが提供するリフレクション構文を使用する必要があります。

リフレクションの原理を知っているので、BeansFactoryのコードを理解するのは難しくありません。具体的なコードの実装は次のとおりです。


public class BeansFactory {
    
    
  private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
  private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();

  public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
    
    
    for (BeanDefinition beanDefinition : beanDefinitionList) {
    
    
      this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
    }

    for (BeanDefinition beanDefinition : beanDefinitionList) {
    
    
      if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) {
    
    
        createBean(beanDefinition);
      }
    }
  }

  public Object getBean(String beanId) {
    
    
    BeanDefinition beanDefinition = beanDefinitions.get(beanId);
    if (beanDefinition == null) {
    
    
      throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
    }
    return createBean(beanDefinition);
  }

  @VisibleForTesting
  protected Object createBean(BeanDefinition beanDefinition) {
    
    
    if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinition.getId())) {
    
    
      return singletonObjects.get(beanDefinition.getId());
    }

    Object bean = null;
    try {
    
    
      Class beanClass = Class.forName(beanDefinition.getClassName());
      List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgs();
      if (args.isEmpty()) {
    
    
        bean = beanClass.newInstance();
      } else {
    
    
        Class[] argClasses = new Class[args.size()];
        Object[] argObjects = new Object[args.size()];
        for (int i = 0; i < args.size(); ++i) {
    
    
          BeanDefinition.ConstructorArg arg = args.get(i);
          if (!arg.getIsRef()) {
    
    
            argClasses[i] = arg.getType();
            argObjects[i] = arg.getArg();
          } else {
    
    
            BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
            if (refBeanDefinition == null) {
    
    
              throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());
            }
            argClasses[i] = Class.forName(refBeanDefinition.getClassName());
            argObjects[i] = createBean(refBeanDefinition);
          }
        }
        bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
      }
    } catch (ClassNotFoundException | IllegalAccessException
            | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
    
    
      throw new BeanCreationFailureException("", e);
    }

    if (bean != null && beanDefinition.isSingleton()) {
    
    
      singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
      return singletonObjects.get(beanDefinition.getId());
    }
    return bean;
  }
}

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/110475682
おすすめ