🌊

【Java】パラメタから実装クラスを選択する方法の検討

2023/08/09に公開

やりたいこと

次のような実装がしたい場面がありました。以下は簡単なクラスを例に挙げます。

  • IAnimalServiceインターフェイスがあり、これはbark()メソッドを持つ
  • 実装クラスとしてDogServiceとLionServiceがある
  • 任意のクラスに実装したprocess()メソッドは引数に適当な型を持つanimalTypeが与えられる
  • 例えばString animalTypeとし、animalType="lion"ならばLionServiceのbark()を呼ぶ
  • if分岐やcase文といった条件分岐で実装クラス選択はしたくない

いわゆるストラテジーパターンのような実装です。
特に最後の1行、これが今回やりたいことのある意味すべてです。
IAnimalServiceの実装クラスが増えて、仮に100種類になったら、100分岐のif文になります。どんどん継ぎ足すコードとなり、見栄えも保守性も落ちてしまいます。

実装方法を考える

私がいろいろ調べたりする中で、3つほど方法が考えられました。
結論、ClassPathScanningCandidateProviderを使用した3つめの実装を採用し、製作を進めました。

enumを利用する

  public void process(AnimalType animalType) {
    animalType.getAnimalServiceInstance().bark();
  }
public class DogService implements IAnimalService{
  @Override
  public void bark() {
    System.out.println("わおーん");
  }

※LionServiceは省略

public enum AnimalType {
  LION {
    public IAnimalService getAnimalServiceInstance() {
      return new LionService();
    }
  },
  DOG {
    public IAnimalService getAnimalServiceInstance() {
      return new DogService();
    }
  };
  public abstract IAnimalService getAnimalServiceInstance();
}

呼び出し元が非常にすっきりする半面、AnimalTypeが煩雑になるのでしたくないパターンです。
私の場合、AnimalTypeが非常に多いため、この実装は正直回避したかったです。AnimalTypeが少ないならありかもしれません。またSpringを使用していないので、柔軟性はある程度高いと思います。

getterを利用する方法

IAnimalServiceにgetAnimalType()を実装し、Map<AnimalType, IAnimalService>の組を作成するパターンです。
実装はすぐできますが、getterをいちいち全IAnimalServiceが保持するので、あまり好みではありませんでした。

@Service
public class DogService implements IAnimalService {
  @Override
  public void bark() {
    System.out.println("わおーん");
  }

  @Override
  public AnimalType getAnimalType() {
    return AnimalType.DOG;
  }
}

収集用に以下を実装し、適宜serviceMapからgetする。

Map<AnimalType, IAnimalService> serviceMap;
public void createMap(Set<IAnimalService> animalServiceSet) {
    serviceMap = new HashMap<>();
    animalServiceSet.forEach(
        service -> serviceMap.put(service.getAnimalType(), service)
    );
}

あとはfindAnimal(AnimalType)を実装し、serviceMapからgetするという流れで実装します。
何も考えずに実装するなら手っ取り早いですが、各Serviceクラスに謎getterが入るので今回は使わず。

アノテーションを付与したクラスを収集する

私はこちらで実装を行いました。ClassPathScanningCandidateProviderを使用した実装方法です。
まずはアノテーションを作成し、各種Serviceに付与します。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Animal {
  AnimalType value()
}

例えばAnimalMapというクラスを用意します。

@Component
public class AnimalMap {
  Map<AnimalType, Class<?>> serviceMap;

  public Map<AnimalType, Class<?>> serviceMap(Set<IAnimalService> animalServiceSet) {
    serviceMap = new HashMap<>();
    
    // Animalアノテーションを付与したクラスを収集
    ClassPathScanningCandidateComponentProvider provider
            = new ClassPathScanningCandidateComponentProvider(false);
    provider.addIncludeFilter(new AnnotationTypeFilter(Animal.class));
    Set<BeanDefinition> beanSet = provider.findCandidateComponents("com.hoge");
    
    // 収集したクラスをMapにまとめる
    beanSet.forEach(beanDefinition -> {
      Class<?> clazz = null;
      try {
        clazz = Class.forName(beanDefinition.getBeanClassName());
        Animal annotation = clazz.getAnnotation(Animal.class);
        // アノテーションのvalueとそのクラスの組を保持
        serviceMap.put(annotation.value(), clazz);
      } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
      }
    });
  }
}

あとはfindAnimal(AnimalType)を実装してserviceMap.get(AnimalType)するような実装をすればOKです。
私の場合は、findAnimalメソッドを実装する方法を使用しました。詳細は以下参考文献の1つ目が参考になるかと思います。BeanFactoryのインスタンスを使っています。

参考文献

3つ目の実装は以下を参考にしました。実装検討時になかなかいい方法が思いつかず、非常に助かりました。

https://future-architect.github.io/articles/20220729a/

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.html

Discussion