ストラテジーパターンとは
条件分岐をinterfaceを用いて一斉に切り替える設計パターンのことを、ストラテジパターンという。
以下参照。
何を実装する?
サンプルとしてマッチングアプリのバチェラーデートを使用する。
ユーザ情報(登録する料金プランなど)を入力し、以下の男性の料金プラン表から月額料金と一括支払料金を算出するプログラムを書いてみる。
ストラテジーパターンなし
ストラテジーパターンなしで実装してみる。
Mainクラスからユーザ情報を入力し、その情報を元にUserクラスで月額料金と一括支払料金を算出する。
package bachelorDateSample;
public class Main {
public static void main(String[] args) {
// ユーザ情報の登録
User user = new User(
"山田太郎"
, GenderConstants.GENTLEMAN
, RegisterPlanConstans.BASIC_PLAN
, MonthlyPlanConstans.SIX_MONTHS_PLAN);
//出力内容を整形
String newLine = "\r\n"; // 改行コード
StringBuilder sb = new StringBuilder();
sb.append("ユーザ名:" + user.getUserName() + newLine);
sb.append("性別:" + user.getGender() + newLine);
sb.append("登録プラン名:" + user.getRegisterPlanName() + newLine);
sb.append("月額プラン名:" + user.getMonthlyPlanName() + newLine);
sb.append("月額料金:" + user.getMonthlyFee() + newLine);
sb.append("一括支払料金:" + user.getBulkFee());
// 出力
System.out.println(sb);
}
}
ユーザクラス
package bachelorDateSample;
/**
* ユーザクラス
*/
public class User {
// ユーザ名
private String userName = "";
// 性別
private String gender = "";
// 登録プラン
private String registerPlanName = "";
// 月額プラン
private String monthlyPlanName = "";
// 月額料金
private int monthlyFee = 0;
// 一括支払い料金
private int bulkFee = 0;
// コンストラクタ
public User(String userName, String gender, String registerPlanName, String monthlyPlanName) {
this.userName = userName;
this.gender = gender;
this.registerPlanName = registerPlanName;
this.monthlyPlanName = monthlyPlanName;
}
/**
* ユーザ名を取得
* @return
*/
public String getUserName() {
return this.userName;
}
/**
* 性別を取得
* @return
*/
public String getGender() {
return this.gender;
}
/**
* 登録プラン名を取得
* @return
*/
public String getRegisterPlanName() {
return this.registerPlanName;
}
/**
* 月額プラン名を取得
* @return
*/
public String getMonthlyPlanName() {
return this.monthlyPlanName;
}
/**
* 月額料金の算出
* @return
*/
public int getMonthlyFee() {
// 女性は月額料金なし
if (!GenderConstants.GENTLEMAN.equals(this.gender)) {
return this.monthlyFee;
}
// 登録プランと月額プランごとに月額料金を算出
switch (this.registerPlanName) {
case RegisterPlanConstans.REASONABLE_PLAN:
switch (this.monthlyPlanName) {
case MonthlyPlanConstans.ONE_MONTH_PLAN:
this.monthlyFee = 9800;
break;
case MonthlyPlanConstans.THREE_MONTHS_PLAN:
this.monthlyFee = 8800;
break;
case MonthlyPlanConstans.SIX_MONTHS_PLAN:
this.monthlyFee = 7800;
break;
case MonthlyPlanConstans.TWELVE_MONTHS_PLAN:
this.monthlyFee = 6800;
break;
default:
this.monthlyFee = 0;
}
break;
case RegisterPlanConstans.BASIC_PLAN:
switch (this.monthlyPlanName) {
case MonthlyPlanConstans.ONE_MONTH_PLAN:
this.monthlyFee = 19800;
break;
case MonthlyPlanConstans.THREE_MONTHS_PLAN:
this.monthlyFee = 17800;
break;
case MonthlyPlanConstans.SIX_MONTHS_PLAN:
this.monthlyFee = 15800;
break;
case MonthlyPlanConstans.TWELVE_MONTHS_PLAN:
this.monthlyFee = 13800;
break;
default:
this.monthlyFee = 0;
}
break;
case RegisterPlanConstans.PREMIUM_PLAN:
switch (this.monthlyPlanName) {
case MonthlyPlanConstans.ONE_MONTH_PLAN:
this.monthlyFee = 29800;
break;
case MonthlyPlanConstans.THREE_MONTHS_PLAN:
this.monthlyFee = 26800;
break;
case MonthlyPlanConstans.SIX_MONTHS_PLAN:
this.monthlyFee = 23800;
break;
case MonthlyPlanConstans.TWELVE_MONTHS_PLAN:
this.monthlyFee = 20800;
break;
default:
this.monthlyFee = 0;
}
break;
default:
this.monthlyFee = 0;
}
return this.monthlyFee;
}
/**
* 一括支払料金の算出
* @return
*/
public int getBulkFee() {
// 女性は一括支払料金なし
if (!GenderConstants.GENTLEMAN.equals(this.gender)) {
return this.bulkFee;
}
// 登録プランと月額プランごとに一括支払料金を算出
switch (this.registerPlanName) {
case RegisterPlanConstans.REASONABLE_PLAN:
switch (this.monthlyPlanName) {
case MonthlyPlanConstans.ONE_MONTH_PLAN:
this.bulkFee = 9800;
break;
case MonthlyPlanConstans.THREE_MONTHS_PLAN:
this.bulkFee = 26400;
break;
case MonthlyPlanConstans.SIX_MONTHS_PLAN:
this.bulkFee = 46800;
break;
case MonthlyPlanConstans.TWELVE_MONTHS_PLAN:
this.bulkFee = 81600;
break;
default:
this.bulkFee = 0;
}
break;
case RegisterPlanConstans.BASIC_PLAN:
switch (this.monthlyPlanName) {
case MonthlyPlanConstans.ONE_MONTH_PLAN:
this.bulkFee = 19800;
break;
case MonthlyPlanConstans.THREE_MONTHS_PLAN:
this.bulkFee = 53400;
break;
case MonthlyPlanConstans.SIX_MONTHS_PLAN:
this.bulkFee = 94800;
break;
case MonthlyPlanConstans.TWELVE_MONTHS_PLAN:
this.bulkFee = 165600;
break;
default:
this.bulkFee = 0;
}
break;
case RegisterPlanConstans.PREMIUM_PLAN:
switch (this.monthlyPlanName) {
case MonthlyPlanConstans.ONE_MONTH_PLAN:
this.bulkFee = 29800;
break;
case MonthlyPlanConstans.THREE_MONTHS_PLAN:
this.bulkFee = 80400;
break;
case MonthlyPlanConstans.SIX_MONTHS_PLAN:
this.bulkFee = 142800;
break;
case MonthlyPlanConstans.TWELVE_MONTHS_PLAN:
this.bulkFee = 249600;
break;
default:
this.bulkFee = 0;
}
break;
default:
this.bulkFee = 0;
}
return this.bulkFee;
}
}
登録プラン、月額プラン、性別の定数クラス
package bachelorDateSample;
/**
* 登録プラン定数クラス
*/
public class RegisterPlanConstans {
// コンストラクタの作成防止
private RegisterPlanConstans() {}
public static final String REASONABLE_PLAN = "お手軽プラン";
public static final String BASIC_PLAN = "ベーシックプラン";
public static final String PREMIUM_PLAN = "プレミアムプラン";
}
package bachelorDateSample;
/**
* 月額プラン定数クラス
*/
public class MonthlyPlanConstans {
// コンストラクタの作成防止
private MonthlyPlanConstans() {}
public static final String ONE_MONTH_PLAN = "1か月プラン";
public static final String THREE_MONTHS_PLAN = "3か月プラン";
public static final String SIX_MONTHS_PLAN = "6か月プラン";
public static final String TWELVE_MONTHS_PLAN = "12か月プラン";
}
package bachelorDateSample;
/**
* 性別定数クラス
*/
public class GenderConstants {
// コンストラクタの作成防止
private GenderConstants() {}
public static final String GENTLEMAN = "男性";
public static final String LADY = "女性";
public static final String OTHER = "女性";
}
実行すると以下のようにコンソールに出力される。
ユーザ名:山田太郎 性別:男性 登録プラン名:ベーシックプラン 月額プラン名:6か月プラン 月額料金:15800 一括支払料金:94800
Userクラスのswitch文の分岐が見辛く、しかもgetMonthlyFee()とgetBulkFee()で重複していてしまっている。
これでは機能追加や仕様変更のたびに、複数のswitch文を修正しなければならず保守が困難になることが考えられる。
次章では、ストラテジーパターンパターンを用いて、仕様変更に強い設計へとリファクタリングしていく。
ストラテジーパターンでリファクタリング
各登録プランと月額プランごとに新規でクラスを作成し、インターフェースを実装させる。
クラスのインスタンスをインターフェースの型で受け取ることで、プランごとに処理を切り分ける。
以下のクラスで登録プランインターフェース実装する。
・ReasonablePlan.java
・BasicPlan.java
・PremiumPlan.java
package bachelorDateSampleInterface;
/**
* 登録プランインターフェース
*/
public interface RegisterPlan {
public String getRegisterPlanName();
public int getMonthlyFee(MonthlyPlan monthlyPlanName);
public int getBulkFee(MonthlyPlan monthlyPlanName);
}
以下のクラスで月額インターフェースを実装する。
・OneMonthPlan
・ThreeMonthsPlan
・SixMonthsPlan
・TwelveMonthsPlan
package bachelorDateSampleInterface;
/**
* 月額プランインターフェース
*/
public interface MonthlyPlan {
public String getMonthlyPlanName();
public int calcBulkFee(int monthlyFee);
}
MainクラスからUserクラスに、ユーザーの登録プランと月額プランのインスタンスを渡す。
各登録プランクラスごと、各月額プランクラスごと同じメソッドを実装しているので、
Userクラスからプランの違いを意識する必要がなくなる。
package bachelorDateSample;
import bachelorDateSampleConstants.GenderConstants;
public class Main {
public static void main(String[] args) {
// ユーザ情報の登録
User user = new User(
"山田太郎"
, GenderConstants.GENTLEMAN
, new BasicPlan()
, new SixMonthsPlan()
);
//出力内容を整形
String newLine = "\r\n"; // 改行コード
StringBuilder sb = new StringBuilder();
sb.append("ユーザ名:" + user.getUserName() + newLine);
sb.append("性別:" + user.getGender() + newLine);
sb.append("登録プラン名:" + user.getRegisterPlanName() + newLine);
sb.append("月額プラン名:" + user.getMonthlyPlanName() + newLine);
sb.append("月額料金:" + user.getMonthlyFee() + newLine);
sb.append("一括支払料金:" + user.getBulkFee());
// 出力
System.out.println(sb);
}
}
package bachelorDateSample;
import bachelorDateSampleConstants.GenderConstants;
import bachelorDateSampleInterface.MonthlyPlan;
import bachelorDateSampleInterface.RegisterPlan;
/**
* ユーザクラス
*/
public class User {
// ユーザ名
private String userName = "";
// 性別
private String gender = "";
// 登録プランインターフェース
private RegisterPlan registerPlan;
// 月額プランインターフェース
private MonthlyPlan monthlyPlan;
// コンストラクタ
public User(String userName, String gender, RegisterPlan registerPlanName, MonthlyPlan monthlyPlanName) {
this.userName = userName;
this.gender = gender;
setPlanName(registerPlanName, monthlyPlanName);
}
/**
* 各プランのインスタンス変数をインターフェースの型で受け取る
* @param registerPlanName
* @param monthlyPlanName
*/
public void setPlanName(RegisterPlan registerPlanName, MonthlyPlan monthlyPlanName) {
this.registerPlan = registerPlanName;
this.monthlyPlan = monthlyPlanName;
}
/**
* ユーザ名を取得
* @return
*/
public String getUserName() {
return this.userName;
}
/**
* 性別を取得
* @return
*/
public String getGender() {
return this.gender;
}
/**
* 性別が男かを判定
* @return
*/
public boolean isGentlemen() {
return GenderConstants.GENTLEMAN.equals(this.gender);
}
/**
* 登録プラン名を取得
* @return
*/
public String getRegisterPlanName() {
return this.registerPlan.getRegisterPlanName();
}
/**
* 月額プラン名を取得
* @return
*/
public String getMonthlyPlanName() {
return this.monthlyPlan.getMonthlyPlanName();
}
/**
* 月額料金の算出
* @return
*/
public int getMonthlyFee() {
// 女性は月額料金なし
if (!isGentlemen()) {
return 0;
}
return this.registerPlan.getMonthlyFee(this.monthlyPlan);
}
/**
* 一括支払料金の算出
* @return
*/
public int getBulkFee() {
// 女性は月額料金なし
if (!isGentlemen()) {
return 0;
}
return this.registerPlan.getBulkFee(this.monthlyPlan);
}
}
登録プランインターフェースを実装した、各登録プランクラス
package bachelorDateSample;
import bachelorDateSampleConstants.RegisterPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
import bachelorDateSampleInterface.RegisterPlan;
/**
* リーズナブルプランクラス
*/
public class ReasonablePlan implements RegisterPlan{
// 一括支払料金
public int monthlyFee = 0;
/**
* 登録プラン名を取得
*/
public String getRegisterPlanName() {
return RegisterPlanConstans.REASONABLE_PLAN;
}
/**
* 月額料金を算出
*/
public int getMonthlyFee(MonthlyPlan monthlyPlan) {
// 月額プランごとに月額料金を算出
if (monthlyPlan instanceof OneMonthPlan) {
this.monthlyFee = 9800;
} else if (monthlyPlan instanceof ThreeMonthsPlan) {
this.monthlyFee = 8800;
} else if (monthlyPlan instanceof SixMonthsPlan) {
this.monthlyFee = 7800;
} else if (monthlyPlan instanceof TwelveMonthsPlan) {
this.monthlyFee = 6800;
} else {
}
return this.monthlyFee;
}
/**
* 一括支払料金を算出
*/
public int getBulkFee(MonthlyPlan monthlyPlan) {
return monthlyPlan.calcBulkFee(this.monthlyFee);
}
}
package bachelorDateSample;
import bachelorDateSampleConstants.RegisterPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
import bachelorDateSampleInterface.RegisterPlan;
/**
* ベーシックプランクラス
*/
public class BasicPlan implements RegisterPlan{
// 一括支払料金
public int monthlyFee = 0;
/**
* 登録プラン名を取得
*/
public String getRegisterPlanName() {
return RegisterPlanConstans.BASIC_PLAN;
}
/**
* 月額料金を算出
*/
public int getMonthlyFee(MonthlyPlan monthlyPlan) {
// 月額プランごとに月額料金を算出
if (monthlyPlan instanceof OneMonthPlan) {
this.monthlyFee = 19800;
} else if (monthlyPlan instanceof ThreeMonthsPlan) {
this.monthlyFee = 17800;
} else if (monthlyPlan instanceof SixMonthsPlan) {
this.monthlyFee = 15800;
} else if (monthlyPlan instanceof TwelveMonthsPlan) {
this.monthlyFee = 13800;
} else {
}
return this.monthlyFee;
}
/**
* 一括支払料金を算出
*/
public int getBulkFee(MonthlyPlan monthlyPlan) {
return monthlyPlan.calcBulkFee(this.monthlyFee);
}
}
package bachelorDateSample;
import bachelorDateSampleConstants.RegisterPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
import bachelorDateSampleInterface.RegisterPlan;
/**
* プレミアムプランクラス
*/
public class PremiumPlan implements RegisterPlan{
// 一括支払料金
public int monthlyFee = 0;
/**
* 登録プラン名を取得
*/
public String getRegisterPlanName() {
return RegisterPlanConstans.PREMIUM_PLAN;
}
/**
* 月額料金を算出
*/
public int getMonthlyFee(MonthlyPlan monthlyPlan) {
// 月額プランごとに月額料金を算出
if (monthlyPlan instanceof OneMonthPlan) {
this.monthlyFee = 29800;
} else if (monthlyPlan instanceof ThreeMonthsPlan) {
this.monthlyFee = 26800;
} else if (monthlyPlan instanceof SixMonthsPlan) {
this.monthlyFee = 23800;
} else if (monthlyPlan instanceof TwelveMonthsPlan) {
this.monthlyFee = 20800;
} else {
}
return this.monthlyFee;
}
/**
* 一括支払料金を算出
*/
public int getBulkFee(MonthlyPlan monthlyPlan) {
return monthlyPlan.calcBulkFee(this.monthlyFee);
}
}
月額プランインターフェースを実装した、各月額プランクラス
package bachelorDateSample;
import bachelorDateSampleConstants.MonthlyPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
/**
* 1か月プランクラス
*/
public class OneMonthPlan implements MonthlyPlan {
/**
* 月額プラン名を取得
*/
public String getMonthlyPlanName() {
return MonthlyPlanConstans.ONE_MONTH_PLAN;
}
/**
* 月額料金 * 月額プランの月数(1か月)で一括支払料金を計算。
*/
public int calcBulkFee(int monthlyFee) {
int bulkFee = monthlyFee * 1;
return bulkFee;
}
}
package bachelorDateSample;
import bachelorDateSampleConstants.MonthlyPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
/**
* 3か月プランクラス
*/
public class ThreeMonthsPlan implements MonthlyPlan {
/**
* 月額プラン名を取得
*/
public String getMonthlyPlanName() {
return MonthlyPlanConstans.THREE_MONTHS_PLAN;
}
/**
* 月額料金 * 月額プランの月数(3か月)で一括支払料金を計算。
*/
public int calcBulkFee(int monthlyFee) {
int bulkFee = monthlyFee * 3;
return bulkFee;
}
}
package bachelorDateSample;
import bachelorDateSampleConstants.MonthlyPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
/**
* 6か月プランクラス
*/
public class SixMonthsPlan implements MonthlyPlan {
/**
* 月額プラン名を取得
*/
public String getMonthlyPlanName() {
return MonthlyPlanConstans.SIX_MONTHS_PLAN;
}
/**
* 月額料金 * 月額プランの月数(6か月)で一括支払料金を計算。
*/
public int calcBulkFee(int monthlyFee) {
int bulkFee = monthlyFee * 6;
return bulkFee;
}
}
package bachelorDateSample;
import bachelorDateSampleConstants.MonthlyPlanConstans;
import bachelorDateSampleInterface.MonthlyPlan;
/**
* 12か月プランクラス
*/
public class TwelveMonthsPlan implements MonthlyPlan {
/**
* 月額プラン名を取得
*/
public String getMonthlyPlanName() {
return MonthlyPlanConstans.TWELVE_MONTHS_PLAN;
}
/**
* 月額料金 * 月額プランの月数(12か月)で一括支払料金を計算。
*/
public int calcBulkFee(int monthlyFee) {
int bulkFee = monthlyFee * 12;
return bulkFee;
}
}
Userクラスのコンストラクタで各プランのインスタンスを格納し、それを利用することでプランごとに処理を切り分けることができた。
今後新しいプランを実装するときに、インターフェースを実装したクラスを作成することで容易に機能追加できるようになった。
課題
今回ストラテジパターンについて学習した。
心残りとして、ReasonablePlan.java、BasicPlan.java、PremiumPlan.javaのgetMonthlyFee()にMonthlyPlanの型判定しているところがあり少々冗長な作りになっている。
料金体系をロジックが、登録プランと月額プランの入れ子になっているためこのような実装になっているが、
今後修正していきたい。