Java
アーキテクチャ
デシジョンテーブル
プログラミング初心者

条件分岐を使わずにデシジョンテーブルを実現する


1. はじめに

今回はデシジョンテーブルで整理された仕様を実装する機会があったので、その定義の通りに動くプログラムを実装する方法について説明したいと思います。

デシジョンテーブルとは、ある問題に対して、考えられる条件とその際の動作を表にまとめたものです。詳細については「デシジョンテーブルの解説」のサイトが分かりやすいので、そちらを参照ねがいます。

今回の題材とするデシジョンテーブル(駐車場料金の割引計算)と以下の図についても、このサイトから引用させて頂きました。

条件記述部(condition stub)
条件指定部(condition entry)

photo_2.png
photo_4.png

動作記述部(action stub)
動作指定部(action entry)

photo_3.png
photo_5.png

今回の記事で説明する内容は以下の通りです。


2. デシジョンテーブルの実装

class-structure.jpg

条件の1つ1つをRuleとしたところは独自ですが、なるべくデシジョンテーブルの見た目と整合性が取れるようにクラスデザインをしてみました。

結果として6つのクラス(インターフェースが3つ、抽象クラスが1つ)で実現することができました。

内容を見てもらえば分かりますが、Javaの標準機能だけ、かつ、条件分岐なしで実装しています。


2.1. DecisionAction

デシジョンテーブルの判定結果である動作(アクション)を定義するためのインターフェースです。内容は実現したいデシジョンテーブルに応じて自由に定義します。


DecisionAction.java

public interface DecisionAction {

}



2.2. RuleInput

Ruleの入力データを定義するためのインターフェースです。


RuleInput.java

public interface RuleInput {

}



2.3. Rule

条件の判断ロジックを実装するためのインターフェースです。

Object evaluate(RuleInput input)メソッドに1つの条件を判断する処理を実装します。戻り値は条件指定部の判定で利用する値になります。Javaの標準データ型(真偽値、文字列型、数値型)であれば何でも構いません。


Rule.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の単なるホルダーです。


ConditionStub.java

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つ分を保持します。

ポイントとしては一意に特定するためにSethashCodeをidとして利用するところです。


ConditionEntry.java

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に該当するものがデシジョンテーブルに登録されているかチェックしているだけです。


DecisionTable.java

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. 駐車場料金の割引計算デシジョンテーブルの実装

photo.png

「駐車場料金の割引計算」デシジョンテーブルを実装してみます。

本当に見た目の通り実装することになります。もっとシンプルに実装したい場合は以下の「4. 駐車場料金の割引計算デシジョンテーブルの実装(シンプル版)」を参照してください。


  • ParkingDiscountAction : 割引結果となる動作を定義します。

  • PriceRule1 : 「3000円以上10000円未満」を判断する処理ロジックを実装します。

  • PriceRule2 : 「10000円以上30000円未満」を判断する処理ロジックを実装します。

  • PriceRule3 : 「30000円以上」を判断する処理ロジックを実装します。

  • CinemaRule : 「シネマ利用」を判断する処理ロジックを実装します。


3.1. DecisionActionの実装


ParkingDiscountAction.java

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の実装


PriceRuleInput.java

public class PriceRuleInput implements RuleInput {

private final int paymentPrice;

// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。入力値は不変なのでsetterは不要。
}



PriceRule1.java

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;
}
}



PriceRule2.java

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;
}
}



PriceRule3.java

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;
}
}



CinemaRuleInput.java

public class CinemaRuleInput implements RuleInput {

private final boolean watchCinema;

// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。入力値は不変なのでsetterは不要。
}



CinemaRule.java

public class CinemaRule implements Rule<CinemaRuleInput> {

@Override
public Object evaluate(RuleInput input) {
CinemaRuleInput cinemaRuleInput = getInput(input);
return cinemaRuleInput.isWatchCinema();
}
}



3.3. デシジョンテーブルの定義

class-structure.jpg

デシジョンテーブルの赤枠のConditionEntryの通りに定義します。

条件はwhen()メソッドで追加し、その際の動作をthen()メソッドで定義します。

デシジョンテーブルの#1~#8の8つのデータパターンの定義が終わった後、それぞれをMapに追加します。その際のキーにはConditionEntrygetId()で取得したidとします。


ParkingDiscountDecisionTable.java

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のインスタンスを生成した後、ParkingDiscountDecisionTableresolve()メソッドに引数として渡して実行するだけです。

実行中にデシジョンテーブルの定義が変わることはないため、ParkingDiscountDecisionTableのインスタンスは毎回生成するではなく、使いまわすことを推奨します。


ParkingDiscountService.java

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. 動作確認


Demo.java

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. 駐車場料金の割引計算デシジョンテーブルの実装(シンプル版)

compact-desition.jpg

これをデシジョンテーブルと呼ぶのか分かりませんが、書かれている仕様は前述のデシジョンテーブルと変わりません。

これを見た目の通り実装すると、前述のデシジョンテーブルの場合よりもシンプルに実装することができます。


4.1. DecisionActionの実装

DecisionActionインターフェースを実装さえしていれば内容に制限はないので、今回はParkingDiscountというENUMを利用するようにします。


ParkingDiscountAction.java

public class ParkingDiscountAction implements DecisionAction {

private final ParkingDiscount discount;

// コンストラクタは省略。フィールドを引数とするコンストラクタを用意。
// getterは省略。デシジョンテーブルの結果を保持するだけなのでsetterは不要。
// 内容を確認するため、toString()メソッドをeclipseで自動生成すると便利。
}



ParkingDiscount.java

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)です。


PriceRule.java

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となったので、それに合わせて定義します。


ParkingDiscountDecisionTable.java

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. デシジョンテーブルの利用


ParkingDiscountService.java

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クラスの実装が面倒そうですが、内容が定型的なため、実際のプロジェクトでは設計書から自動生成するのがよさそうです。