第2章 逆引きカタログ ロジック編
Authors:Yoshihara Hidehiko
入出力は同じだけど条件によってアルゴリズムの交換を行いたい場合や、将来的にアルゴリズムが変更される可能性がある処理に遭遇する場面があります。
また、switch文などの条件分岐にアルゴリズムを埋め込むような処理を行うと、変更が発生した場合に他のアルゴリズムへ影響が生じたりコードが冗長になったりし、保守性がよくありません。
ストラテジパターンは、アルゴリズムをクラス化することにより、アルゴリズムの切り替えを使用するクラスとは無関係に簡単に行えるようにするパターンです。
なお、ストラテジは「戦略」という意味です。条件によってアルゴリズムを切り替えるところは、まるで戦略を練っているようですね。
データ処理を例にストラテジパターンを説明します。
図6の左側のように、入力したデータ処理が条件により振り分けられ、最終的に同じ型のデータを出力する処理が存在します。
この処理に条件の追加や振り分け方法の変更が行われた場合、他処理へ影響が及ぶ可能性があります。
コードの見た目も冗長で、かっこいいとはいえませんよね。
ここで、ストラテジパターンを適用してみます。
- @使用者に提供する窓口をインタフェースとして作成する
- Aインタフェースを適用したクラスを作成して、切り出したアルゴリズムを実装する
- B使用者は、条件によってアルゴリズムクラスを生成する。インタフェースを使用してアルゴリズムの実行を行う
図6の右側のように、すっきりしたコードにまとめることができましたね。
図6ではアルゴリズムクラスの生成を条件分岐で行っていますが、先ほどのファクトリパターンを併用すれば、アルゴリズムクラスの切り替えがより柔軟に行えます。
図6 ストラテジパターン適用前/適用後
ストラテジパターンの利点は、アルゴリズムをカプセル(クラス)化できることと、アルゴリズムの実行にインタフェースを使用することです。
カプセル化を行うことで、アルゴリズムの拡張性と保守性を高め、再利用を可能にします。
インタフェースが提供するのは、操作(メソッド)の窓口だけです。この窓口は、ある目的に対して共通に行われる操作を表します。
たとえば、コーヒーを飲みたいときの共通操作である「コーヒーを淹れる」を行えば、あとはコーヒー豆の挽き方や、使う道具、抽出の方法について知る必要はありません。
図7では、「コーヒーを淹れる」がインタフェースにあたり、「インスタントコーヒー」「ドリップ」「サイフォン」が、インタフェースで提供されている操作(メソッド)を実装したアルゴリズムクラスに相当します。
少し大げさですが、サイフォンがインスタントコーヒーにこっそり変更されたとしても気が付くことはありません(インスタントコーヒーとサイフォンでは明らかに味は違いますから、飲んでみて気づくのでしょうね)。
このインタフェースを使用したアクセス方法は「関心の分離」と言われ、オブジェクト指向で非常に重要視されています。
具体的な例として、CSVデータファイルの解析にストラテジパターンを使用してみます。
ファイルにデータを保存する場合、そのフォーマットにはXMLを採用するのが世の流れでしょうが、ここではCSVと呼ばれる、データ項目をカンマ区切りで区別するシンプルなフォーマットを取り上げます
(汎用機などの基幹システムでは、データフォーマットとしてCSV形式を採用しているものもまだまだ健在です)。
図8のように2社のフォーマットが異なるCSVデータファイルの共通情報を画面に表示するサンプルです。
ここでは、データフォーマットの解析操作をインタフェースとして作成し、各社の解析処理をアルゴリズムクラスに実装します。
また、アルゴリズムクラスの生成には、先ほどのファクトリパターンを使用して行います(図9)。
図9 サンプルのクラス図
◎AnalysisStaffStrategy(リスト9)
依頼されたデータの解析を行いStaffクラスの作成を行う窓口(analysisStaff())を提供するインタフェースです。
実際に解析処理を行うアルゴリズムクラスは、AnalysisStaffStrategyをimplementsしなければいけません。
リスト9 AnalysisStaffStrategy.java
public interface AnalysisStaffStrategy {
public abstract Staff analysisStaff(String line);
}
◎AnalysisStaffCompanyA(リスト10),AnalysisStaffCompanyB(リスト11)
CSVデータの解析処理が実装されたアルゴリズムクラスです。解析データをStaffクラスに格納して返却します。
A社とB社のデータフォーマットの違いについて整理しておきましょう。
- 項目の順番が違う
- A社は「姓」「名」を空白で区切って同一データ項目内に管理しているが、B社は「姓」と「名」を別データとして管理している
- A社は「住所」を一括りに管理しているが、B社は「住所」と「マンション名」で分けて管理している
リスト10 AnalysisStaffCompanyA.java
public class AnalysisStaffCompanyA implements AnalysisStaffStrategy {
public static final int COMPANY_A = 1;
public Staff analysisStaff(String line) {
Staff staff = new Staff();
StringTokenizer st = new StringTokenizer(line, ",");
staff.setId(Integer.parseInt(st.nextToken()));
String name= st.nextToken();
StringTokenizer st2 = new StringTokenizer(name, " ");
staff.setLastName(st2.nextToken());
staff.setFirstName(st2.nextToken());
staff.setPostCode(st.nextToken());
staff.setAddress(st.nextToken());
staff.setTel("(" + st.nextToken() + ")" + st.nextToken());
return staff;
}
}
リスト11 AnalysisStaffCompanyB.java
public class AnalysisStaffCompanyB implements AnalysisStaffStrategy {
public static final int COMPANY_B = 2;
public Staff analysisStaff(String line) {
Staff staff = new Staff();
StringTokenizer st = new StringTokenizer(line, ",");
staff.setId(Integer.parseInt(st.nextToken()));
staff.setLastName(st.nextToken());
staff.setFirstName(st.nextToken());
st.nextToken();
st.nextToken();
staff.setTel("(" + st.nextToken() + ")" + st.nextToken());
staff.setPostCode(st.nextToken());
staff.setAddress(st.nextToken() + st.nextToken());
return staff;
}
}
◎AnalysisStaffFactory(リスト12)
AnalysisStaffFactoryクラスは、StaffContextクラスから指定された条件に合ったアルゴリズムクラスを生成し、StaffContextクラスにAnalysisStaffStrategyインタフェースを返します。
ここでは、会社コードによりA社CSVデータ解析クラスAnalysisStaffCompanyAおよびB社CSVデータ解析クラスAnalysisStaffCompanyBの生成を行います。
AnalysisStaffFactoryを使用することで、新しいフォーマットのCSVデータが追加されてもStaffContextクラスに影響を与えません。
リスト12 AnalysisStaffFactory.java
public class AnalysisStaffFactory {
public static AnalysisStaffStrategy
createAnalysisStaff(int companyCode) {
AnalysisStaffStrategy strategy = null;
if (companyCode == AnalysisStaffCompanyA.COMPANYCODE_A) {
strategy = new AnalysisStaffCompanyA();
} else {
strategy = new AnalysisStaffCompanyB();
}
return strategy;
}
}
◎StaffContext(リスト13)
StaffContextは、AnaliysisStaffStrategyを保持します。
AnaliysisStaffStrategyのanaliysisStaffStrategyの切り替えを行います。
リスト13 StaffContext.java
public class StaffContext {
private AnalysisStaffStrategy strategy;
AnalysisStaffStrategy strategy = null;
StaffContext(int companyCode) {
strategy =
AnalysisStaffFactory.createAnalysisStaff(companyCode);
}
public Staff getStaff(String line) {
return strategy.analysisStaff(line);
}
}
ストラテジパターンは比較的簡単なデザインパターンです。
ストラテジパターンを適用できる場面に遭遇する機会は多いのではないでしょうか。
ただし、ストラテジパターンは「アルゴリズムの交換」を行うことが本来の目的です。
アルゴリズムクラスに、アルゴリズムと呼ぶにはあまりに範囲の広い複雑な処理や他に影響を与えるような処理を盛り込んだ場合、クラスの切り替えをする可能性があります。
と言って、わずかな差をアルゴリズムクラスにしてしまうと、それはそれで管理が大変になります。
指針としては、継承を使用してアルゴリズム処理の差分を実装しているような処理や、switch文を多用しているようなクラスが存在する場合は、ストラテジパターンの採用を検討したほうがよいでしょう。
まずは、リファクタリングでこつこつとストラテジパターンの練習を行ってみましょう。