msawady’s tech-note

フルスタックエンジニアの学んだことや考えていること

【Java】【Spring】interface を継承するクラスを @Autowired を利用して一括で DI する ~そして Strategy パターン へ~

Spring の Autowired の便利な使い方と Strategy パターンへの応用

  • 最近、仕事で久しぶりに Java を使うようになりました
  • リファクタリングタスクの中で Autowiredの便利な使い方を知りました
  • Strategy パターンと組み合わせて綺麗にリファクタ出来たのでブログにします

interface を継承するクラスを一括で Autowired する

こちらの記事を参考にします。記事ではある具象クラスを継承するサブクラスを一括でDIしていますが、interface でも同様のことが出来ます。

https://dzone.com/articles/load-inheritance-tree-list

まずはベースとなる interface を定義します。

public interface Strategy {

    boolean needAnalyze(Situation situation);

    void analyze(Situation situation);
}

この interface を実装する具象クラスを作り、@Componentアノテーションを付与します。

@Component
public class AgainstTrendStrategy implements Strategy {

    @Override
    public boolean needAnalyze(Situation situation) {
        return !situation.isNeutral();
    }

    @Override
    public void analyze(Situation situation) {
        System.out.print("Analysis by AgainstTrendStrategy: ");
        System.out.println(situation.getTrend().equals("up") ? "Lets Sell!" : "Lets Buy!");
    }
}

そして、この Strategy interface を実装する具象クラスを一括で取得するためには、 以下のように List<Strategy> strategies と interface の Listを宣言して @Autowired アノテーションを付与することで、DIすることが出来ます。 (List でなくとも、Collection を継承するクラスであればOKです。Setも使えます)

    private List<Strategy> strategies;

    public Analyze(@Autowired List<Strategy> strategies) {
        this.strategies = strategies;
    }

    public void doAnalyze(Situation situation) {
        strategies.stream()
                .filter(s -> s.needAnalyze(situation))
                .forEach(s -> s.analyze(situation));
    }

ちなみに、自分はコンストラクタでDIする派です。テストしやすいので。

Strategy パターンへの応用

これが出来ると何が嬉しいかというと、デザインパターンの Strategy パターンと非常に相性が良いのです。 「Strategy パターン is 何?」はこちらの TechScore の記事が分かりやすいかと思います。

www.techscore.com

Before

このパターンを利用してリファクタリングする前は、以下のようなコードでした。

public class YabaiService {

    @Autowired
    HogeRepository hogeRepository;

    @Autowired
    FugaRepository fugaRepository;

// 以下大量の依存する repository やサービス....

   public List<AnalyzeResult> doAnalyze(Situation situation) {

        List<AnalyzeResult> results = Lists.newArrayList();

        if(situation.isConditionA()){
            if(situation.isConditionB()){
                results.append(doHogeAnalysis(situation));
                if(situation.isConditionC()) {
                    results.append(doPiyoAnalysis(situation));
                } else {
                    results.append(doPiyoPiyoAnalysis(situation));
                }
            }else if(situation.isConditionD()){
                results.append(doHogeHogeAnalysis());
                // 以下、鬼のような条件分岐と、それぞれの条件でコールされるメソッドが並ぶ...

Enum を利用した switch-case なども有り、ドン引きレベルで複雑な作りになっていました。(doXxxAnalysisメソッドが 30個くらいありました...)
問題点は山ほど有りますが、メインの問題は、

  • コールするメソッドを選択する条件分岐が複雑かつ、長大
  • コールされたメソッドが利用するための依存クラスが大量にあるためテストしにくい。
    • 各々のメソッドでは利用するのは 2〜3クラス だが、全体では大量にある

ということでした。

今回のパターンを利用して、以下のように変更しました。

  • 条件分岐のロジックを各 Strategy のサブクラスで実装する
    • 各々のサブクラスが必要な分だけ、依存するクラスを DI する
  • エントリーポイントとなる doAnalyze メソッドでは、サブクラスの中から条件に合う Strategyを集めて結果を返すようにする
    private List<Strategy> strategies;

    public Analyze(@Autowired List<Strategy> strategies) {
        this.strategies = strategies;
    }

    public List<AnalyzeResult> doAnalyze(Situation situation) {
        strategies.stream()
                .filter(s -> s.needAnalyze(situation))
                .map(s -> s.analyze(situation))
                .collect(Collectors.toList());
    }

これにより、エントリーポイントのdoAnalyzeメソッドがすっきりするだけでなく、

  • Strategy の追加をしたい場合は、具象クラスを実装するだけで良い
    • doAnalyzeメソッドの修正が不要になる
  • Strategy の各サブクラスで needAnalyzeメソッドが実装されているため、analyzeが実行される条件が分かりやすい
  • Strategy の各サブクラスがDIする依存クラスは必要最小限になるため、テストが書きやすい

といったメリットがあります。

他にも...

「処理の分岐」を Strategy パターンを利用して嬉しくなるシーンは多く、汎用的なパターンかと思います。 たとえば、Enum ごとに処理が分岐するパターン(e.g 出力する帳票によってファイル形式が違う)などでも、 このパターンを利用したリファクタリングで分かりやすい構造に出来ます。

終わりに

Strategy パターン自体は知っていましたが、「interface を継承するクラスを一括で取得できる」Spring の機能と 組み合わせることで、処理の分岐を綺麗にリファクタリング出来たのは個人的には大きな学びでした。

皆様のご参考になれば幸いです。