[JAVA] Realize a decision table without using conditional branching

1.First of all

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
photo_2.png photo_4.png
Action description part (action stub) Action entry
photo_3.png photo_5.png

The contents explained in this article are as follows.

2. Implementation of decision table

class-structure.jpg

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, 8ConditionEntrys 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;
    }
}

3. Implementation of discount calculation decision table for parking lot fee

photo.png

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.

3.1. Implementation of Decision Action

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

3.2. Implementation of RuleInput and Rule

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

3.3. Definition of decision table

class-structure.jpg

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

3.4. Use of decision table

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

3.5. Operation check

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]

4. Implementation of discount calculation decision table for parking lot fee (simple version)

compact-desition.jpg

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.

4.1. Implementation of Decision Action

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

4.2. Implementation of Rule

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

4.3. Definition of decision table

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

4.4. Use of decision table

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

4.5. Operation check

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]

5. Finally

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

Realize a decision table without using conditional branching
Program using conditional branching
A little complicated conditional branching
How to join a table without using DBFlute and sql
Conditional branching with a flowing interface
[Android] Create a sliding menu without using NavigationView
Try using conditional branching other than if statement
When performing a full outer join without using a full outer join
Find the remainder divided by 3 without using a number