与往常一样,我们在配套存储库EmailSchedulingRules中共享本教程中提供的代码。
业务规则很好地表现了某些领域的逻辑。 它们之所以有效,是因为它们可以直观直观地接近许多类型的领域专家的思维方式 。 其原因在于它们允许分解单个组件中的大问题。 这样,用户不必处理所有单个规则的编排:这是业务规则引擎提供的附加值。
在本文中,我们将讨论一个使用业务规则编写的应用程序的特定示例。 我们将编写规则,以确定将哪些电子邮件发送给新闻订阅者。 我们将看到不同类型的规则,以及如何使用Drools规则语言表达它们。 我们还将看到如何配置Drools (扰流器:这很容易),并使系统详细说明规则以产生可使用的结果。
我认为业务规则非常有趣,因为它们允许以不同的方式看待问题。 作为开发人员,我们非常习惯命令式范式或功能式范式。 但是,还有其他范式,例如状态机和业务规则,它们并不是很常用,在某些情况下可能更适合。
与往常一样,我们在配套存储库EmailSchedulingRules中共享本教程中提供的代码。
我们正在尝试解决什么问题
让我们考虑一下电子邮件营销领域。 作为营销人员,我们有对我们的内容感兴趣的人员的电子邮件列表。 他们每个人都可能对某个特定主题表现出兴趣,阅读了一些文章并购买了某些产品。 考虑到他们的所有历史记录和偏好,我们希望每次都向他们发送最合适的内容。 此内容可能具有教育意义或提出了一些建议。 问题在于,我们要考虑一些限制因素(即,不于星期日发送电子邮件或不向已购买产品的人发送促销产品的电子邮件)。
所有这些规则本身都是简单的,但是复杂性是由它们如何组合以及如何相互作用得出的。 业务规则引擎将为我们处理这种复杂性,我们要做的就是清楚地表达单个规则。 规则将以域数据的形式表达,因此让我们首先关注域模型。
我们领域的模型
在我们的域模型中,我们有:
- 电子邮件 :我们要发送的单个电子邮件,按其标题和内容进行描述
- 电子邮件序列 :必须按特定顺序发送的电子邮件组,例如代表教程或描述产品不同功能的一组电子邮件
- 订户 :邮件列表的单个订户。 我们将需要知道我们发送给他的电子邮件,他对哪些东西感兴趣以及他购买了哪些产品
- 产品 :我们出售的产品
- 购买 :订户已进行的购买
- 电子邮件发送:我们在某个日期或将要发送特定电子邮件给特定订户的事实
- 电子邮件计划 :发送电子邮件的计划,以及一些其他信息
与其他域元素相比,我们域模型的后两个元素似乎不太明显,但是我们会在实现中看到我们需要它们的原因。
我们的系统应该做什么
我们的系统应使用Drools引擎执行所有规则,并确定每个用户在特定日期应发送的电子邮件。 结果可能是决定不发送任何电子邮件,或者发送电子邮件,从许多可能的电子邮件中选择一个。
要考虑的重要一点是,这些规则可能会随着时间的推移而发展。 市场营销负责人可能想尝试新规则,看看它们如何影响系统。 使用Drools,他们应该可以轻松添加或删除规则或调整现有规则。
让我们强调一下:
这些领域专家应该能够对系统进行试验并快速尝试,而无需始终需要开发人员的帮助 。
规则
好的,现在我们知道我们拥有哪些数据,我们可以基于该模型表达规则。
让我们看一些我们可能要编写的规则示例:
- 我们可能会有一系列电子邮件,例如课程内容。 必须按顺序发送
- 我们可能有时间敏感的电子邮件,应该在特定的时间范围内发送,或者根本不发送
- 我们可能希望避免在一周的特定日期发送电子邮件,例如在订户所在国家/地区的公共假日
- 我们可能只想发送某些类型的电子邮件(例如,提议交易)给收到某些其他电子邮件的人(例如,至少3则关于同一主题的信息性电子邮件)
- 我们不想向已经购买该产品的订户提议某种产品的交易
- 我们可能希望限制向用户发送电子邮件的频率。 例如,如果我们在过去5天内已经发送过一封电子邮件,我们可能决定不向用户发送电子邮件
设置流口水
设置流口水可能非常简单。 我们正在研究在独立应用程序中运行流口水。 根据您的情况,这可能是也可能不是一个可接受的解决方案,在某些情况下,您将不得不研究支持Drools的应用服务器JBoss。 但是,如果您想入门,则可以忘记所有这些,而只需使用Gradle(或Maven)配置依赖项即可。 如果确实需要,您可以稍后找出无聊的配置位。
buildscript { ext.droolsVersion = "7.20.0.Final" repositories { mavenCentral() } } plugins { id "org.jetbrains.kotlin.jvm" version "1.3.21" "org.jetbrains.kotlin.jvm" "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" }
在我们的Gradle脚本中,我们使用:
- Kotlin ,因为Kotlin摇滚!
- IDEA,因为它是我最喜欢的IDE
- Kotlin StdLib,反映和测试
- 流口水
这就是我们程序的结构:
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 the future or we may want to run tests using a different val dayToConsider = LocalDate.now() loadDataIntoSession(ksession, dayToConsider) ksession.fireAllRules() showSending(ksession) } catch (t: Throwable) { t.printStackTrace() } }
很简单,很整洁。
我们在做什么,细节是:
- 我们从文件加载规则。 现在,我们只加载文件
rules/generic.drl
- 我们设置了一个新的会话。 将会话视为规则所看到的宇宙:他们可以访问的所有数据都在那里
- 我们将数据模型加载到会话中
- 我们执行所有规则。 他们可以在会议中更改内容
- 我们阅读了修改后的数据模型(又称会话),以确定我们今天应该发送哪些电子邮件
编写数据模型的类
前面我们已经看到了数据模型的外观,现在让我们看一下它的代码。
鉴于我们正在使用Kotlin,它将非常简洁明了。
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 } 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 } }
这里不足为奇:我们有七个班级。 我们到处都有一些实用程序方法,但您无法自己解决。
编写规则以安排电子邮件
现在是时候编写我们的第一个业务规则了。 该规则将说明,在给定序列和给定人员的情况下,如果该人尚未从该序列接收电子邮件,我们将安排该序列的第一封电子邮件发送给该人。
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
在规则的标题中,我们指定用于编写子句的语言。 在本教程中,我们将仅考虑Java。 还有另一个可能的值: mvel 。 我们不会对此进行调查。 同样,尽管在此示例中,我们在规则中指定了方言,但也可以为整个文件指定一次方言。 甚至还有一个更好的选择:根本不指定方言,因为Java仍然是默认语言,不鼓励使用mvel。
when
部分确定我们的规则将对哪些元素进行操作。 在这种情况下,我们声明它将在EmailSequence和Subscriber上运行 。 它不仅对任何人都有效,而仅对满足!isInSequence(sequence)
条件的人有效。 此条件基于对方法isInsequence
的调用,我们将在下面显示:
data class Subscriber(...) { fun isInSequence(emailSequence: EmailSequence) = hasReceived(emailSequence.first) && !hasReceived(emailSequence.last) fun hasReceived(email: Email) = emailReceived.any { it.email == email } }
现在让我们看一下规则的then
部分。 在此部分中,我们指定触发规则时会发生什么。 when
找到满足when
部分的元素when
将触发该规则。
在这种情况下,我们将创建一个EmailScheduling
并将其添加到会话中。 特别是,我们希望在考虑的当天将序列的第一封电子邮件发送给考虑的人。 我们还指定了此电子邮件的优先级(在这种情况下为NORMAL
)。 当我们有多个电子邮件时,有必要决定有效发送的电子邮件。 的确,我们还有另一条规则将根据这些值来确定要优先处理的电子邮件(提示:这将是优先级最高的电子邮件)。
通常,您可能通常希望在then
子句中将内容添加到会话中。 或者,您可能要修改属于会话一部分的对象。 您也可以在有副作用的对象上调用方法。 虽然建议的方法是限制自己来操纵会话,但是例如,您可能希望添加副作用以进行日志记录。 这在学习Drools并尝试围绕您的第一条规则时特别有用。
编写规则以阻止发送电子邮件
我们将看到我们有两种可能的规则类型:用于调度新电子邮件的规则和用于阻止调度电子邮件发送的规则。 之前我们已经了解了如何编写规则以发送电子邮件,现在我们将看到如何编写电子邮件以防止发送电子邮件。
在此规则中,我们要检查是否计划将电子邮件发送给最近三天内已收到电子邮件的人。 如果是这种情况,我们希望阻止该电子邮件的发送。
rule "Prevent overloading" when scheduling : EmailScheduling( sending.subscriber.hasReceivedEmailsInLastDays(3, day), !blocked ) then scheduling.setBlocked( true ); end
在when
部分,我们指定此规则将在EmailScheduling
上EmailScheduling
。 因此,每当另一个规则将添加EmailScheduling
时,都会触发此规则来决定是否必须阻止其发送。
该规则将适用于所有计划,这些计划针对的是最近3天内收到电子邮件的订户。 除此之外,我们还将检查EmailScheduling
是否尚未被阻止。 在这种情况下,我们将不需要应用此规则。
我们使用调度对象的setBlocked
方法来修改作为会话一部分的元素。
至此,我们已经看到了将使用的模式:
- 当我们认为向用户发送电子邮件有意义时,我们将创建
EmailScheduling
- 我们将检查是否有理由阻止这些电子邮件。 如果是这种情况,我们将
blocked
标志设置为true,从而有效地删除EmailScheduling
使用标记标记元素以删除/无效/阻止是业务规则中常用的模式。 刚开始时听起来可能有点陌生,但实际上非常有用。 您可能认为您可以只删除会话中的元素,但是这样做很容易创建无限循环,在无限循环中您可以使用一些规则创建新元素,然后将其与其他规则一起删除,然后继续重新创建它们。 阻止标志模式避免了所有这些情况。
会议
规则对作为会话一部分的数据进行操作。 通常在初始化阶段将数据插入会话中。 稍后,我们可以使用规则将更多数据插入到会话中,从而可能触发其他规则。
这是我们可以用一些示例数据填充会话的方式:
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) } }
当然,在实际的应用程序中,我们将访问某些数据库或某种形式的存储,以检索用于填充会话的数据。
全局对象
在规则中,我们不仅将访问作为会话一部分的元素,而且还将访问全局对象。
使用setGlobal
将全局对象插入会话中。 我们在loadDataIntoSession
看到了一个示例:
fun loadDataIntoSession(ksession: StatefulKnowledgeSession, dayToConsider: LocalDate) : EmailScheduler { ... ksession.setGlobal( "day" , dayToConsider) ... }
在规则中,我们声明全局变量:
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;
在这一点上,我们可以在所有规则中引用这些全局变量。 在我们的示例中,我们使用day
值来了解我们正在考虑进行调度的日期。 通常是明天,因为我们想提前一天安排时间。 但是出于测试的原因,我们可以根据需要使用任何一天。 或者,我们可能希望将未来的日子用于模拟目的。
全球不应滥用。 我个人喜欢使用它们来指定配置参数。 其他人则喜欢将此数据插入会话中,这是推荐的方法。 我使用全局变量的原因(谨慎而很少)是因为我喜欢区分正在处理的数据(存储在会话中)和配置(为此使用全局变量)。
编写通用规则
现在让我们看看我们编写的整套通用规则。 通用规则是指可以应用于我们要执行的所有电子邮件计划的规则。 为了补充这些规则,我们可能还会针对其他产品或主题进行推广。
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
让我们一一检查所有这些规则:
- 继续序列:如果某人开始接收电子邮件序列,但他尚未收到最后一封电子邮件,则他应该获得序列中的下一封电子邮件
- 开始序列:如果某人尚未收到序列的第一封电子邮件,则应该发送。 请注意,从技术上来讲,仅从规则上讲,所有完成序列的人都会立即重新启动它。 由于从不重新发送同一电子邮件规则,因此不会发生这种情况。 但是,您可以决定重写此规则,以明确禁止已收到特定序列的某人重新插入该规则。
- 防止超载:如果某人在过去三天内收到了电子邮件,则我们应阻止针对该人的任何电子邮件调度
- 禁止放假:如果某人放假,我们不应该向他们发送电子邮件
- 对时间敏感的电子邮件的优先顺序:给定一对在同一日期定向到同一个人的电子邮件调度,如果两者中只有一个对时间敏感,我们应该阻止另一个
- 优先级较高的电子邮件的优先级:给定一对在同一日期发给同一个人的电子邮件调度既对时间敏感又对时间不敏感,因此我们应屏蔽重要性较低的电子邮件
- 每天最多只能发送一封电子邮件:我们不应安排每天向同一个人发送多封电子邮件。 如果发生这种情况,我们必须以某种方式选择一个。 我们使用内部ID来区分两者
- 永远不要重新发送同一封电子邮件:如果某人已经收到了某封电子邮件,他以后就不会再收到该电子邮件
编写特定于图书电子邮件的规则
我们的营销专家可能希望针对特定产品或主题编写特定规则。 假设他们想创建一组电子邮件来促销和出售一本书。 我们可以将这些规则写在一个单独的文件中,该文件可能由负责销售该书的营销专家维护。
为了编写有关特定主题的规则,我们将利用标签,该标签将为我们提供一定程度的灵活性。 让我们看看我们可以编写的规则:
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, "book_offer" 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, "book_offer" 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, "book_offer" sending.email.tags contains "book_offer" ) then scheduling.setBlocked( true ); end
让我们检查一下规则:
- 仅在至少3本书的介绍电子邮件之后发送图书报价:如果订户没有收到至少3封解释该书内容的电子邮件,我们希望阻止销售该书的任何电子邮件
- 禁止在星期一进行预定:我们想禁止在星期一发送的预定,例如,因为我们已经看到订户在一周中的这一天不太愿意购买
- 为购买者提供大批量书籍报价:我们不想向已购买该书籍的订户提出交易建议
测试业务规则
我们可能需要编写不同类型的测试来验证我们的规则是否符合预期。 一方面,我们可能需要进行测试,以验证复杂的场景并检查规则之间的意外交互。 这些测试将在考虑复杂数据集和整个业务规则的情况下运行。 另一方面,我们可能想编写简单的单元测试来验证单个规则。 我们将看到这些单元测试的示例,但是我们将看到的大多数内容都可以用于测试整个规则集而不是单个规则。
我们要在单元测试中做什么?
- 我们建立知识库
- 我们想将一些数据加载到会话中
- 我们要运行规则业务引擎,仅启用我们要测试的一条业务规则
- 我们要验证产生的电子邮件调度是否是预期的
为了满足第1点,我们加载了包含规则的所有文件,并确认没有问题。
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 }
我们如何将数据加载到会话中? 我们通过加载一些默认数据,然后在每次测试中稍稍更改此数据来做到这一点。 在下面的代码中,您将看到我们可以将一个函数作为dataTransformer参数传递。 在将数据加载到会话之前,此类功能可以对数据进行操作。 这是我们调整每个测试中的数据的钩子。
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) } }
我们通过在要执行的规则上指定一个过滤器来达到第3点:
ksession.fireAllRules { match -> match.rule.name in rulesToKeep } ksession.fireAllRules { match -> match.rule.name rulesToKeep }
此时,我们可以简单地检查结果。
将此基础结构放置到位后,我们将编写的测试将如下所示:
@ 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) }
在第一次测试中,我们希望Amelie能够收到序列的第一封电子邮件,因为她尚未收到。 相反,在第二个测试中,我们在会话中将Amelie设置为已接收到该序列的第一封电子邮件,因此我们希望它不再再次接收到该电子邮件(根本不希望安排任何电子邮件)。
这是测试类的完整代码:
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 } ksession.fireAllRules { match -> match.rule.name 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) } }
结论
营销人员应该能够轻松地尝试并尝试他们的策略和想法:例如,他们是否想创建仅以每天20个订阅者发送的特价商品? 他们是否要向特定国家/地区的订户发送特别优惠? 他们是否想考虑订户的生日或国定假日向他发送特殊消息? 我们的领域专家(在这种情况下为营销人员)应具有将这些想法倒入系统并加以应用的工具。 由于有了业务规则,他们可以自行实现大多数规则。 不必经历开发人员或其他“守门人”,就意味着可以自由地进行试验,尝试并最终获得业务利润。
需要考虑的事情:提供编写业务规则的可能性还不够。 为了使我们的领域专家对他们编写的规则充满信心,我们应该给他们提供与他们一起玩耍并在安全的环境中进行尝试的可能性:应该建立测试或模拟机制。 通过这种方式,他们可以尝试尝试并查看他们是否将想法正确地转换为代码。
当然,与典型代码相比,业务规则更容易编写。 之所以如此,是因为它们具有预定义的格式。 这样,我们可以选择一个现有规则并进行一些调整。 尽管如此,仍需要对领域专家进行一些培训以使其适应他们。 他们需要发展将思想形式化的能力,根据他们的背景,这可能容易还是很难。 例如,对于营销人员而言,这是可行的,而对于其他专业人员而言,则可能需要更多的锻炼。 为了简化他们的生活,使领域专家更加高效,我们可以做的是在我们的业务规则之前放置特定于领域的语言 。
通过创建简单的DSL,我们可以简化营销人员的工作。 该DSL将允许操纵我们已经看到的域模型(订户,电子邮件等),并执行营销人员感兴趣的两个动作:调度和阻止电子邮件。 我们可以提供具有自动完成和错误检查功能的简单编辑器,并在其中集成测试和仿真环境。 在这种情况下,营销人员将完全独立,并能够在非常有限的支持需求下快速设计和验证其规则。
致谢
Mario Fusco(Java冠军)和Luca Molteni都在RedHat从事Drools的工作,他们非常乐于评论本文并提出重大改进建议。 我非常感谢他们。
谢谢!
翻译自: https://www.javacodegeeks.com/2019/04/a-complete-tutorial-on-the-drools-business-rule-engine.html