Groovy & Grails Rules Engine

Groovy  Rules Engine  Programming  Grails 
Aug 10, 2014
Goals:
  1. * Extendable engine that can be used in any scenario.
  2. * Typeless rules that can be added or removed at runtime. "convention over configuration"
  3. * Decoupled decision that can ask for 1 or more rules engine results.

Design with a soldier's promotion/demotion implementation

Lets start with an interface to define the basic contract for a rules engine.

public interface IRulesEngine {
    def apply(obj)
}

Here is a concrete base implementation of our rules engine. All of our business derived rules engines will extend this. Methods with the word "Rule" at the end will be executed when calling the apply() method.

class RulesEngine implements IRulesEngine {

    def getRules() {
        def rules = []
        this.class.declaredFields.each {
            def field = this."${it.name}"
            if (!it.isSynthetic() && field instanceof Closure && it.name.endsWith("Rule")) {
                rules << it.name
            }
        }
        rules
    }

    def apply(obj) {
        Set responseSet = [] as Set
        rules.each { rule ->
            responseSet << this."$rule"(obj)
        }
        responseSet
    }
}

We can start off with 2 engines. Ones which return promotable and demoteable traits.

class PromotableTraitsRulesEngineService extends RulesEngine {

    // example of a rule that returns many traits
    def heroOnBattlefieldRule = { soldier ->

        def traits = []

        log.info("heroOnBattlefieldRule() ran")

        War*.campaigns*.battles.collect { battle -> battle.heros in soldier}.each { battle ->
            traits << """heroOnBattlefield - ${battle.campaign.war} ${battle.campaign}"""
        }

        return traits
    }
}
class DemotableTraitsRulesEngineService extends RulesEngine {

    // example of a rule that returns many traits
    def insubordinationOnLeaveRule = { soldier ->

        def traits = []

        log.info("insubordinationOnLeaveRule() ran")

        soldier.leaves.findAll{it.insubordination == true}.each { leave ->
            traits << """insubordinationOnLeave - ${leave.country} ${leave.city} ${leave.place}"""
        }

        return traits
    }

    // example of a rule that returns 1 traits
    def insubordinationDuringBattleRule = { soldier ->

        def traits = []

        log.info("insubordinationDuringBattleRule() ran")

        def hasInsubordinationDuringBattleRule = War*.campaigns*.battles*.insubordinates.find { insubordinate ->
            insubordinate.soldier == soldier
        }

        if (hasInsubordinationDuringBattleRule)
            traits << "insubordinationDuringBattle"

        return traits

    }
}

Now we can create a service that aggregates and applies these traits.

class PromotionRulesEngineService implements IRulesEngine {

    def promotableTraitsRulesEngineService
    def demotableTraitsRulesEngineService

    def apply( soldier ){
        
        def traits = []
        
        traits << promotableTraitsRulesEngineService.apply( soldier )
        traits << demotableTraitsRulesEngineService.apply( soldier )
        
        return traits
    }
}

Lets get on to our decision already...

class PromotionService {

    enum Decision {PROMOTE, DEMOTE, NONE, DISHONORABLE_DISCHARGE}

    def promotionRulesEngineService

    def decide(soldier) {

        def traits = promotionRulesEngineService.apply(soldier)

        if (traits.collect{it == "insubordinationDuringBattle"}.count())
            return Decision.DISHONORABLE_DISCHARGE

        if (traits.collect{it.contains "insubordinationOnLeave"}.count() < 10  
        && traits.collect{it.contains "heroOnBattlefield"}.count() > 0 )
            return Decision.PROMOTE

        if (traits.collect{it.contains "insubordinationOnLeave"}.count() > 3  
        && traits.collect{it.contains "heroOnBattlefield"}.count() == 0 )
            return Decision.DEMOTE

        return Decision.NONE
    }

}
I hope you enjoyed this solution. You can clone it here on github