Dan
Dan

Reputation: 741

Rule Engine design with multiple predicates

I am trying to implement Rule engine desing pattern in my use case but not able to fit the pieces in the right place.

RuleEngine is where all rules are validated for a transaction before making it approved

public class RuleEngine {
    private Predicate<Transaction> predicates;

    private Transaction transaction;

    public void setTransaction(Transaction transaction){
        this.transaction = transaction;
    }

    public void addRules(Predicate<Transaction> predicates) {
        this.predicates = predicates;
    }

    public void executeRules() {
        if(predicates.test(transaction)) {
            // all rules are valided - payment success
        }
    }
}

Below Payments class is invoked by parent where transaction and its type are provided.

Then based on transaction, rules are added which is the difficult part.

Because of transactionUtils - hard dependency required to autowired, causing predicate chaining looks very ugly and seems not the correct way.

@Component
public class Payments {

    @Autowired
    PredicateHelper predicateHelper;

    public void process(Transaction transaction, String type) {
        RuleEngine ruleEngine = new RuleEngine();
        ruleEngine.setTransaction(transaction);

        switch (type) {
            case "card" :
                ruleEngine.addRules(getCardRules());
                break;
            case "cash" :
                ruleEngine.addRules(getCashRules());
                break;
            default : log.error("Invalid case");
        }
        ruleEngine.executeRules();
    }

    private Predicate<Transaction> getCardRules(){
        return predicateHelper.rule1
                .and(predicateHelper.rule2)
                .and(predicateHelper.rule3);        // Predicate chaining 
    }

    private Predicate<Transaction> getCashRules(){
        return predicateHelper.rule1
                .and(predicateHelper.rule4)
                .and(predicateHelper.rule5);        // Predicate chaining
    }
}
@Component
public class PredicateHelper {

    @Autowired
    TransactionUtils transactionUtils;      // hard dependency - in house library

    public Predicate<Transaction> rule1 = transaction -> "rule1".equals(transactionUtils.getName(transaction));
    public Predicate<Transaction> rule2 = transaction -> "rule2".equals(transactionUtils.getName(transaction));
    public Predicate<Transaction> rule3 = transaction -> "rule3".equals(transactionUtils.getName(transaction));
    public Predicate<Transaction> rule4 = transaction -> "rule4".equals(transactionUtils.getName(transaction));
    public Predicate<Transaction> rule5 = transaction -> "rule5".equals(transactionUtils.getName(transaction));
}

Is there a ways to have better predicate chaining with this solution. Thanks in advance.

Upvotes: 0

Views: 447

Answers (1)

Sagar
Sagar

Reputation: 173

You could make the dependency itself as a parameter for arguments passed in api, so when required, we could inject the component while doing validation. Also, since we want to bind the transactionType along with its rules,it deserves a class/enum for itself defining the coupling though lossely.

//Component
class RuleValidator {

    //Autowired
    TransactionUtils transactionUtils;

    boolean validate(Transaction transaction, BiPredicate<Transaction,TransactionUtils> predicate){
        return predicate.test(transaction,transactionUtils);
    }

}


public enum TransactionType {

    CARD(RulesFactory.getDefaultCardRules()),CASH(RulesFactory.getDefaultCashRules()),NO_RULES((key1,key2)->true);

    private final BiPredicate<Transaction,TransactionUtils> rulesApplied;

    TransactionType(BiPredicate<Transaction, TransactionUtils> rulesApplied) {
        this.rulesApplied = rulesApplied;
    }

    public BiPredicate<Transaction, TransactionUtils> getRulesApplied() {
        return rulesApplied;
    }

    public BiPredicate<Transaction, TransactionUtils> addAdditionalPredicate(BiPredicate<Transaction,TransactionUtils> ... additional ) {
        BiPredicate<Transaction, TransactionUtils> additionalValidation = Arrays.stream(additional)
                                                                                .reduce((k, v) -> true, BiPredicate::and);
        return additionalValidation.and(rulesApplied);
    }

}

 interface RulesFactory {

     public static boolean rule1(Transaction transaction,TransactionUtils transactionUtils) {
         return "rule1".equals(transactionUtils.getName(transaction));
     }

     public static boolean rule2(Transaction transaction,TransactionUtils transactionUtils) {
         return "rule2".equals(transactionUtils.getName(transaction));
     }

     public static boolean rule5(Transaction transaction,TransactionUtils transactionUtils) {
         return "rule5".equals(transactionUtils.getName(transaction));
     }

     public static BiPredicate<Transaction, TransactionUtils> getDefaultCardRules(){
       BiPredicate<Transaction,TransactionUtils> andPredicate = (currentTransaction,transactionUtilsComponent) -> true;
       return andPredicate.and(RulesFactory::rule1)
                    .and(RulesFactory::rule2);
     }

     public static BiPredicate<Transaction, TransactionUtils> getDefaultCashRules() {
         BiPredicate<Transaction,TransactionUtils> andPredicate = (currentTransaction,transactionUtilsComponent) -> true;
         return andPredicate.and(RulesFactory::rule2)
                 .and(RulesFactory::rule5);
     }

 }
class RuleEngine{
    
    RuleValidator ruleValidator;

    private Transaction transaction;

    private TransactionType transactionType;

    public void setTransaction(Transaction transaction){
        this.transaction = transaction;
    }

    public void setTransactionType(TransactionType transactionType){
        this.transactionType = transactionType;
    }

    public void executeRules() {
        if(ruleValidator.validate(transaction,transactionType.getRulesApplied())) {
            // all rules are valided - payment success
        }
    }
}

Upvotes: 0

Related Questions