This time I had the opportunity to implement the specifications organized in the decision table, so I would like to explain how to implement a program that works according to the definition.
A decision table is a table that summarizes possible conditions and actions for a certain problem. For details, please refer to the "Decision Table Explanation" site, which is easy to understand. The decision table (calculation of parking lot fee discount) and the following figure, which are the subjects of this time, are also quoted from this site.
Condition description part (condition stub) | Condition entry |
---|---|
Action description part (action stub) | Action entry |
The contents explained in this article are as follows.
It is unique that each of the conditions is set to Rule, but I tried to design the class so that it would be as consistent as the appearance of the decision table. As a result, we were able to realize it with 6 classes (3 interfaces and 1 abstract class). As you can see from the contents, it is implemented only with standard Java functions and without conditional branching.
2.1. DecisionAction
It is an interface for defining the action that is the judgment result of the decision table. The content can be freely defined according to the decision table you want to realize.
DecisionAction.java
public interface DecisionAction {
}
2.2. RuleInput
Interface for defining the input data of Rule.
RuleInput.java
public interface RuleInput {
}
2.3. Rule
An interface for implementing the condition judgment logic. ʻImplement the process to judge one condition in the Object evaluate (RuleInput input)` method. The return value is the value used in the judgment of the condition specification part. It can be any standard Java data type (validity, string, numeric).
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
This class is an argument when executing conditional judgment. As you can see, it's just a holder for Rule and Rule Input.
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
It is a class that defines the condition description part of the decision table and the operation determined at that time.
As a data retention item, one of the data patterns defined in # 1 to # 8 is retained.
The point is to use the hashCode
of Set
as the id to uniquely identify it.
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
A class that defines a decision table.
As data, it only holds ConditionEntry
in Map. If there are 8 data patterns, # 1 to # 8
, 8ConditionEntry
s are stored in Map.
When actually using it, define the data of the decision table with ʻinitDecisionTable ()`.
The process that determines the action of the decision table is the resolve
method.
As you can see from the contents, all you have to do is execute Rule sequentially to create an instance of ConditionEntry
and check if the one corresponding to that id is registered in the decision table.
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;
}
}
Let's implement the "Parking fee discount calculation" decision table. It will be implemented as it really looks. If you want to implement it more simply, refer to "[4. Implementation of parking lot fee discount calculation decision table (simple version)](# 4-Implementation of parking lot fee discount calculation decision table simple version)" Please give me.
ParkingDiscountAction.java
public class ParkingDiscountAction implements DecisionAction {
private final boolean discount30Minute;
private final boolean discount1Hour;
private final boolean discount2Hour30Minute;
private final boolean discount3Hour30Minute;
//The constructor is omitted. Prepare a constructor that takes a field as an argument.
//getter omitted. No setter is needed as it only holds the results of the decision table.
//ToString to check the contents()It is convenient to automatically generate the method with eclipse.
}
PriceRuleInput.java
public class PriceRuleInput implements RuleInput {
private final int paymentPrice;
//The constructor is omitted. Prepare a constructor that takes a field as an argument.
//getter omitted. The input value is immutable, so no setter is required.
}
PriceRule1.java
public class PriceRule1 implements Rule<PriceRuleInput> {
//Processing logic to judge "3000 yen or more and less than 10000 yen"
@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> {
//Processing logic to judge "10,000 yen or more and less than 30,000 yen"
@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> {
//Processing logic to judge "30,000 yen or more"
@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;
//The constructor is omitted. Prepare a constructor that takes a field as an argument.
//getter omitted. The input value is immutable, so no setter is required.
}
CinemaRule.java
public class CinemaRule implements Rule<CinemaRuleInput> {
@Override
public Object evaluate(RuleInput input) {
CinemaRuleInput cinemaRuleInput = getInput(input);
return cinemaRuleInput.isWatchCinema();
}
}
Define as ConditionEntry
in the red frame of the decision table.
The condition is added by the when ()
method, and the operation at that time is defined by the then ()
method.
After defining the eight data patterns # 1 to # 8 in the decision table, add each to the Map. The key at that time is the id obtained by getId ()
of ConditionEntry
.
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;
}
}
Using the decision table is easy, just create an instance of ConditionStub
and then pass it as an argument to theresolve ()
method of ParkingDiscountDecisionTable
and execute it.
Since the definition of the decision table does not change during execution, it is recommended to reuse the instance of ParkingDiscountDecisionTable
instead of creating it every time.
ParkingDiscountService.java
public class ParkingDiscountService {
//Injection if using DI container
PriceRule1 priceRule1;
PriceRule2 priceRule2;
PriceRule3 priceRule3;
CinemaRule cinemaRule;
ParkingDiscountDecisionTable parkingDiscountDecisionTable;
public ParkingDiscountService() {
//Instantiate instead of injection
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);
}
}
Demo.java
public class Demo {
public static void main(String[] args) {
//If you are using a DI container, get it from there
//This time create an instance on the spot
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);
}
}
Execution result
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]
I don't know if this is called a decision table, but the specifications written are the same as the decision table mentioned above. If you implement this as it looks, it will be simpler than the decision table mentioned above.
As long as the DecisionAction
interface is implemented, there is no limit to the content, so this time we will use an ENUM called ParkingDiscount.
ParkingDiscountAction.java
public class ParkingDiscountAction implements DecisionAction {
private final ParkingDiscount discount;
//The constructor is omitted. Prepare a constructor that takes a field as an argument.
//getter omitted. No setter is needed as it only holds the results of the decision table.
//ToString to check the contents()It is convenient to automatically generate the method with 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;
//The constructor is omitted. Prepare a constructor that takes a field as an argument.
//getter omitted.
}
Implement PriceRule1, PriceRule2, and PriceRule3 mentioned above together. The return value this time is a numeric type (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;
}
}
}
Since the return value of PriceRule
is a numeric type (int), the value is defined accordingly in the when
method.
Similarly, for the then
method, the constructor for ParkingDiscountAction
is now ENUM, so define it accordingly.
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;
}
}
ParkingDiscountService.java
public class ParkingDiscountService {
//Injection if using DI container
PriceRule priceRule;
CinemaRule cinemaRule;
ParkingDiscountDecisionTable parkingDiscountDecisionTable;
public ParkingDiscountService() {
//Instantiate instead of injection
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);
}
}
Execution result
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]
This time, I explained how to implement the decision table, which is implemented as it looks.
By making good use of collections (Set, Map), we were able to implement it without using conditional branching.
Implementation of the DecisionTable
class that defines the contents of the decision table seems to be troublesome, but since the contents are standard, it seems better to automatically generate it from the design document in an actual project.
Recommended Posts