Groovy & Grails Rules Engine
Groovy Rules Engine Programming Grails- * Extendable engine that can be used in any scenario.
- * Typeless rules that can be added or removed at runtime. "convention over configuration"
- * 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