1. はじめに
今回はデシジョンテーブルで整理された仕様を実装する機会があったので、その定義の通りに動くプログラムを実装する方法について説明したいと思います。
デシジョンテーブルとは、ある問題に対して、考えられる条件とその際の動作を表にまとめたものです。詳細については「デシジョンテーブルの解説」のサイトが分かりやすいので、そちらを参照ねがいます。
今回の題材とするデシジョンテーブル(駐車場料金の割引計算)と以下の図についても、このサイトから引用させて頂きました。
条件記述部(condition stub) | 条件指定部(condition entry) |
---|---|
![]() |
![]() |
動作記述部(action stub) | 動作指定部(action entry) |
![]() |
![]() |
今回の記事で説明する内容は以下の通りです。
-
2. デシジョンテーブルの実装
- デシジョンテーブルを実現する機能の実装について説明します。
-
3. 駐車場料金の割引計算デシジョンテーブルの実装
- 作成したデシジョンテーブルを「駐車場料金の割引計算」に適用したサンプルです。
-
4. 駐車場料金の割引計算デシジョンテーブルの実装(シンプル版)
- 「駐車場料金の割引計算」のデシジョンテーブルをもう少しシンプルに変更した場合のサンプルです。
2. デシジョンテーブルの実装
条件の1つ1つをRuleとしたところは独自ですが、なるべくデシジョンテーブルの見た目と整合性が取れるようにクラスデザインをしてみました。
結果として6つのクラス(インターフェースが3つ、抽象クラスが1つ)で実現することができました。
内容を見てもらえば分かりますが、Javaの標準機能だけ、かつ、条件分岐なしで実装しています。
2.1. DecisionAction
デシジョンテーブルの判定結果である動作(アクション)を定義するためのインターフェースです。内容は実現したいデシジョンテーブルに応じて自由に定義します。
public interface DecisionAction {
}
2.2. RuleInput
Ruleの入力データを定義するためのインターフェースです。
public interface RuleInput {
}
2.3. Rule
条件の判断ロジックを実装するためのインターフェースです。
Object evaluate(RuleInput input)
メソッドに1つの条件を判断する処理を実装します。戻り値は条件指定部の判定で利用する値になります。Javaの標準データ型(真偽値、文字列型、数値型)であれば何でも構いません。
public interface Rule<I extends RuleInput> {
Object evaluate(RuleInput input);
@SuppressWarnings("unchecked")
default I getInput(RuleInput input) {
return (I) input;
};
}
2.4. ConditionStub
条件判断の実行時に引数となるクラスです。見ての通りRuleとRuleInputの単なるホルダーです。
public class ConditionStub {
private Map<Rule<? extends RuleInput>, RuleInput> rules = new HashMap<>();
public void when(Rule<? extends RuleInput> rule, RuleInput input) {
rules.put(rule, input);
}
public Map<Rule<? extends RuleInput>, RuleInput> getRules() {
return rules;
}
}
2.5. ConditionEntry
デシジョンテーブルの条件記述部と、その際に決定される動作を定義するクラスです。
データの保持項目としては#1~#8で定義されるデータパターンの1つ分を保持します。
ポイントとしては一意に特定するためにSet
のhashCode
をidとして利用するところです。
public class ConditionEntry<A extends DecisionAction> {
private Set<String> resultMap = new HashSet<>();
private A action;
public void when(@SuppressWarnings("rawtypes") Class ruleClass, Object entry) {
resultMap.add(ruleClass.getSimpleName() + "/" + entry);
}
public void then(A action) {
this.action = action;
}
public int getId() {
return resultMap.hashCode();
}
public A getAction() {
return action;
}
}
2.6. DecisionTable
デシジョンテーブルを定義するクラスです。
データとしてはConditionEntry
をMapで保持しているだけです。データパターンが#1~#8
と8つある場合、Mapには8つのConditionEntry
が格納されます。
実際に利用する際はinitDecisionTable()
でデシジョンテーブルのデータを定義します。
デシジョンテーブルのアクションを決定する処理がresolve
メソッドです。
内容を見えてもらえば分かりますが、Ruleを順次実行してConditionEntry
のインスタンスを作成し、そのidに該当するものがデシジョンテーブルに登録されているかチェックしているだけです。
public abstract class DecisionTable<A extends DecisionAction> {
private Map<Integer, ConditionEntry<A>> decisionTable;
public DecisionTable() {
decisionTable = initDecisionTable();
}
protected abstract Map<Integer, ConditionEntry<A>> initDecisionTable();
public A resolve(ConditionStub conditionStub) {
// execute rule
ConditionEntry<A> result = new ConditionEntry<>();
conditionStub.getRules().forEach((R, I) -> {
result.when(R.getClass(), R.evaluate(I));
});
// search action
Integer key = result.getId();
if (decisionTable.containsKey(key)) {
return decisionTable.get(key).getAction();
}
return null;
}
}
3. 駐車場料金の割引計算デシジョンテーブルの実装
「駐車場料金の割引計算」デシジョンテーブルを実装してみます。
本当に見た目の通り実装することになります。もっとシンプルに実装したい場合は以下の「4. 駐車場料金の割引計算デシジョンテーブルの実装(シンプル版)」を参照してください。
- ParkingDiscountAction : 割引結果となる動作を定義します。
- PriceRule1 : 「3000円以上10000円未満」を判断する処理ロジックを実装します。
- PriceRule2 : 「10000円以上30000円未満」を判断する処理ロジックを実装します。
- PriceRule3 : 「30000円以上」を判断する処理ロジックを実装します。
- CinemaRule : 「シネマ利用」を判断する処理ロジックを実装します。
3.1. DecisionActionの実装
public class ParkingDiscountAction implements DecisionAction {
private final boolean discount30Minute;
private final boolean discount1Hour;
private final boolean discount2Hour30Minute;
private final boolean discount3Hour30Minute;
// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。デシジョンテーブルの結果を保持するだけなのでsetterは不要。
// 内容を確認するため、toString()メソッドをeclipseで自動生成すると便利。
}
3.2. RuleInputとRuleの実装
public class PriceRuleInput implements RuleInput {
private final int paymentPrice;
// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。入力値は不変なのでsetterは不要。
}
public class PriceRule1 implements Rule<PriceRuleInput> {
// 「3000円以上10000円未満」を判断する処理ロジック
@Override
public Object evaluate(RuleInput input) {
PriceRuleInput priceRuleInput = getInput(input);
int paymentPrice = priceRuleInput.getPaymentPrice();
if (paymentPrice >= 3000 && paymentPrice < 10000) {
return true;
}
return false;
}
}
public class PriceRule2 implements Rule<PriceRuleInput> {
// 「10000円以上30000円未満」を判断する処理ロジック
@Override
public Object evaluate(RuleInput input) {
PriceRuleInput priceRuleInput = getInput(input);
int paymentPrice = priceRuleInput.getPaymentPrice();
if (paymentPrice >= 10000 && paymentPrice < 30000) {
return true;
}
return false;
}
}
public class PriceRule3 implements Rule<PriceRuleInput> {
// 「30000円以上」を判断する処理ロジック
@Override
public Object evaluate(RuleInput input) {
PriceRuleInput priceRuleInput = getInput(input);
int paymentPrice = priceRuleInput.getPaymentPrice();
if (paymentPrice >= 30000) {
return true;
}
return false;
}
}
public class CinemaRuleInput implements RuleInput {
private final boolean watchCinema;
// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。入力値は不変なのでsetterは不要。
}
public class CinemaRule implements Rule<CinemaRuleInput> {
@Override
public Object evaluate(RuleInput input) {
CinemaRuleInput cinemaRuleInput = getInput(input);
return cinemaRuleInput.isWatchCinema();
}
}
3.3. デシジョンテーブルの定義
デシジョンテーブルの赤枠のConditionEntry
の通りに定義します。
条件はwhen()
メソッドで追加し、その際の動作をthen()
メソッドで定義します。
デシジョンテーブルの#1~#8の8つのデータパターンの定義が終わった後、それぞれをMapに追加します。その際のキーにはConditionEntry
のgetId()
で取得したidとします。
public class ParkingDiscountDecisionTable extends DecisionTable<ParkingDiscountAction> {
@Override
protected Map<Integer, ConditionEntry<ParkingDiscountAction>> initDecisionTable() {
// #1
ConditionEntry<ParkingDiscountAction> pattern01 = new ConditionEntry<>();
pattern01.when(PriceRule1.class, false);
pattern01.when(PriceRule2.class, false);
pattern01.when(PriceRule3.class, false);
pattern01.when(CinemaRule.class, false);
pattern01.then(new ParkingDiscountAction(true, false, false, false));
// #2
ConditionEntry<ParkingDiscountAction> pattern02 = new ConditionEntry<>();
pattern02.when(PriceRule1.class, true);
pattern02.when(PriceRule2.class, false);
pattern02.when(PriceRule3.class, false);
pattern02.when(CinemaRule.class, false);
pattern02.then(new ParkingDiscountAction(false, true, false, false));
// #3
ConditionEntry<ParkingDiscountAction> pattern03 = new ConditionEntry<>();
pattern03.when(PriceRule1.class, false);
pattern03.when(PriceRule2.class, true);
pattern03.when(PriceRule3.class, false);
pattern03.when(CinemaRule.class, false);
pattern03.then(new ParkingDiscountAction(false, false, true, false));
// #4
ConditionEntry<ParkingDiscountAction> pattern04 = new ConditionEntry<>();
pattern04.when(PriceRule1.class, false);
pattern04.when(PriceRule2.class, false);
pattern04.when(PriceRule3.class, true);
pattern04.when(CinemaRule.class, false);
pattern04.then(new ParkingDiscountAction(false, false, false, true));
// #5
ConditionEntry<ParkingDiscountAction> pattern05 = new ConditionEntry<>();
pattern05.when(PriceRule1.class, false);
pattern05.when(PriceRule2.class, false);
pattern05.when(PriceRule3.class, false);
pattern05.when(CinemaRule.class, true);
pattern05.then(new ParkingDiscountAction(false, false, true, false));
// #6
ConditionEntry<ParkingDiscountAction> pattern06 = new ConditionEntry<>();
pattern06.when(PriceRule1.class, true);
pattern06.when(PriceRule2.class, false);
pattern06.when(PriceRule3.class, false);
pattern06.when(CinemaRule.class, true);
pattern06.then(new ParkingDiscountAction(false, false, true, false));
// #7
ConditionEntry<ParkingDiscountAction> pattern07 = new ConditionEntry<>();
pattern07.when(PriceRule1.class, false);
pattern07.when(PriceRule2.class, true);
pattern07.when(PriceRule3.class, false);
pattern07.when(CinemaRule.class, true);
pattern07.then(new ParkingDiscountAction(false, false, true, false));
// #8
ConditionEntry<ParkingDiscountAction> pattern08 = new ConditionEntry<>();
pattern08.when(PriceRule1.class, false);
pattern08.when(PriceRule2.class, false);
pattern08.when(PriceRule3.class, true);
pattern08.when(CinemaRule.class, true);
pattern08.then(new ParkingDiscountAction(false, false, false, true));
// create map
Map<Integer, ConditionEntry<ParkingDiscountAction>> tables = new HashMap<>();
tables.put(pattern01.getId(), pattern01);
tables.put(pattern02.getId(), pattern02);
tables.put(pattern03.getId(), pattern03);
tables.put(pattern04.getId(), pattern04);
tables.put(pattern05.getId(), pattern05);
tables.put(pattern06.getId(), pattern06);
tables.put(pattern07.getId(), pattern07);
tables.put(pattern08.getId(), pattern08);
return tables;
}
}
3.4. デシジョンテーブルの利用
デシジョンテーブルを利用するのは簡単でConditionStub
のインスタンスを生成した後、ParkingDiscountDecisionTable
のresolve()
メソッドに引数として渡して実行するだけです。
実行中にデシジョンテーブルの定義が変わることはないため、ParkingDiscountDecisionTable
のインスタンスは毎回生成するではなく、使いまわすことを推奨します。
public class ParkingDiscountService {
// DIコンテナを利用するならインジェクション
PriceRule1 priceRule1;
PriceRule2 priceRule2;
PriceRule3 priceRule3;
CinemaRule cinemaRule;
ParkingDiscountDecisionTable parkingDiscountDecisionTable;
public ParkingDiscountService() {
// インジェクションの代わりにインスタンス生成
priceRule1 = new PriceRule1();
priceRule2 = new PriceRule2();
priceRule3 = new PriceRule3();
cinemaRule = new CinemaRule();
parkingDiscountDecisionTable = new ParkingDiscountDecisionTable();
}
public void business(int paymentPrice, boolean watchCinema) {
// create input data
PriceRuleInput priceRuleInput = new PriceRuleInput(paymentPrice);
CinemaRuleInput cinemaRuleInput = new CinemaRuleInput(watchCinema);
// create conditionStub
ConditionStub conditionStub = new ConditionStub();
conditionStub.when(priceRule1, priceRuleInput);
conditionStub.when(priceRule2, priceRuleInput);
conditionStub.when(priceRule3, priceRuleInput);
conditionStub.when(cinemaRule, cinemaRuleInput);
// resolve by decisionTable
ParkingDiscountAction parkingDiscountAction = parkingDiscountDecisionTable.resolve(conditionStub);
System.out.println("paymentPrice : " + paymentPrice + ", watchCinema : " + watchCinema);
System.out.println(parkingDiscountAction);
}
}
3.5. 動作確認
public class Demo {
public static void main(String[] args) {
// DIコンテナを利用しているならそこから取得
// 今回はその場でインスタンスを生成
ParkingDiscountService service = new ParkingDiscountService();
// # 1
service.business(2000, false);
// # 2
service.business(5000, false);
// # 3
service.business(17000, false);
// # 4
service.business(45000, false);
// # 5
service.business(100, true);
// # 6
service.business(7000, true);
// # 7
service.business(20000, true);
// # 8
service.business(100000, true);
}
}
paymentPrice : 2000, watchCinema : false
ParkingDiscountAction [discount30Minute=true, discount1Hour=false, discount2Hour30Minute=false, discount3Hour30Minute=false]
paymentPrice : 5000, watchCinema : false
ParkingDiscountAction [discount30Minute=false, discount1Hour=true, discount2Hour30Minute=false, discount3Hour30Minute=false]
paymentPrice : 17000, watchCinema : false
ParkingDiscountAction [discount30Minute=false, discount1Hour=false, discount2Hour30Minute=true, discount3Hour30Minute=false]
paymentPrice : 45000, watchCinema : false
ParkingDiscountAction [discount30Minute=false, discount1Hour=false, discount2Hour30Minute=false, discount3Hour30Minute=true]
paymentPrice : 100, watchCinema : true
ParkingDiscountAction [discount30Minute=false, discount1Hour=false, discount2Hour30Minute=true, discount3Hour30Minute=false]
paymentPrice : 7000, watchCinema : true
ParkingDiscountAction [discount30Minute=false, discount1Hour=false, discount2Hour30Minute=true, discount3Hour30Minute=false]
paymentPrice : 20000, watchCinema : true
ParkingDiscountAction [discount30Minute=false, discount1Hour=false, discount2Hour30Minute=true, discount3Hour30Minute=false]
paymentPrice : 100000, watchCinema : true
ParkingDiscountAction [discount30Minute=false, discount1Hour=false, discount2Hour30Minute=false, discount3Hour30Minute=true]
4. 駐車場料金の割引計算デシジョンテーブルの実装(シンプル版)
これをデシジョンテーブルと呼ぶのか分かりませんが、書かれている仕様は前述のデシジョンテーブルと変わりません。
これを見た目の通り実装すると、前述のデシジョンテーブルの場合よりもシンプルに実装することができます。
4.1. DecisionActionの実装
DecisionAction
インターフェースを実装さえしていれば内容に制限はないので、今回はParkingDiscountというENUMを利用するようにします。
public class ParkingDiscountAction implements DecisionAction {
private final ParkingDiscount discount;
// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。デシジョンテーブルの結果を保持するだけなのでsetterは不要。
// 内容を確認するため、toString()メソッドをeclipseで自動生成すると便利。
}
public enum ParkingDiscount {
THIRTY_MINUTE(1),
ONE_HOUR(2),
TWO_HOUR_THIRTY_MINUTE(3),
THREE_HOUR_THIRTY_MINUTE(4);
private int code;
// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。
}
4.2. Ruleの実装
前述のPriceRule1、PriceRule2、PriceRule3をまとめて実装します。
今回の戻り値は数値型(int)です。
public class PriceRule implements Rule<PriceRuleInput> {
@Override
public Object evaluate(RuleInput input) {
PriceRuleInput priceRuleInput = getInput(input);
int paymentPrice = priceRuleInput.getPaymentPrice();
if (paymentPrice >= 3000 && paymentPrice < 10000) {
return 1;
} else if (paymentPrice >= 10000 && paymentPrice < 30000) {
return 2;
} else if (paymentPrice >= 30000) {
return 3;
} else {
return 0;
}
}
}
4.3. デシジョンテーブルの定義
PriceRule
の戻り値を数値型(int)としたので、when
メソッドでもそれに合わせて値を定義します。
then
メソッドも同様に、ParkingDiscountAction
のコンストラクタがENUMとなったので、それに合わせて定義します。
public class ParkingDiscountDecisionTable extends DecisionTable<ParkingDiscountAction> {
@Override
protected Map<Integer, ConditionEntry<ParkingDiscountAction>> initDecisionTable() {
// #1
ConditionEntry<ParkingDiscountAction> pattern01 = new ConditionEntry<>();
pattern01.when(PriceRule.class, 0);
pattern01.when(CinemaRule.class, false);
pattern01.then(new ParkingDiscountAction(ParkingDiscount.THIRTY_MINUTE));
// #2
ConditionEntry<ParkingDiscountAction> pattern02 = new ConditionEntry<>();
pattern02.when(PriceRule.class, 1);
pattern02.when(CinemaRule.class, false);
pattern02.then(new ParkingDiscountAction(ParkingDiscount.ONE_HOUR));
// #3
ConditionEntry<ParkingDiscountAction> pattern03 = new ConditionEntry<>();
pattern03.when(PriceRule.class, 2);
pattern03.when(CinemaRule.class, false);
pattern03.then(new ParkingDiscountAction(ParkingDiscount.TWO_HOUR_THIRTY_MINUTE));
// #4
ConditionEntry<ParkingDiscountAction> pattern04 = new ConditionEntry<>();
pattern04.when(PriceRule.class, 3);
pattern04.when(CinemaRule.class, false);
pattern04.then(new ParkingDiscountAction(ParkingDiscount.THREE_HOUR_THIRTY_MINUTE));
// #5
ConditionEntry<ParkingDiscountAction> pattern05 = new ConditionEntry<>();
pattern05.when(PriceRule.class, 0);
pattern05.when(CinemaRule.class, true);
pattern05.then(new ParkingDiscountAction(ParkingDiscount.TWO_HOUR_THIRTY_MINUTE));
// #6
ConditionEntry<ParkingDiscountAction> pattern06 = new ConditionEntry<>();
pattern06.when(PriceRule.class, 1);
pattern06.when(CinemaRule.class, true);
pattern06.then(new ParkingDiscountAction(ParkingDiscount.TWO_HOUR_THIRTY_MINUTE));
// #7
ConditionEntry<ParkingDiscountAction> pattern07 = new ConditionEntry<>();
pattern07.when(PriceRule.class, 2);
pattern07.when(CinemaRule.class, true);
pattern07.then(new ParkingDiscountAction(ParkingDiscount.TWO_HOUR_THIRTY_MINUTE));
// #8
ConditionEntry<ParkingDiscountAction> pattern08 = new ConditionEntry<>();
pattern08.when(PriceRule.class, 3);
pattern08.when(CinemaRule.class, true);
pattern08.then(new ParkingDiscountAction(ParkingDiscount.THREE_HOUR_THIRTY_MINUTE));
// create map
Map<Integer, ConditionEntry<ParkingDiscountAction>> tables = new HashMap<>();
tables.put(pattern01.getId(), pattern01);
tables.put(pattern02.getId(), pattern02);
tables.put(pattern03.getId(), pattern03);
tables.put(pattern04.getId(), pattern04);
tables.put(pattern05.getId(), pattern05);
tables.put(pattern06.getId(), pattern06);
tables.put(pattern07.getId(), pattern07);
tables.put(pattern08.getId(), pattern08);
return tables;
}
}
4.4. デシジョンテーブルの利用
public class ParkingDiscountService {
// DIコンテナを利用するならインジェクション
PriceRule priceRule;
CinemaRule cinemaRule;
ParkingDiscountDecisionTable parkingDiscountDecisionTable;
public ParkingDiscountService() {
// インジェクションの代わりにインスタンス生成
priceRule = new PriceRule();
cinemaRule = new CinemaRule();
parkingDiscountDecisionTable = new ParkingDiscountDecisionTable();
}
public void business(int paymentPrice, boolean watchCinema) {
// create input data
PriceRuleInput priceRuleInput = new PriceRuleInput(paymentPrice);
CinemaRuleInput cinemaRuleInput = new CinemaRuleInput(watchCinema);
// create conditionStub
ConditionStub conditionStub = new ConditionStub();
conditionStub.when(priceRule, priceRuleInput);
conditionStub.when(cinemaRule, cinemaRuleInput);
// resolve by decisionTable
ParkingDiscountAction parkingDiscountAction = parkingDiscountDecisionTable.resolve(conditionStub);
System.out.println("paymentPrice : " + paymentPrice + ", watchCinema : " + watchCinema);
System.out.println(parkingDiscountAction);
}
}
4.5. 動作確認
paymentPrice : 2000, watchCinema : false
ParkingDiscountAction [discount=THIRTY_MINUTE]
paymentPrice : 5000, watchCinema : false
ParkingDiscountAction [discount=ONE_HOUR]
paymentPrice : 17000, watchCinema : false
ParkingDiscountAction [discount=TWO_HOUR_THIRTY_MINUTE]
paymentPrice : 45000, watchCinema : false
ParkingDiscountAction [discount=THREE_HOUR_THIRTY_MINUTE]
paymentPrice : 100, watchCinema : true
ParkingDiscountAction [discount=TWO_HOUR_THIRTY_MINUTE]
paymentPrice : 7000, watchCinema : true
ParkingDiscountAction [discount=TWO_HOUR_THIRTY_MINUTE]
paymentPrice : 20000, watchCinema : true
ParkingDiscountAction [discount=TWO_HOUR_THIRTY_MINUTE]
paymentPrice : 100000, watchCinema : true
ParkingDiscountAction [discount=THREE_HOUR_THIRTY_MINUTE]
5. さいごに
今回はデシジョンテーブルの見た目そのままに実装する、デシジョンテーブルの実装方法について説明しました。
コレクション(Set,Map)をうまく使うことで条件分岐を利用することなく実装することができました。
デシジョンテーブルの内容を定義するDecisionTable
クラスの実装が面倒そうですが、内容が定型的なため、実際のプロジェクトでは設計書から自動生成するのがよさそうです。