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 の記事が分かりやすいかと思います。
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 の機能と 組み合わせることで、処理の分岐を綺麗にリファクタリング出来たのは個人的には大きな学びでした。
皆様のご参考になれば幸いです。