A Complete Tutorial on the Drools Business Rule Engine
Check out this post for a complete guide to the Drools business rule engine.
Join the DZone community and get the full member experience.
Join For FreeBusiness rules work very well to represent the logic for certain domains. They work well because they result intuitive and close to the way of thinking of many types of domain experts. The reason for that it is that they permit to decompose a large problem in single components. In this way, the user does not deal with the orchestration of all the single rules: this is the added value provided by the business rule engine.
In this article, we will discuss one specific example of an application written by using business rules. We will write the rules to decide, which email to send to the subscribers to a newsletter. We will see different types of rules and how we could express them using the Drools Rule Language. We will also see how to configure Drools (spoiler: it will be easy) and have the system elaborate on the rules to produce a result we can use.
I think that business rules are quite interesting because they permit to look at problems in a different way. As developers, we are very used to the imperative paradigm or functional paradigms. However, there are other paradigms, like state machines and business rules, which are not so commonly used and which can be a much better fit in some contexts.
As always we share the code presented in the tutorial in a companion repository: EmailSchedulingRules.
What Problem We Are Trying to Solve
Let’s consider the domain of email marketing. As marketers, we have an email list of persons interested in our content. Each of them may have demonstrated an interest in a specific topic, read some of our articles and bought certain products. Considering all their history and preferences, we want to send to them the most appropriate content each time. This content may be either educative or proposing some deal. The problem is that there are constraints we want to consider (i.e. not sending emails on Sunday or not sending emails promoting a product to someone who already bought it).
All these rules are simple per se, but the complexity derives by how they are combined and how they interact. The business rule engine will deal with that complexity for us, all we have to do is to express clearly the single rules. Rules will be expressed in the terms of our domain data so let’s focus on our domain model first.
The Model of Our Domain
In our domain model, we have:
- Emails: the single emails we want to send, described by their title and content
- Email Sequences: groups of emails that have to be sent in a specific order, for example, a set of emails representing a tutorial or describing different features of a product
- Subscribers: the single subscriber to the mailing list. We will need to know which emails we sent to him, what things he is interested in, and which products he bought
- Products: the products we sell
- Purchases: the purchases subscribers have made
- Email Sending: the fact we sent, or are about to send, a certain email, on a certain date to a certain subscriber
- Email Scheduling: the plan for sending an email, with some additional information
The latter two elements of our domain model could seem less obvious compared to the others, but we will see in the implementation for which reasons we need them.
What Our System Should Do
Our system should execute all the rules, using the Drools engine, and to determine for each user which email we should send on a specific day. The result could be the decision to not send any email, or to send an email, selecting one among many possible emails.
An important thing to consider is that these rules may evolve over time. The people in charge of marketing may want to try new rules and see how they affect the system. Using Drools, it should be easy for them to add or remove rules or tweak the existing rules.
Let’s stress this out:
These domain experts should be able to experiment with the system and try things out quickly, without always needing help from developers.
The Rules
Ok, now that we know which data do we have, we can express rules based on that model.
Let’s see some examples of rules we may want to write:
- We may have sequences of emails, for example, the content of a course. They have to be sent in order
- We may have time-sensitive emails that should either be sent in a specific time window or not sent at all
- We may want to avoid sending emails on specific days of the week, for example, on the public holidays in the country where the subscriber is based
- We may want to send certain types of emails (for example proposing a deal) only to persons who received certain other emails (for example at least 3 informative emails on the same subject)
- We do not want to propose a deal on a certain product to a subscriber who has already bought that product
- We may want to limit the frequency we send emails to users. For example, we may decide to not send an email to a user if we have sent already one in the last five days
Setting up Drools
Setting up drools can be very simple. We are looking into running drools in a standalone application. Depending on your context, this may or may not be an acceptable solution, and in some cases, you will have to look into JBoss, the application server supporting Drools. However, if you want to get started, you can forget all of this and just configure your dependencies using Gradle (or Maven). You can figure out the boring configuration bits later if you really have to.
buildscript {
ext.droolsVersion = "7.20.0.Final"
repositories {
mavenCentral()
}
}
plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.21"
}
apply plugin: 'java'
apply plugin: 'idea'
group 'com.strumenta'
version '0.1.1-SNAPSHOT'
repositories {
mavenLocal()
mavenCentral()
maven {
url 'https://repository.jboss.org/nexus/content/groups/public/'
}
}
dependencies {
compile "org.kie:kie-api:${droolsVersion}"
compile "org.drools:drools-compiler:${droolsVersion}"
compile "org.drools:drools-core:${droolsVersion}"
compile "ch.qos.logback:logback-classic:1.1.+"
compile "org.slf4j:slf4j-api:1.7.+"
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "org.jetbrains.kotlin:kotlin-reflect"
testImplementation "org.jetbrains.kotlin:kotlin-test"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
}
In our Gradle script, we use:
- Kotlin because Kotlin rocks!
- IDEA because it is my favorite IDE
- Kotlin StdLib, reflect and test
- Drools
And this is how our program will be structured:
fun main(args: Array<String>) {
try {
val kbase = readKnowledgeBase(listOf(
File("rules/generic.drl"),
File("rules/book.drl")))
val ksession = kbase.newKieSession()
// typically we want to consider today but we may decide to schedule
// emails in the future or we may want to run tests using a different date
val dayToConsider = LocalDate.now()
loadDataIntoSession(ksession, dayToConsider)
ksession.fireAllRules()
showSending(ksession)
} catch (t: Throwable) {
t.printStackTrace()
}
}
Pretty simple, and pretty neat.
What we do, in detail, is:
- We load the rules from a file. For now, we just load the file
rules/generic.drl
- We set up a new session. Think of the session as the universe as seen by the rules: all data they can access is there
- We load our data model into the session
- We fire all the rules. They could change stuff in the session
- We read the modified data model (a.k.a. the session) to figure out which emails we should send today
Writing the Classes for the Data Model
We have previously seen what our data model looks like; let’s now see the code for it.
Given we are using Kotlin, it will be pretty concise and obvious.
package com.strumenta.funnel
import java.time.DayOfWeek
import java.time.LocalDate
import java.util.*
enum class Priority {
TRIVIAL,
NORMAL,
IMPORTANT,
VITAL
}
data class Product(val name: String,
val price: Float)
data class Purchase(val product: Product,
val price: Float,
val date: LocalDate)
data class Subscriber(val name: String,
val subscriptionDate: LocalDate,
val country: String,
val email: String = "$name@foo.com",
val tags: List<String> = emptyList(),
val purchases: List<Purchase> = emptyList(),
val emailsReceived: MutableList<EmailSending> = LinkedList()) {
val actualEmailsReceived
get() = emailsReceived.map { it.email }
fun isInSequence(emailSequence: EmailSequence) =
hasReceived(emailSequence.first)
&& !hasReceived(emailSequence.last)
fun hasReceived(email: Email) = emailsReceived.any { it.email == email }
fun hasReceivedEmailsInLastDays(nDays: Long, day: LocalDate)
: Boolean {
return emailsReceived.any {
it.date.isAfter(day.minusDays(nDays))
}
}
fun isOnHolidays(date: LocalDate) : Boolean {
return date.dayOfWeek == DayOfWeek.SATURDAY
|| date.dayOfWeek == DayOfWeek.SUNDAY
}
fun emailReceivedWithTag(tag: String) =
emailsReceived.count { tag in it.email.tags }
}
data class Email(val title: String,
val content: String,
val tags: List<String> = emptyList())
data class EmailSequence(val title: String,
val emails: List<Email>,
val tags: List<String> = emptyList()) {
val first = emails.first()
val last = emails.last()
init {
require(emails.isNotEmpty())
}
fun next(emailsReceived: List<Email>) =
emails.first { it !in emailsReceived }
}
data class EmailSending(val email: Email,
val subscriber: Subscriber,
val date: LocalDate) {
override fun equals(other: Any?): Boolean {
return if (other is EmailSending) {
this.email === other.email && this.subscriber === other.subscriber && this.date == other.date
} else {
false
}
}
override fun hashCode(): Int {
return this.email.title.hashCode() * 7 + this.subscriber.name.hashCode() * 3 + this.date.hashCode()
}
}
data class EmailScheduling @JvmOverloads constructor(val sending: EmailSending,
val priority: Priority,
val timeSensitive: Boolean = false,
var blocked: Boolean = false) {
val id = ++nextId
companion object {
private var nextId = 0
}
}
Nothing surprising here: we have the seven classes we were expecting. We have a few utility methods here and there, but nothing that you cannot figure out by yourself.
Writing a Rule to Schedule an Email
It is now time to write our first business rule. This rule will state that, given a sequence and given a person, we will schedule the first email of the sequence to be sent to a person if that person is not already receiving an email from that sequence.
dialect "java"
rule "Start sequence"
when
sequence : EmailSequence ()
subscriber : Subscriber ( !isInSequence(sequence) )
then
EmailSending $sending = new EmailSending(sequence.getFirst(), subscriber, day);
EmailScheduling $scheduling = new EmailScheduling($sending, Priority.NORMAL);
insert($scheduling);
end
In the header of the rule, we specify the language we are using for writing the clauses. In this tutorial, we will consider only Java. There is another possible value: mvel. We will not look into that. Also, while, in this example, we specify the dialect on the rule, it can be, instead, specified once for the whole file. There is even a better option: not specifying the dialect at all, as Java is the default anyway and the usage of mvel is discouraged.
The when
section determines on which elements our rule will operate. In this case, we state that it will operate on an EmailSequence
and a Subscriber
. It will not work just on any person but only on a person for which the condition !isInSequence(sequence)
is satisfied. This condition is based on a call to the method isInsequence
that we will show below:
data class Subscriber(...) {
fun isInSequence(emailSequence: EmailSequence) =
hasReceived(emailSequence.first) &&
!hasReceived(emailSequence.last)
fun hasReceived(email: Email) =
emailReceived.any { it.email == email }
}
Let’s now look at the then
section of our rule. In such a section, we specify what happens when the rule is fired. The rule will be fired when elements satisfying the when
section can be found.
In this case, we will create an EmailScheduling
and add it to the session. In particular, we want to send to the considered person the first email of the sequence, on the day considered. We also specify the priority of this email (NORMAL
in this case). This is necessary to decide which email effectively to send when we have more than one. Indeed, we will have another rule looking at these values to decide which emails to prioritize (hint: it will be the email with the highest priority).
In general, you may want to typically add things into the session in the then
clause. Alternatively, you may want to modify objects that are part of the session. You could also call methods on objects which have side-effects. While the recommended approach is to limit yourself to manipulate the session, you may want to add side effects for logging, for example. This is especially useful when learning Drools and trying to wrap your head around your first rules.
Writing a Rule to Block an Email From Being Sent
We will see that we have two possible types of rules: rules to schedule new emails and rules to prevent scheduled emails to be sent. We have seen before how to write a rule to send an email and we will now see how to write an email to prevent an email from being sent.
In this rule, we want to check if an email is scheduled to be sent to a person who has received already emails in the last three days. If this is the case, we want to block that email from being sent.
rule "Prevent overloading"
when
scheduling : EmailScheduling(
sending.subscriber.hasReceivedEmailsInLastDays(3, day),
!blocked )
then
scheduling.setBlocked(true);
end
In the when
section, we specify that this rule will operate on an EmailScheduling
. So, every time another rule will add an EmailScheduling
; this rule could be triggered to decide if we have to block it from being sent.
This rule will apply to all scheduling, which is directed to subscribers who have received emails in the last 3 days. In addition to that, we will check if the EmailScheduling
was not already blocked. If that is the case, we will not need to apply this rule.
We use the setBlocked
method of the scheduling object to modify an element, which is part of the session.
At this point, we have seen the pattern we will use:
- We will create
EmailScheduling
when we think it makes sense to send an email to the user - We will check if we have reasons to block those emails. If that is the case, we will set the
blocked
flag to true, effectively removing theEmailScheduling
Using a flag to mark elements to remove/invalidate/block is a common pattern used in business rules. It can sound a bit unfamiliar at the beginning, but it is actually quite useful. You may think that you could just delete
elements from the session. However, doing so, it becomes easy to create infinite loops in which you create new elements with some rules, remove them with others, and keep recreating them again. The block-flag pattern avoids all of that.
The Session
Rules operate on data which is part of the session. Data is typically inserted into the session during the initialization phase. Later, we could have rules inserting more data into the session, potentially triggering other rules.
This is how we could populate the session with some example data:
fun loadDataIntoSession(ksession: KieSession,
dayToConsider: LocalDate) {
val products = listOf(
Product("My book", 20.0f),
Product("Video course", 100.0f),
Product("Consulting package", 500.0f)
)
val persons = listOf(
Subscriber("Mario",
LocalDate.of(2019, Month.JANUARY, 1),
"Italy"),
Subscriber("Amelie",
LocalDate.of(2019, Month.FEBRUARY, 1),
"France"),
Subscriber("Bernd",
LocalDate.of(2019, Month.APRIL, 18),
"Germany"),
Subscriber("Eric",
LocalDate.of(2018, Month.OCTOBER, 1),
"USA"),
Subscriber("Albert",
LocalDate.of(2016, Month.OCTOBER, 12),
"USA")
)
val sequences = listOf(
EmailSequence("Present book", listOf(
Email("Present book 1", "Here is the book...",
tags= listOf("book_explanation")),
Email("Present book 2", "Here is the book...",
tags= listOf("book_explanation")),
Email("Present book 3", "Here is the book...",
tags= listOf("book_explanation"))
)),
EmailSequence("Present course", listOf(
Email("Present course 1", "Here is the course...",
tags= listOf("course_explanation")),
Email("Present course 2", "Here is the course...",
tags= listOf("course_explanation")),
Email("Present course 3", "Here is the course...",
tags= listOf("course_explanation"))
))
)
ksession.insert(Email("Question to user",
"Do you..."))
ksession.insert(Email("Interesting topic A",
"Do you..."))
ksession.insert(Email("Interesting topic B",
"Do you..."))
ksession.insert(Email("Suggest book",
"I wrote a book...",
tags= listOf("book_offer")))
ksession.insert(Email("Suggest course",
"I wrote a course...",
tags= listOf("course_offer")))
ksession.insert(Email("Suggest consulting",
"I offer consulting...",
tags= listOf("consulting_offer")))
ksession.setGlobal("day", dayToConsider)
ksession.insert(products)
persons.forEach {
ksession.insert(it)
}
sequences.forEach {
ksession.insert(it)
}
}
Of course, in a real application, we would access some database or some form of storage to retrieve the data to be used to populate the session.
Global Objects
In rules, we will not only access elements, which are part of the session, but also global objects.
Global objects are inserted in the session using setGlobal
. We have seen an example in loadDataIntoSession
:
fun loadDataIntoSession(ksession: StatefulKnowledgeSession, dayToConsider: LocalDate) : EmailScheduler {
...
ksession.setGlobal("day", dayToConsider)
...
}
In the rules, we declare the globals:
package com.strumenta.funnellang
import com.strumenta.funnel.Email;
import com.strumenta.funnel.EmailSequence;
import com.strumenta.funnel.EmailScheduling
import com.strumenta.funnel.EmailScheduler;
import com.strumenta.funnel.Person
import java.time.LocalDate;
global LocalDate day;
At this point, we can refer to these globals in all rules. In our example, we use day
value to know which day we are considering for the scheduling. Typically, it would be tomorrow, as we would like to do the scheduling one day in advance. However, for testing reasons, we could use any day we want. Or we may want to use days in the future for simulation purposes.
Global should not be abused. Personally, I like to use them to specify configuration parameters. Others prefer to insert this data into the session and this is the recommended approach. The reason why I use globals (carefully and rarely) is that I like to distinguish between the data I am working on (stored in the session) and the configuration (for that I use globals).
Writing the Generic Rules
Let’s now see the whole set of generic rules that we have written. By generic rules, we mean rules that could be applied to all email schedulings we want to do. To complement these rules, we may have others for specific products or topics we are promoting.
package com.strumenta.funnellang
import com.strumenta.funnel.Email;
import com.strumenta.funnel.EmailSequence;
import com.strumenta.funnel.EmailScheduling
import com.strumenta.funnel.EmailSending;
import com.strumenta.funnel.Subscriber
import java.time.LocalDate;
import com.strumenta.funnel.Priority
global LocalDate day;
rule "Continue sequence"
when
sequence : EmailSequence ()
subscriber : Subscriber ( isInSequence(sequence) )
then
EmailSending $sending = new EmailSending(sequence.next(subscriber.getActualEmailsReceived()), subscriber, day);
EmailScheduling $scheduling = new EmailScheduling($sending, Priority.IMPORTANT, true);
insert($scheduling);
end
rule "Start sequence"
when
sequence : EmailSequence ()
subscriber : Subscriber ( !isInSequence(sequence) )
then
EmailSending $sending = new EmailSending(sequence.getFirst(), subscriber, day);
EmailScheduling $scheduling = new EmailScheduling($sending, Priority.NORMAL);
insert($scheduling);
end
rule "Prevent overloading"
when
scheduling : EmailScheduling(
sending.subscriber.hasReceivedEmailsInLastDays(3, day),
!blocked )
then
scheduling.setBlocked(true);
end
rule "Block on holidays"
when
scheduling : EmailScheduling( sending.subscriber.isOnHolidays(scheduling.sending.date), !blocked )
then
scheduling.setBlocked(true);
end
rule "Precedence to time sensitive emails"
when
scheduling1 : EmailScheduling( timeSensitive == true, !blocked )
scheduling2 : EmailScheduling( this != scheduling1,
!blocked,
sending.subscriber == scheduling1.sending.subscriber,
sending.date == scheduling1.sending.date,
timeSensitive == false)
then
scheduling2.setBlocked(true);
end
rule "Precedence to higher priority emails"
when
scheduling1 : EmailScheduling( !blocked )
scheduling2 : EmailScheduling( this != scheduling1,
!blocked,
sending.subscriber == scheduling1.sending.subscriber,
sending.date == scheduling1.sending.date,
timeSensitive == scheduling1.timeSensitive,
priority < scheduling1.priority)
then
scheduling2.setBlocked(true);
end
rule "Limit to one email per day"
when
scheduling1 : EmailScheduling( blocked == false )
scheduling2 : EmailScheduling( this != scheduling1,
blocked == false,
sending.subscriber == scheduling1.sending.subscriber,
sending.date == scheduling1.sending.date,
timeSensitive == scheduling1.timeSensitive,
priority == scheduling1.priority,
id > scheduling1.id)
then
scheduling2.setBlocked(true);
end
rule "Never resend same email"
when
scheduling : EmailScheduling( !blocked )
subscriber : Subscriber( this == scheduling.sending.subscriber,
hasReceived(scheduling.sending.email) )
then
scheduling.setBlocked(true);
end
Let’s examine all these rules, one by one:
- Continue sequence: If someone started receiving an email sequence and he did not receive the last email yet, then he should get the next email in the sequence
- Start sequence: If someone did not yet receive the first email of a sequence he should. Note that, technically speaking, this rule alone would cause everyone who has finished a sequence to immediately restart it. This does not happen because of the Never resend same email rule. However, you could decide to rewrite this rule to explicitly forbidding someone who has already received a certain sequence to be re-inserted in it.
- Prevent overloading: If someone has received an email in the last three days, then we should block any email scheduling directed to that person
- Block on holidays: If someone is on holiday, we should not send emails to them
- Precedence to time-sensitive emails: Given a pair of email schedulings directed to the same person on the same date, if only one of the two is time sensitive, we should block the other
- Precedence to higher priority emails: Given a pair of email schedulings directed to the same person on the same date being both time sensitive or both not time sensitive, we should block the one with lower importance
- Limit to one email per day: We should not schedule to send more than one email per day to the same person. If this happens, we have to pick one somehow. We use the internal ID to discriminate between the two
- Never resend same email: If someone has already received a certain email, he should not receive it again in the future
Writing the Rules Specific to the Book Emails
Our marketing experts may want to write specific rules for specific products or topics. Let’s assume they want to create a set of emails to promote and sell a book. We could write these rules in a separate file, perhaps maintained by the marketing expert in charge of selling that book.
To write rules regarding a specific topic, we will take advantage of tags, a mechanism that will give us a certain amount of flexibility. Let’s see the rules we can write:
package com.strumenta.funnellang
import com.strumenta.funnel.Subscriber;
import com.strumenta.funnel.EmailScheduling;
import java.time.DayOfWeek;
rule "Send book offer only after at least 3 book presentation emails"
when
subscriber : Subscriber (
emailReceivedWithTag("book_explanation") < 3
)
scheduling : EmailScheduling(
!blocked,
sending.subscriber == subscriber,
sending.email.tags contains "book_offer"
)
then
scheduling.setBlocked(true);
end
rule "Block book offers on monday"
when
scheduling : EmailScheduling(
!blocked,
sending.date.dayOfWeek == DayOfWeek.MONDAY,
sending.email.tags contains "book_offer"
)
then
scheduling.setBlocked(true);
end
rule "Block book offers for people who bought"
when
subscriber : Subscriber (
tags contains "book_bought"
)
scheduling : EmailScheduling(
!blocked,
sending.subscriber == subscriber,
sending.email.tags contains "book_offer"
)
then
scheduling.setBlocked(true);
end
Let’s examine our rules:
- Send book offer only after at least 3 book presentation emails: We want to block any email selling the book if the subscriber did not receive at least three emails explaining the content of the book
- Block book offers on Monday: We want to block book offers to be sent on Monday, for example, because we have seen that subscribers are less inclined to buy on that day of the week
- Block book offers for people who bought: We do not want to propose a deal on the book to subscribers who already bought it
Testing the Business Rules
There are different types of tests we may want to write to verify that our rules behave as expected. On one side of the spectrum, we may want to have tests that verify complex scenarios and check for unexpected interactions between rules. These tests will run considering complex data sets and the whole set of business rules. On the other side of the spectrum, we may want to write simple unit tests to verify single rules. We will see an example of these unit tests, but most of what we will see could be adapted to test the whole set of rules instead of single rules.
What do we want to do in our unit tests?
- We set up the knowledge base
- We want to load some data into the session
- We want to run the rule business engine, enabling just the one business rule we want to test
- We want to verify that the resulting email schedulings are the one expected
To satisfy point 1, we load all the files containing our rules and we verify there are no issues.
private fun prepareKnowledgeBase(files: List<File>): InternalKnowledgeBase {
val kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder()
files.forEach { kbuilder.add(ResourceFactory.newFileResource(it), ResourceType.DRL) }
val errors = kbuilder.errors
if (errors.size > 0) {
for (error in errors) {
System.err.println(error)
}
throw IllegalArgumentException("Could not parse knowledge.")
}
val kbase = KnowledgeBaseFactory.newKnowledgeBase()
kbase.addPackages(kbuilder.knowledgePackages)
return kbase
}
How do we load data into the session? We do that by loading some default data and then giving the possibility to change this data a little bit in each test. In the following piece of code, you will see that we can pass a function as the dataTransformer
parameter. Such a function can operate on the data before we load them into the session. This is our hook to tweak the data in each test.
fun loadDataIntoSession(ksession: KieSession,
dayToConsider: LocalDate, dataTransformer: ((Subscriber, Email) -> Unit)? = null) {
val amelie = Subscriber("Amelie",
LocalDate.of(2019, Month.FEBRUARY, 1),
"France")
val bookSeqEmail1 = Email("Present book 1", "Here is the book...",
tags= listOf("book_explanation"))
val products = listOf(
Product("My book", 20.0f),
Product("Video course", 100.0f),
Product("Consulting package", 500.0f)
)
val persons = listOf(amelie)
val sequences = listOf(
EmailSequence("Present book", listOf(
bookSeqEmail1,
Email("Present book 2", "Here is the book...",
tags= listOf("book_explanation")),
Email("Present book 3", "Here is the book...",
tags= listOf("book_explanation"))
))
)
dataTransformer?.invoke(amelie, bookSeqEmail1)
ksession.insert(Email("Question to user",
"Do you..."))
ksession.insert(Email("Interesting topic A",
"Do you..."))
ksession.insert(Email("Interesting topic B",
"Do you..."))
ksession.insert(Email("Suggest book",
"I wrote a book...",
tags= listOf("book_offer")))
ksession.insert(Email("Suggest course",
"I wrote a course...",
tags= listOf("course_offer")))
ksession.insert(Email("Suggest consulting",
"I offer consulting...",
tags= listOf("consulting_offer")))
ksession.setGlobal("day", dayToConsider)
ksession.insert(products)
persons.forEach {
ksession.insert(it)
}
sequences.forEach {
ksession.insert(it)
}
}
We achieve point 3 by specifying a filter on the rules to be executed:
ksession.fireAllRules { match -> match.rule.name in rulesToKeep }
At this point, we can simply check the results.
Once this infrastructure has been put in place the tests, we will write will look like this:
@test fun startSequencePositiveCase() {
val schedulings = setupSessionAndFireRules(
LocalDate.of(2019, Month.MARCH, 17), listOf("Start sequence"))
assertEquals(1, schedulings.size)
assertNotNull(schedulings.find {
it.sending.email.title == "Present book 1"
&& it.sending.subscriber.name == "Amelie" })
}
@test fun startSequenceWhenFirstEmailReceived() {
val schedulings = setupSessionAndFireRules(
LocalDate.of(2019, Month.MARCH, 17),
listOf("Start sequence")) { amelie, bookSeqEmail1 ->
amelie.emailsReceived.add(
EmailSending(bookSeqEmail1, amelie,
LocalDate.of(2018, Month.NOVEMBER, 12)))
}
assertEquals(0, schedulings.size)
}
In the first test, we expect Amelie to receive the first email of a sequence, given she did not receive yet. In the second test, instead, we set in the session that Amelie already received the first email of the sequence, so we expect it to not receive it again (no email schedulings expected at all).
This is the whole code of the test class:
package com.strumenta.funnel
import org.drools.core.impl.InternalKnowledgeBase
import org.drools.core.impl.KnowledgeBaseFactory
import org.kie.api.io.ResourceType
import org.kie.api.runtime.KieSession
import org.kie.internal.builder.KnowledgeBuilderFactory
import org.kie.internal.io.ResourceFactory
import java.io.File
import java.time.LocalDate
import java.time.Month
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import org.junit.Test as test
class GenericRulesTest {
private fun prepareKnowledgeBase(files: List<File>): InternalKnowledgeBase {
val kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder()
files.forEach { kbuilder.add(ResourceFactory.newFileResource(it), ResourceType.DRL) }
val errors = kbuilder.errors
if (errors.size > 0) {
for (error in errors) {
System.err.println(error)
}
throw IllegalArgumentException("Could not parse knowledge.")
}
val kbase = KnowledgeBaseFactory.newKnowledgeBase()
kbase.addPackages(kbuilder.knowledgePackages)
return kbase
}
fun loadDataIntoSession(ksession: KieSession,
dayToConsider: LocalDate, dataTransformer: ((Subscriber, Email) -> Unit)? = null) {
val amelie = Subscriber("Amelie",
LocalDate.of(2019, Month.FEBRUARY, 1),
"France")
val bookSeqEmail1 = Email("Present book 1", "Here is the book...",
tags= listOf("book_explanation"))
val products = listOf(
Product("My book", 20.0f),
Product("Video course", 100.0f),
Product("Consulting package", 500.0f)
)
val persons = listOf(amelie)
val sequences = listOf(
EmailSequence("Present book", listOf(
bookSeqEmail1,
Email("Present book 2", "Here is the book...",
tags= listOf("book_explanation")),
Email("Present book 3", "Here is the book...",
tags= listOf("book_explanation"))
))
)
dataTransformer?.invoke(amelie, bookSeqEmail1)
ksession.insert(Email("Question to user",
"Do you..."))
ksession.insert(Email("Interesting topic A",
"Do you..."))
ksession.insert(Email("Interesting topic B",
"Do you..."))
ksession.insert(Email("Suggest book",
"I wrote a book...",
tags= listOf("book_offer")))
ksession.insert(Email("Suggest course",
"I wrote a course...",
tags= listOf("course_offer")))
ksession.insert(Email("Suggest consulting",
"I offer consulting...",
tags= listOf("consulting_offer")))
ksession.setGlobal("day", dayToConsider)
ksession.insert(products)
persons.forEach {
ksession.insert(it)
}
sequences.forEach {
ksession.insert(it)
}
}
private fun setupSessionAndFireRules(dayToConsider: LocalDate, rulesToKeep: List<String>,
dataTransformer: ((Subscriber, Email) -> Unit)? = null) : List<EmailScheduling> {
val kbase = prepareKnowledgeBase(listOf(File("rules/generic.drl")))
val ksession = kbase.newKieSession()
loadDataIntoSession(ksession, dayToConsider, dataTransformer)
ksession.fireAllRules { match -> match.rule.name in rulesToKeep }
return ksession.selectScheduling(dayToConsider)
}
@test fun startSequencePositiveCase() {
val schedulings = setupSessionAndFireRules(
LocalDate.of(2019, Month.MARCH, 17), listOf("Start sequence"))
assertEquals(1, schedulings.size)
assertNotNull(schedulings.find {
it.sending.email.title == "Present book 1"
&& it.sending.subscriber.name == "Amelie" })
}
@test fun startSequenceWhenFirstEmailReceived() {
val schedulings = setupSessionAndFireRules(
LocalDate.of(2019, Month.MARCH, 17),
listOf("Start sequence")) { amelie, bookSeqEmail1 ->
amelie.emailsReceived.add(
EmailSending(bookSeqEmail1, amelie,
LocalDate.of(2018, Month.NOVEMBER, 12)))
}
assertEquals(0, schedulings.size)
}
}
Conclusions
Marketers should be able to experiment and try out their strategies and ideas easily: for example, do they want to create a special offer just to be sent at 20 subscribers per day? Do they want to send special offers to subscribers in a certain country? Do they want to consider the birthday or the national holiday of a subscriber to send him a special message? Our domain experts, marketers, in this case, should have a tool to pour these ideas into the system and see them applied. Thanks to business rules they could be able to implement most of them by themselves. Not having to go through developers or other “gate keepers” could mean having the freedom to experiment, to try things and in the end to make the business profit.
There are things to consider: giving the possibility to write business rules could not be enough. To make our domain experts confident in the rules they write we should give them the possibility to play with them and try them out in a safe environment: a testing or simulation mechanism should be put in place. In this way they could try things and see if they translated correctly into code the idea that they had in mind.
Of course, business rules are much easier to write compared to typical code. This is the case because they have a predefined format. In this way, we can pick an existing rule and tune a little bit. Still, it requires some training for the domain experts to get used to them. They need to develop the ability to formalize their thoughts and this could be easy or hard depending on their background. For example, for marketers, it could be doable while for other professionals it could require more exercise. What we could do to simplify their life and make domain experts more productive is to put a Domain Specific Language in front of our business rules.
By creating a simple DSL we could make things easier for our marketers. This DSL would permit to manipulate the domain model we have seen (subscribers, emails, etc) and perform the two actions marketers are interested into: scheduling and blocking emails. We could provide a simple editor, with auto-completion and error checking, and integrate a testing and simulation environment in it. In this scenario, marketers would be fully independent and able to design and verify their rules quickly and with very limited supported needed.
Acknowledgments
Mario Fusco (a Java champion) and Luca Molteni, both working on Drools at RedHat, were so very kind to review the article and suggest significant improvements. I am extremely thankful to them.
Thank you!
Published at DZone with permission of Federico Tomassetti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments