Decision Table Modeling and Evaluation in Power Flows DMN
Let's look at decision table modeling and evaluation in Power Flows DMN.
Join the DZone community and get the full member experience.
Join For FreePower Flows, being a full-fledged and versatile decision engine, allows modeling decision tables and their evaluation. The source code is available on GitHub, and the home page can be found at Power Flows. The decision engine itself was written in Java. The latest version, 2.0.0, supports the modularity of dependencies, and this article will be based on it.
Decisions Modeling
For modeling, you can use several approaches supported by the DMN Power Flows. This is, among others:
YAML file — on YAML syntax basis. Very concise and precise format, which I personally recommend
Java or Groovy file — here there are two approaches. Fluent style and functional style. Both work great in specific cases, especially when you prefer the modeling in the languages in which you feel best and can be either Java or very close to Groovy. This approach gives us automatically highlighting a syntax, possible and available fields, etc. if you use one of the leading IDEs, for example, Eclipse, NetBeans, IntelliJ IDEA. This modeling approach is extremely easy.
DSL in Kotlin — this is a novelty that came with version 2.0.0. Thanks to DSL, it is possible to model using a concise syntax, and at the same time, gets a suggestion of available properties. No advanced Kotlin knowledge and experience is required here.
XML file — supported for the purpose of compliance with the original OMG DMN definition defined on XML basis, but due to the fact of wordy form of the notation, it is not personally recommended, it is useful when you have an existing project that uses a different XML-only decision engine, and you would like to quickly make your decision tables run on Power Flows DMN before you start modeling in the recommended formats. Unfortunately, some elements from the XML of other decision engine vendors may not be supported.
For the purposes of this article, I will use sample models from the project repository. You can decide on one of the below examples and go further to an evaluation step.
YAML File Modeling Approach
id: loan_qualifier
name: Loan qualifier
hit-policy: COLLECT
expression-type: FEEL
fields:
in:
age:
type: INTEGER
activeLoansNumber:
description: Number of active loans on user's account
type: INTEGER
expression-type: LITERAL
startDate:
type: DATE
out:
loanAmount:
description: Loan amount in Euro
type: DOUBLE
loanTerm:
description: Loan term in months
type: INTEGER
rules:
- description: Loan for 18 years
in:
age: 18
activeLoansNumber: 0
startDate: '[date and time("2019-01-01T12:00:00")..date and time("2019-12-31T12:00:00")]'
out:
loanAmount: 10000
loanTerm: 12
- in:
age: 18
startDate: '[date and time("2019-03-01T12:00:00")..date and time("2019-03-31T12:00:00")]'
out:
loanAmount: 15000
loanTerm: 6
- description: Loan for older than 18 years
in:
age: '>18'
out:
loanAmount: 20000
loanTerm: 12
Java/Groovy Fluet Style Modeling Approach
Decision decision = Decision.fluentBuilder()
.id("loan_qualifier")
.name("Loan qualifier")
.hitPolicy(HitPolicy.COLLECT)
.expressionType(ExpressionType.FEEL)
.withInputs()
.name("age")
.type(ValueType.INTEGER)
.next()
.name("activeLoansNumber")
.description("Number of active loans on user's account")
.type(ValueType.INTEGER)
.withExpression()
.type(ExpressionType.LITERAL)
.and()
.next()
.name("startDate")
.type(ValueType.DATE)
.end()
.withOutputs()
.name("loanAmount")
.description("Loan amount in Euro")
.type(ValueType.DOUBLE)
.next()
.name("loanTerm")
.description("Loan term in months")
.type(ValueType.INTEGER)
.end()
.withRules()
.description("Loan for 18 years")
.withInputEntries()
.name("age")
.withExpression()
.type(ExpressionType.FEEL)
.value(18)
.and()
.next()
.name("activeLoansNumber")
.evaluationMode(EvaluationMode.INPUT_COMPARISON)
.withExpression()
.type(ExpressionType.LITERAL)
.value(0)
.and()
.next()
.name("startDate")
.withExpression()
.type(ExpressionType.FEEL)
.value("[date and time(\"2019-01-01T12:00:00\")..date and time(\"2019-12-31T12:00:00\")]")
.and()
.end()
.withOutputEntries()
.name("loanAmount")
.withExpression()
.type(ExpressionType.LITERAL)
.value(10000)
.and()
.next()
.name("loanTerm")
.withExpression()
.type(ExpressionType.LITERAL)
.value(12)
.and()
.end()
.next()
.withInputEntries()
.name("age")
.evaluationMode(EvaluationMode.INPUT_COMPARISON)
.withExpression()
.type(ExpressionType.LITERAL)
.value(18)
.and()
.next()
.name("startDate")
.withExpression()
.type(ExpressionType.FEEL)
.value("[date and time(\"2019-03-01T12:00:00\")..date and time(\"2019-03-31T12:00:00\")]")
.and()
.end()
.withOutputEntries()
.name("loanAmount")
.withExpression()
.type(ExpressionType.LITERAL)
.value(15000)
. and()
.next()
.name("loanTerm")
.withExpression()
.type(ExpressionType.LITERAL)
.value(6)
.and()
.end()
.next()
.withInputEntries()
.name("age")
.withExpression()
.type(ExpressionType.FEEL)
.value(">18")
.and()
.end()
.withOutputEntries()
.name("loanAmount")
.withExpression()
.type(ExpressionType.LITERAL)
.value(20000)
.and()
.next()
.name("loanTerm")
.withExpression()
.type(ExpressionType.LITERAL)
.value(12)
.and()
.end()
.end()
.build();
Power Flows DMN has prepared a set of comprehensive builders. The built code looks very clear and readable. Of course, it has many advantages. However, the disadvantage is the need for manual formatting. IDE environments are not yet able to format such extended fluent syntax for builders. In order to protect your manually formatted code, i.e. in IntelliJ IDEA, you can use respectively before a start of modeling:
// @formatter:off
and after:
// @formatter:on
Then IntelliJ IDEA will not format your code.
Java/Groovy Functional Style Modeling Approach
Decision decision = Decision.builder()
.id("loan_qualifier")
.name("Loan qualifier")
.hitPolicy(HitPolicy.COLLECT)
.expressionType(ExpressionType.FEEL)
.withInput(in -> in
.name("age")
.type(ValueType.INTEGER)
.build())
.withInput(in -> in
.name("activeLoansNumber")
.description("Number of active loans on user's account")
.type(ValueType.INTEGER)
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.build())
.build())
.withInput(in -> in
.name("startDate")
.type(ValueType.DATE)
.build())
.withOutput(out -> out
.name("loanAmount")
.description("Loan amount in Euro")
.type(ValueType.DOUBLE)
.build())
.withOutput(out -> out
.name("loanTerm")
.description("Loan term in months")
.type(ValueType.INTEGER)
.build())
.withRule(rule -> rule
.description("Loan for 18 years")
.withInputEntry(in -> in
.name("age")
.evaluationMode(EvaluationMode.INPUT_COMPARISON)
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(18)
.build())
.build())
.withInputEntry(in -> in
.name("activeLoansNumber")
.evaluationMode(EvaluationMode.INPUT_COMPARISON)
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(0)
.build())
.build())
.withInputEntry(in -> in
.name("startDate")
.withExpression(ex -> ex
.type(ExpressionType.FEEL)
.value("[date and time(\"2019-01-01T12:00:00\")..date and time(\"2019-12-31T12:00:00\")]")
.build())
.build())
.withOutputEntry(out -> out
.name("loanAmount")
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(10000)
.build())
.build())
.withOutputEntry(out -> out
.name("loanTerm")
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(12)
.build())
.build())
.build())
.withRule(rule -> rule
.withInputEntry(in -> in
.name("age")
.withExpression(ex -> ex
.type(ExpressionType.FEEL)
.value(18)
.build())
.build())
.withInputEntry(in -> in
.name("startDate")
.withExpression(ex -> ex
.type(ExpressionType.FEEL)
.value("[date and time(\"2019-03-01T12:00:00\")..date and time(\"2019-03-31T12:00:00\")]")
.build())
.build())
.withOutputEntry(out -> out
.name("loanAmount")
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(15000)
.build())
.build())
.withOutputEntry(out -> out
.name("loanTerm")
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(6)
.build())
.build())
.build())
.withRule(rule -> rule
.withInputEntry(in -> in
.name("age")
.withExpression(ex -> ex
.type(ExpressionType.FEEL)
.value(">18")
.build())
.build())
.withOutputEntry(out -> out
.name("loanAmount")
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(20000)
.build())
.build())
.withOutputEntry(out -> out
.name("loanTerm")
.withExpression(ex -> ex
.type(ExpressionType.LITERAL)
.value(12)
.build())
.build())
.build())
.build();
Kotlin DSL Style Modeling Approach
val decision = decision {
id = "loan_qualifier"
name = "Loan qualifier"
hitPolicy = HitPolicy.COLLECT
expressionType = ExpressionType.FEEL
inputs {
input("age") {
type = ValueType.INTEGER
}
input("activeLoansNumber") {
description = "Number of active loans on user's account"
type = ValueType.INTEGER
expression(ExpressionType.LITERAL)
}
input("startDate") {
type = ValueType.DATE
}
}
outputs {
output("loanAmount") {
description = "Loan amount in Euro"
type = ValueType.DOUBLE
}
output("loanTerm") {
description = "Loan term in months"
type = ValueType.INTEGER
}
}
rules {
rule {
input("age") {
value = 18
}
input("activeLoansNumber")
input("startDate") {
expression("[date and time(\"2019-01-01T12:00:00\")..date and time(\"2019-12-31T12:00:00\")]")
}
output("loanAmount") {
value = 10000
}
output("loanTerm") {
value = 12
}
}
rule {
input("age") {
value = 18
}
input("startDate") {
expression("[date and time(\"2019-03-01T12:00:00\")..date and time(\"2019-03-31T12:00:00\")]")
}
output("loanAmount") {
value = 15000
}
output("loanTerm") {
value = 6
}
}
rule {
description = "Loan for older than 18 years"
input("age") {
expression(">18")
}
output("loanAmount") {
value = 20000
}
output("loanTerm") {
value = 12
}
}
}
}
Reading YAML File to Decision Model
In case you decided to go with YAML, having the decision model in *.yaml file, Power Flows DMN allows to read it and convert to Decision
class as follows:
File loanQualifierFile = new File("loan-qualifier.yml");
InputStream loanQualifierInputStream = new FileInputStream(loanQualifierFile);
Decision decision = new YamlDecisionReader().read(loanQualifierInputStream).get();
Decision Evaluation
The next step is the evaluation process of the prepared model as Decision
class object. To do this, you can use the appropriate API provided with the decision engine:
DecisionEngine decisionEngine = new DefaultDecisionEngineConfiguration().configure();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Map<String, Serializable> variables = new HashMap<>();
variables.put("age", 18);
variables.put("activeLoansNumber", 0);
variables.put("startDate", format.parse("2019-01-05"));
DecisionVariables decisionVariables = new DecisionVariables(variables);
DecisionResult decisionResult = decisionEngine.evaluate(decision, decisionVariables);
At the end having the evaluation decisionResult
, you can operate on it using the provided methods:
decisionResult.isSingleEntryResult();
decisionResult.isSingleRuleResult();
decisionResult.isCollectionRulesResult();
decisionResult.getSingleEntryResult();
decisionResult.getSingleRuleResult();
decisionResult.getCollectionRulesResult();
Firstly, it is a good practice to check how many entries the result contains. For this, you can call i.e. isCollectionRulesResult()
method.
And if true
, get the whole collection using getCollectionRulesResult()
method. And now you can process the result according to your need. DecisionResult
contains a list of RuleResult
and each RuleResult
has EntryResult
with name and value.
Opinions expressed by DZone contributors are their own.
Comments