【ITニュース解説】🧠 Analyzing SOLID Principles in an Epsilon-Greedy Recommender (Java)
2025年09月24日に「Dev.to」が公開したITニュース「🧠 Analyzing SOLID Principles in an Epsilon-Greedy Recommender (Java)」について初心者にもわかりやすく解説しています。
ITニュース概要
Javaの推薦システムを例に、良いプログラム設計の「SOLID原則」を解説する。既存のコードは複数の役割を持ち、機能追加やテストが難しかった。これを役割ごとに分離し、外部から設定可能に改善する。その結果、保守性、拡張性、テスト性の高いコードを実現できる。
ITニュース解説
このニュース記事は、Javaで書かれた「Epsilon-Greedy Recommender」という推薦システムの簡単なプログラムを例に、より良いソフトウェア設計のための「SOLID原則」という五つの指針について解説し、実際にコードを改善する(リファクタリングする)方法を紹介している。
まず、記事で提示されている最初のプログラム「EpsilonGreedyRecommender」クラスについて説明する。このクラスは、ユーザーにアイテム(商品など)を推薦し、その推薦が成功したか失敗したか(報酬)に応じて、次回以降の推薦を改善するための学習を行う。具体的には、どのアイテムが何回選ばれたか(counts)や、各アイテムが平均してどれくらいの報酬をもたらしたか(values)といった統計データを保持し、このデータに基づいて次に推薦するアイテムを選び(recommendメソッド)、報酬を受け取ると統計データを更新する(updateメソッド)という一連の処理を全てこのクラス一つで担当していた。
次に、この元のプログラムがSOLID原則にどの程度従っているかを分析していく。SOLID原則とは、変更に強く、拡張しやすい、保守しやすいプログラムを作るための設計の原則である。
一つ目の「SRP(単一責任の原則)」は、「一つのクラスは、変更される理由が一つだけであるべき、つまり一つの責任だけを持つべき」という原則である。元のEpsilonGreedyRecommenderクラスは、アイテムの統計データを保存する、どのアイテムを推薦するかというロジックを実装する、そして受け取った報酬に基づいて統計データを更新するという、複数の異なる責任を持っていた。そのため、推薦ロジックが変わる場合でも、データの保存方法が変わる場合でも、同じクラスを修正する必要があり、SRPに部分的に違反していたと判断された。
二つ目の「OCP(オープン/クローズドの原則)」は、「クラスは拡張に対して開かれていて、変更に対して閉じているべき」という原則である。これは、新しい機能を追加したい場合に、既存のコードを直接修正するのではなく、コードを拡張する形で対応できるべき、という意味である。元のプログラムでは、もしEpsilon-Greedyとは異なる新しい推薦ロジック(例えば、SoftmaxやUCBといった別のアルゴリズム)を使いたいと考えた場合、recommendメソッドの中身を直接書き換える必要があった。これは既存のコードを変更しているため、OCPに違反していると判断された。
三つ目の「LSP(リスコフの置換原則)」は、「サブタイプ(子クラス)は、そのベースタイプ(親クラス)の代わりに使われても、プログラムの正しさを変えてはならない」という原則である。この元のプログラムには継承関係が一切なかったため、この原則は違反していないと判断された。
四つ目の「ISP(インターフェース分離の原則)」は、「クライアント(クラスを利用する側)は、自分が使わないメソッドが定義されている大きなインターフェースに依存すべきではない」という原則である。元のプログラムではインターフェースが全く使われていなかったため、この原則も違反していないと判断された。
五つ目の「DIP(依存関係逆転の原則)」は、「具体的な実装ではなく、抽象的なもの(インターフェースなど)に依存すべき」という原則である。元のEpsilonGreedyRecommenderクラスは、乱数を生成するためにjava.util.Randomという特定のクラスのインスタンスを内部で直接作成して使用していた。これは、抽象的な概念(例えばRandomのインターフェース)ではなく、具体的な実装に直接依存している状態である。これにより、テストを行う際に予測可能な乱数を注入することが難しく、テストがしにくいコードになっていたため、DIPに違反していると判断された。
これらのSOLID原則の違反を解消し、より良いコードにするために、記事ではリファクタリング後のコードが提示されている。
まず、推薦ロジックの共通の形を定義するために、「SelectionPolicy」というインターフェースが導入された。このインターフェースはselectというメソッドを持ち、アイテムの値を引数として受け取り、次に選ぶべきアイテムのインデックスを返す、というシンプルな役割を持つ。
次に、元のEpsilon-Greedyの推薦ロジックは、「EpsilonGreedyPolicy」という新しいクラスとして独立して実装された。このクラスはSelectionPolicyインターフェースを実装しており、Epsilon-Greedyという特定の推薦ロジックのみに責任を持つ。さらに、このクラスではRandomインスタンスを内部で直接生成するのではなく、コンストラクタを通して外部から受け取る(注入される)ように変更された。これにより、DIPの原則が満たされ、テスト時に特定の乱数を発生させるモックオブジェクトを渡せるようになり、テストが容易になった。
最後に、元のEpsilonGreedyRecommenderクラスは「Bandit」という新しい名前に変更され、その役割も大きく変わった。このBanditクラスは、アイテムごとの統計データ(countsとvalues)を管理することだけに責任を持つようになった。そして、推薦ロジック自体は、コンストラクタで受け取ったSelectionPolicyインターフェースを実装するオブジェクトに委ねる形になった。recommendメソッドが呼び出されると、Banditクラスは自身が持つアイテムのvaluesデータをpolicy.selectメソッドに渡し、その結果を受け取る。updateメソッドは引き続き統計データを更新する役割を持つ。
このリファクタリングによって、SOLID原則がどのように満たされたかを説明する。
- SRPが尊重された。
Banditクラスは状態管理という単一の責任を持ち、EpsilonGreedyPolicyクラスは推薦ロジックという単一の責任を持つようになった。 - OCPが尊重された。新しい推薦ロジックを追加したい場合でも、
SelectionPolicyインターフェースを実装する新しいクラスを作成するだけでよく、Banditクラスのコードを一切変更する必要がなくなった。 - DIPが尊重された。
RandomインスタンスやSelectionPolicyの実装が外部から注入されるようになったことで、具体的な実装に依存するのではなく、抽象的なインターフェースに依存する設計となり、プログラムのテストが格段に容易になった。
このように、SOLID原則を適用することで、プログラムは将来の変更に対して柔軟になり、新しい機能の追加が容易になり、そしてより効率的にテストできる、高品質で保守しやすいコードへと改善されるのである。この例は、たとえ小さなクラスであっても、アルゴリズムが将来的に変更される可能性がある場合には、SOLID原則を適用することが非常に有効であることを示している。