[技術講座]
(株)オージス総研
福田 直樹
連載の最終回となる今回は、アルゴリズムをクラスとして抽出する Strategy パターンを検討します。変動しやすい一連の処理を持つ部分を分離し、それを簡単に切り替えて実行できるような仕組みを構築します。Strategy パターンを検討した後、その Strategy パターンに見られる設計指針を確認してデザインパターン活用のポイントを振り返ることにしましょう。
※雑誌『Java WORLD』 2006 年 9 月号に掲載した記事のオリジナル原稿を Java WORLD 編集部の了解を得て掲載しています。
前回は、GUI クラスと GUI に依存しないデータ処理クラスを分離し、リアルタイムに情報を更新するような仕組みを提供する Observer パターンを取り上げました。
Observer パターンは、適用した際のクラスやメソッドが多く、シーケンスが少し複雑になっているため、取っ付きにくい印象を受けたかもしれません。他のデザインパターンでも同様に複雑そうに見えるパターンがあります。そういうものこそ、特に解決する問題、適用の目的、メリットを押さえながら、1 つ 1 つじっくりと理解してみてください。
X 社では、アンケート調査を実施しており、そのデータを使って現状分析や品質評価のチェックをしています。今回取り上げる部分は、様々に存在するセミナーの品質評価機能の部分となります。X 社では、全社単位、部署単位、チーム単位などで複数の品質評価基準が設けられているようです。(図 1)その品質評価基準として決められているアルゴリズムを自由に切り替えて動作させる仕組みを検討していきます。
![]() |
図 1 :様々な品質評価基準が存在する |
C さん 今回取り上げる処理は、セミナー毎にチェックする品質評価機能の部分だね。最近は、品質に対する取り組みを全社をあげて力を入れてるから、いろいろと品質基準が出てきているみたいだね。
A 君 そうですね。品質基準文書がよくまわってきますね。今年のうちのチームの品質基準はさらに厳しくなってるんですよ。明らかに無理な基準なんかも出てるんです。困ったもんです。
C さん そういう状況の部署もよく耳にするね。。ま、気を取り直して、今回の機能に該当する部分のソース・コードを見てみようか。
現状のソース・コードをリスト 1 に、クラス図は図 2 に示しています。SeminarList クラスは、Seminar オブジェクトを管理し、それらに対する処理を持っています。今回焦点を当てる部分は、品質評価アルゴリズムが実装されている getNeedImproveSeminars メソッド(品質基準を満たさないセミナーのリストを取得するメソッド)です。
リスト 1 : 既存の SeminarList クラス |
public class SeminarList { private static final int AVERAGE_ALGORITHM = 1; private static final int INSTRUCTOR_ALGORITHM = 2; private static final String INSTRUCTOR_SKILL_ID = "3"; private static final String INSTRUCTOR_PRESEN_ID = "4"; private static final double STANDARD_VALUE = 3.9; private static final double INSTRUCTOR_SKILL_VALUE = 4.2; private static final double INSTRUCTOR_PRESEN_VALUE = 4.0; private List seminars; // 適用するアルゴリズムのタイプ private int algorithmType; …略… // 品質を満たさないセミナーのリストを返却する。 public List getNeedImproveSeminars() { List ngSeminars = new ArrayList(); Iterator iterator = seminars.iterator(); while(iterator.hasNext()) { Seminar seminar = (Seminar) iterator.next(); int quality = QualityConstants.INVALID; // 全社品質基準の場合 if(algorithmType == AVERAGE_ALGORITHM) { List evaluations = seminar.getEvaluations(); Iterator eIterator = evaluations.iterator(); double total = 0.0; int i = 0; // 平均値を算出する while(eIterator.hasNext()) { Evaluation evaluation = (Evaluation) eIterator.next(); total += evaluation.getValue(); i++; } double average = (double) total / i; // 小数点第 2 位で四捨五入する BigDecimal bd = new BigDecimal(String.valueOf(average)); double data = bd.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); // 全社基準値と実データを比較する if(STANDARD_VALUE <= data) { quality = QualityConstants.ACCEPTABLE; } else { quality = QualityConstants.UNACCEPTABLE; } // チーム品質基準の場合 } else if(algorithmType == INSTRUCTOR_ALGORITHM) { List evaluations = seminar.getEvaluations(); Iterator eIterator = evaluations.iterator(); // 講師評価の2項目についてチーム基準値と実データを比較する while(eIterator.hasNext()) { Evaluation evaluation = (Evaluation) eIterator.next(); double data = 0; if(evaluation.getId().equals(INSTRUCTOR_SKILL_ID)) { data = evaluation.getValue(); if(INSTRUCTOR_SKILL_VALUE <= data) { quality = QualityConstants.ACCEPTABLE; } else { quality = QualityConstants.UNACCEPTABLE; break; } } data = 0; if(evaluation.getId().equals(INSTRUCTOR_PRESEN_ID)) { data = evaluation.getValue(); if(INSTRUCTOR_PRESEN_VALUE <= data) { quality = QualityConstants.ACCEPTABLE; } else { quality = QualityConstants.UNACCEPTABLE; break; } } } } // 品質基準を満たさないものは、返却する List に追加する if(quality == QualityConstants.UNACCEPTABLE) { ngSeminars.add(seminar); } } return ngSeminars; } } |
![]() |
図 2 :既存の設計を表現したクラス図 |
A 君 現状は、以下のような品質基準について実装しています。
A 君 SeminarList の getNeedImproveSeminars メソッドでは、保持する Seminar オブジェクトから評価情報である Evaluation オブジェクトを取り出し、その数値から評価数値を算出しています。その値とそれぞれの品質評価基準値とを比べて基準を満たすか満たさないかを返り値で返却しています。その後、評価基準を満たさないものについて、戻り値として返却する List オブジェクトに追加しています。
C さん なるほど。まずは、A 君の方で SeminarList クラスを見て気になる点は挙げられるかい?
A 君 getNeedImproveSeminars メソッドがちょっと複雑になってますね。ネストも深いし、やっていることがわかりづらくなってます。また、SeminarList クラスでは、algorithmType を使って条件分岐させていますけど、こういうタイプを使用している部分は怪しい臭いを感じます。
C さん 具体的に言うと?
A 君 他のデザインパターンを検討したときにも出てきましたけど、条件分岐がある部分というのは何か機能を追加したり変更したりした場合に影響を受けますね。品質基準が今後追加された場合にはここの部分がさらに複雑になってしまいます。改善できそうなポイントじゃないかと思いますが。
C さん いいところに目を付けてるよ。このあたりの視点は確かに以前にも何回か出てきているよね。今回の部分は、自分でもかなり設計の改善策が見えているんじゃないかい?今回は、現状の問題点について A 君の方で整理しておいてくれないかな。
今回は、A 君自らが問題点を考えながらノートに記入し始めました。記入したメモは以下の通りです。
C さん 問題点は、うまくまとめられてるじゃないか。じゃ、今回の解決策としては、どういうものが考えられる?
A 君 これらの問題点を解決するためには、まずは getNeedImproveSeminars メソッドを整理しないといけませんね。if 文で条件分岐しているロジックは、、、メソッドとして抽出するだけでは SeminarList クラスにメソッドがどんどん増えていって責務が大きくなり過ぎる可能性がありますね。
C さん そうだね。そうなると?
A 君 クラスとして抽出する方がよさそうですかね。そうすると、変動する部分を分割してうまくクラスの責務分割ができそうです。
C さん クラスを抽出するのはよさそうだけど、どういう方法があったかな?
A 君 継承とコンポジションですね。
C さん 両者を使った場合の考慮点について、考えてみてくれないか。
A 君 継承を使った場合(図 3)には、SeminarList クラスを継承して具象クラスを定義し、そこに独自の実装を行うことになります。継承を使用すると、スーパークラスをより簡単に拡張することができますよね。ただ、継承を使用した場合は、利用する側が具象クラスに静的に依存してしまうので、実行時に処理を切り替えることはできないですね。あと、継承してしまうと、アルゴリズムの部分を SeminarList 以外のクラスで再利用することが難しくなってしまいます。要は、クラスの独立性が失われてしまいますね。
![]() |
図 3 :継承を使った解決 継承では、SeminarList クラスを拡張して具象クラス A を定義する |
C さん 一方、コンポジションを使用した場合は?
A 君 SeminarList クラスに依存しない形でクラスを定義して、SeminarList クラスでは定義したクラスに対して処理を委譲する形になりますね。(図 4)その場合は、継承を使った場合と比べてオブジェクト間の結合度が疎になります。コンポジションを使用して、かつポリモフィズムを使って動的に処理を切り替える形にすると、再利用性も高まるし、クラスの追加時もソース・コードの修正範囲が限定されます。アルゴリズム自体を他のクラスから再利用することがある場合も、こちらが良いですね。
![]() |
図 4 :コンポジションを使った解決 コンポジションでは SeminarList とは別オブジェクト としてインタフェース A 、具象クラス C を定義する |
C さん ポイントはしっかり押さえられているよ。今回の場合は、コンポジションを使用した方がよさそうだね。継承、コンポジションの両方の適用方法があるけど、一般的には継承の適用というのは多くなくて、コンポジションを適用した例の方が多いと言えるんだ。
A 君 あ、そういうものなんですか。でも、その意図はわかる気がしますね。
C さん 修正の指針については、ある程度見えてきたよね。今回のポイントは、A 君も感づいてると思うけど、コンポジションを利用して、あるまとまったアルゴリズムを別クラスとして抽出する、という点になるんだ。これを Strategy パターンと言うんだよ。アルゴリズムを「戦略」と見立てて、その戦略をクラスとして抽出し、それ切り替えながら処理を進めていくというイメージだね。
A 君 なるほど、わかりました。実装に移ってみます。
C さん クラス図で整理することも忘れないでくれよ。
A 君の方で作成したクラス図、ソース・コードを図 5 、リスト 2 ~ 5 に示しています。品質評価アルゴリズムの一連の処理部分を別のクラス階層に分離しています。SeminarList から呼び出すインタフェースとして QualityStrategy インタフェースを定義し、その実現クラスとして AverageQualityStrategy クラス、InstructorQualityStrategy クラスを定義しています。これによって、クラス間でうまく責務分割が進み、ソース・コードの可読性も上がっています。また、品質評価アルゴリズムが追加された場合にも、QualityStrategy インタフェースを実装するクラスを用意すれば対応することができます。
![]() |
図 5 : Strategy パターンを適用したクラス図 |
リスト 2 : Strategy パターンを適用し、品質評価のアルゴリズムがなくなった SeminarList クラス |
public class SeminarList { private List seminars; private QualityStrategy qualityStrategy; …略… public List getNeedImproveSeminars() { List ngSeminars = new ArrayList(); Iterator iterator = seminars.iterator(); while(iterator.hasNext()) { Seminar seminar = (Seminar) iterator.next(); // 品質評価アルゴリズムを呼び出す int quality = qualityStrategy.evaluateQuality(seminar); if(quality == QualityConstants.UNACCEPTABLE) { ngSeminars.add(seminar); } } return ngSeminars; } } |
リスト 3 :品質評価アルゴリズムの共通インタフェース QualityStrategy |
public interface QualityStrategy { public int evaluateQuality(Seminar seminar); } |
リスト 4 :全社品質基準のアルゴリズムを表す AverageQualityStrategy クラス |
public class AverageQualityStrategy implements QualityStrategy { private static final double STANDARD_VALUE = 3.9; // 平均値を算出して、基準値をクリアしているかを確認する public int evaluateQuality(Seminar seminar) { // 評価値を取得する List evaluations = seminar.getEvaluations(); Iterator iterator = evaluations.iterator(); double total = 0.0; int i = 0; // 平均値を算出する while(iterator.hasNext()) { Evaluation evaluation = (Evaluation) iterator.next(); total += evaluation.getValue(); i++; } double average = (double) total / i; // 小数点第 2 位で四捨五入する BigDecimal bd = new BigDecimal(String.valueOf(average)); double data = bd.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); int returnValue = QualityConstants.INVALID; // 全社基準値と実データを比較する if(STANDARD_VALUE <= data) { returnValue = QualityConstants.ACCEPTABLE; } else { returnValue = QualityConstants.UNACCEPTABLE; } return returnValue; } } |
リスト 5 :チーム品質基準のアルゴリズムを表す InstructorQualityStrategy クラス |
public class InstructorQualityStrategy implements QualityStrategy { private static final String INSTRUCTOR_SKILL_ID = "3"; private static final String INSTRUCTOR_PRESEN_ID = "4"; private static final double INSTRUCTOR_SKILL_VALUE = 4.2; private static final double INSTRUCTOR_PRESEN_VALUE = 4.0; public int evaluateQuality(Seminar seminar) { List evaluations = seminar.getEvaluations(); Iterator iterator = evaluations.iterator(); int returnValue = QualityConstants.INVALID; // 講師評価の2項目についてチーム基準値と実データを比較する while(iterator.hasNext()) { Evaluation evaluation = (Evaluation) iterator.next(); double data = 0; if(evaluation.getId().equals(INSTRUCTOR_SKILL_ID)) { data = evaluation.getValue(); if(INSTRUCTOR_SKILL_VALUE <= data) { returnValue = QualityConstants.ACCEPTABLE; } else { returnValue = QualityConstants.UNACCEPTABLE; break; } } data = 0; if(evaluation.getId().equals(INSTRUCTOR_PRESEN_ID)) { data = evaluation.getValue(); if(INSTRUCTOR_PRESEN_VALUE <= data) { returnValue = QualityConstants.ACCEPTABLE; } else { returnValue = QualityConstants.UNACCEPTABLE; break; } } } return returnValue; } } |
C さん 今回は問題点から、解決策まで A 君一人で考えられたね。どうだい?デザインパターンを適用する視点はだいぶわかってきたんじゃないか?
A 君 そうですね。今回の Strategy パターンでは、かなりしっくりきましたね。問題を解決する手段はある程度わかってきました。ただ、今回気づいた点として、問題点を整理しているとき思ったんですが、「その問題が何なのか」を認識すること自体が非常に重要ですよね。まだこのあたりについては不安がありますが、デザインパターンの考え方には慣れてきた気がします。どのパターンでも、1 つ 1 つの設計判断について順を追って理解すると、そんなに難しいものではないですね。
C さん ああ、そうなんだよ。その言葉が出てくるのを期待していたんだ!(もう A 君にまかせてしまっていいなぁ。。)
A 君 えっ、なんですか?
C さん 突然だけど、いろんな理由があって新しいプロジェクトに入ることになりそうなんだ。それで、8 月からは A 君のフォローができそうにないんだよ。
A 君 ええっ。タイミングいいですね。。って、そうですか。
C さん 新人が研修を終えて何人か配属されるようだし、A 君が今度は教える側になって、いろいろとアドバイスしてあげてくれ。
ということで、システム再構築プロジェクトは A 君主導で推進していくことになりそうです。
Strategy パターンは、「アルゴリズム」をクラスとして抽出し、それを利用する側からは独立して管理する仕組みを提供します。複数のアルゴリズムが存在するものに対して、それを実行時に自由に切り替えて処理をさせることができます。
Strategy パターンの基本構造は、図 6 のようになります。、同パターンの構成要素(および、本稿で紹介したサンプル・アプリケーションの各クラスとの対応)は表 1 に示すとおりです。同パターンの適用メリットをまとめると、以下のようになります。
![]() |
図 6 : Strategy パターンの基本クラス構造 |
表 1 : Strategy パターンの構成要素 | ||||||||||||||||
|
変動要素として「アルゴリズム」をクラスとして抽出することが同パターンのポイントになります。意味的には Context クラスに保持させてもよいような処理 (アルゴリズム) を別クラスとすることも多いため、その具象クラスの中から Context クラスの情報にアクセスすることも考えられます。そのため、Context クラスから呼び出されるメソッドの引数に Context オブジェクトを渡すような実現方法がとられることがあります。
今回取り上げた Strategy パターンを通じて、デザインパターンを活用するポイントを振り返ってみましょう。
まずは、設計指針の重要なポイントから。デザインパターンの構造を適用する場合、それ自体を「適用しよう!」と意識するよりもオブジェクト指向設計の責務分割の一環として「適用できた」となる方が好ましいと言えます。最終的にできた構造は同じ場合でも、デザインパターンとして形成されている思想を理解しているかどうかで設計変更への対応や応用性は大きく違ってくるものです。
Strategy パターンでは、他のデザインパターンにも共通する以下の設計指針がありました。
これらは、オブジェクト指向設計の定石とも言えます。他のデザインパターンでも問題の解決指針としてこのような考え方を持っているものがいくつもありました。それぞれの考え方は、特に難しい考え方をしているわけではありません。オブジェクト指向の基本テクニックを使用しているに過ぎず、共通する考え方としては、モジュールの結合度、凝集度の考慮点と何ら変わるものではありません。このような基本的な指針を組み合わせながら、個々のデザインパターンの思想を 1 つ 1 つ理解していってください。
次に、思想を理解する際のやり方として、他のデザインパターンとの違いを把握することも有用なため、少し触れておきます。今回は、すべてのデザインパターンについては取り上げていないため、他のデザインパターンについても理解したいと思われた方もいらっしゃるでしょう。そういう方には、Strategy パターンを軸として他のパターンを理解するというのも 1 つの手です。
Strategy パターンはデザインパターンの中でもよく目にするパターンであり、多くの他のデザインパターンとも関係することも特徴の 1 つです。例えば、State パターンとはクラスの構造が同じような形をしています。(図 7)しかし、適用する目的が異なっており、State パターンでは「状態」を変動要素として捉える点が大きく違います。さらにもう 1 つ挙げると、Decorator パターンとは、クラス構造は一見全く異なっているように見えますが (図 8) 、複雑な条件分岐を排除することができる点や機能の追加が容易にできるような点が似通っていると言えます。Strategy パターンを理解できた方は、State パターン、Decorator パターンを参照し、その違いを見てみることをお勧めします。
![]() |
図 7 : State パターンのクラス構造 |
![]() |
図 8 : Decorator パターンのクラス構造 |
今回の連載では、Java プログラミング初心者のさらなるレベルアップとして、デザインパターンを紹介していきました。デザインパターンという言葉自体を知らなかった方には、イメージは沸きましたでしょうか?また、デザインパターンを敷居の高いものだと思っていた方には、「デザインパターンってたいしたことないじゃん」という気になっていただけたでしょうか。筆者としては、なるべく実際の適用の際の思考過程について順を追いながら説明するように、かつ平易に解説をしていくように心掛けましたが、解説が駆け足になり一部難しく感じられた方もいらっしゃったかもしれません。 さらなるステップアップのために補足すると、今回はデザインパターンに焦点を当てましたが、既存のソース・コードを洗練していく流れは、「リファクタリング」と呼ばれるものとも関係が深いものです。リファクタリングについての解説は割愛しますが、リファクタリング関連の書籍(※)もいくつか出版されています。そちらを参照されることでもさらにデザインパターンの理解やより良い設計の考え方を深めることができるでしょう。
※リファクタリング関連の書籍でお勧めなものとしては、『パターン指向リファクタリング入門』(著者:ジョシュア・ケリーエブスキー/発行:日経 BP 出版センター)や『リファクタリング―プログラムの体質改善テクニック』(著者:マーチン・ファウラー/発行:ピアソンエデュケーション)が挙げられます。
© 2007 OGIS-RI Co., Ltd. |
|