【Java】パラメタから実装クラスを選択する方法の検討
やりたいこと
次のような実装がしたい場面がありました。以下は簡単なクラスを例に挙げます。
- 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つ目の実装は以下を参考にしました。実装検討時になかなかいい方法が思いつかず、非常に助かりました。
Discussion