有关Drools业务规则引擎的完整教程

与往常一样,我们在配套存储库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部分确定我们的规则将对哪些元素进行操作。 在这种情况下,我们声明它将在EmailSequenceSubscriber上运行 。 它不仅对任何人都有效,而仅对满足!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部分,我们指定此规则将在EmailSchedulingEmailScheduling 。 因此,每当另一个规则将添加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. 我们建立知识库
  2. 我们想将一些数据加载到会话中
  3. 我们要运行规则业务引擎,仅启用我们要测试的一条业务规则
  4. 我们要验证产生的电子邮件调度是否是预期的

为了满足第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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/342445.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[渝粤教育] 中国人民解放军陆军工程大学 机械基础 参考 资料

教育 -机械基础-章节资料考试资料-中国人民解放军陆军工程大学【】 第1章测试 1、【单选题】机器中&#xff0c;将其他形式的能转化为机械能的部分称为&#xff08; &#xff09;。 A、传动部分 B、动力部分 C、工作部分 D、控制部分 参考资料【 】 2、【单选题】具有确定的相对…

物联网无线数传通信模块:工业级高精度电源模块

电源模块是可以直接贴装在印刷电路板上的电源供应器&#xff0c;其特点是可为专用集成电路&#xff08;ASIC&#xff09;、数字信号处理器 (DSP)、微处理器、存储器、现场可编程门阵列 (FPGA) 及其他数字或模拟负载提供供电。一般来说&#xff0c;这类模块称为负载 (POL) 电源供…

[渝粤教育] 中国农业大学 大学计算机基础 参考 资料

教育 -大学计算机基础-章节资料考试资料-中国农业大学【】 第1单元测验 - 信息技术与计算机 1、【单选题】计算思维是运用计算机科学的( )进行问题求解、系统设计、以及人类行为理解等涵盖计算机科学之广度的一系列思维活动。 A、基础概念 B、思维方式 C、程序设计原理 D、操作…

飞畅科技-专业交换机厂家解读市场对工业交换机产品的要求有哪些?

近些年来&#xff0c;随着网络通信技术的发展和工业控制领域对网络性能的要求越来越高&#xff0c;工业交换机通信要求要有足够的高实时性、高可靠性、抗干扰、抗网络故障、抗截取、抗伪造性能&#xff0c;保证高质量的控制数据通信。那么&#xff0c;市场对工业交换机产品的要…

网管交换机和非网管交换机有什么区别?

对局域网了解的人应该都知道交换机在网络中肩负着数据传输的重担&#xff0c;若是没有交换机&#xff0c;网站就无法登陆&#xff0c;人们将无法上网。目前市场上交换机可分为网管交换机和非网管交换机&#xff0c;对于这两种类型的交换机您了解多少&#xff1f;两者之间存在着…

[渝粤教育] 中国民用航空飞行学院 概率论与数理统计B 参考 资料

教育 -概率论与数理统计B-章节资料考试资料-中国民用航空飞行学院【】 作业&#xff1a;1.3频率与概率 练习&#xff1a;1.2样本空间、随机事件 1、【单选题】 A、相等 B、 C、互斥 D、对立 参考资料【 】 2、【单选题】 A、三个射手同时击中目标 B、三个射手至少一次没有击中目…

activemq主从配置_使用ActiveMQ –具有故障转移协议的“主/从”配置

activemq主从配置介绍 ActiveMQ代理往往是企业中消息传递基础结构的核心部分。 此消息传递基础结构的高度可用性和可伸缩性至关重要。 请阅读此链接 &#xff0c;以了解有关创建经纪人网络以支持各种用例的更多信息。 ActiveMQ的流行用例之一是带有共享数据库的主/从配置。 使用…

物联网lora无线数传模块应用案例:LoRawan网关通信技术

什么是LoRa LoRa(Long Range) 无线通信技术是 Semtech 在2012年开发出来的一款适合物联网使用的射频IC、其设计理念为低功耗、长距离、低成本、网路简单、易于扩展的无线数传技术。 在一般的通信中、通信的距离与功耗成正比、传输距离越远、功耗越高&#xff0c;而LoRa无线通…

网管交换机怎么设置?网管交换机设置方法

交换机可以说是局域网里面相对较重要的网络连通设备&#xff0c;在一些公共场所局域网的管理还是会依靠交换机来进行。但是就目前来说还有很多朋友对于网管交换机相对比较陌生。那么&#xff0c;接下来飞畅科技的小编就结合实际的情况来给大家介绍一下网管交换机怎么设置&#…

每个人都在谈论硒替代品-明智地选择!

什么是硒&#xff1f; Selenium是一套Web浏览器自动化工具&#xff0c;用于跨多种平台实现浏览器自动化。 尽管Selenium工具具有更多功能&#xff0c;但出于测试原因&#xff0c;它还是用于自动执行Web应用程序。 Jason Huggins于2004年创建了Selenium&#xff0c;作为ThoughtW…

网管型工业交换机的三大指标介绍

网管型交换机产品提供了基于终端控制口&#xff08;Console&#xff09;、基于Web页面以及支持Telnet远程登录网络等多种网络管理方式。因此网络管理人员可以对该交换机的工作状态、网络运行状况进行本地或远程的实时监控&#xff0c;纵观全局地管理所有交换端口的工作状态和工…

飞行安全背后,不可或缺的物联网无线通信传感器设备

飞行安全背后&#xff0c;不可或缺的传感器 近期&#xff0c;东方航空MU5735航班事件可谓是牵动全中国人民的心&#xff0c;网友们时时刻刻关心着救援工作的进展。目前&#xff0c;飞机的两部黑匣子正在加紧解码中&#xff0c;事故调查组同时也在进行调查认定&#xff0c;我们…

【zigbee无线通信模块步步详解】ZigBee3.0模块建立远程网络控制方法

本文以路灯控制应用为例&#xff0c;简述ZigBee3.0模块使用流程。 一、建立网络 1.通过USB转串口模块将出厂的ZigBee自组网模块连接&#xff0c;打开上位机软件“E180-ZG120A-Setting”&#xff0c;如下截图所示&#xff0c;选择端口号&#xff0c;并设置串口波特率&#xff…

网络交换机功能和原理详解

网络交换机&#xff0c;是一个扩大网络的器材&#xff0c;能为子网络中提供更多的连接端口&#xff0c;以便连接更多的计算机。它具有性能价格比高、高度灵活、相对简单、易于实现等特点。那么&#xff0c;网络交换机具体有哪些功能呢&#xff1f;网络交换机的原理是什么呢&…

浙江交换机厂家带你全面了解什么是工业交换机?

我们在工作中会经常接触到交换机&#xff0c;那么&#xff0c;什么是交换机呢&#xff1f;交换机又分为商业级交换机、工业级交换机、非网管型POE型交换机和网管型交换机。今天&#xff0c;在这里我们主要介绍下工业级交换机这块&#xff0c;接下来我们就跟随飞畅科技的小编一起…

室内无线通信定位技术AOD/AOA,都有哪些无线传输优势?

物联网无线数传室内定位技术&#xff1a;AOA与AOD 针对室内环境无法使用卫星定位的问题&#xff0c;目前市场上已经有了很多不同的室内定位技术。 常见的室内无线定位技术有&#xff1a;WiFi通信模块、蓝牙模块、红外线遥控开关、超宽带无线通信技术、RFID射频技术、ZigBee模…

[渝粤教育] 南京信息职业技术学院 电工电子技术基础 参考 资料

教育 -电工电子技术基础-章节资料考试资料-南京信息职业技术学院【】 【谈一谈】我希望从这门课中学到什么&#xff1f; 【猜猜看】谁会和我一起学习&#xff1f; 1、【多选题】你认为&#xff0c;在该课程中&#xff0c;大家主要基于什么原因前来选修学习&#xff1f;&#xf…

DLT645和modbus rtu无线通信协议介绍对比

超详细&#xff01;DLT645通信协议介绍 无线通信协议传输简介 目前主要使用的有两个版本DLT645-97和DLT645-07&#xff0c;该协议主要用于电表抄表&#xff0c;采用为主-从结构的半双工通讯模式&#xff0c;硬件接口使用RS-485&#xff0c;协议帧报文和使用方法与modbus rtu类…

【串口服务器rs485通信教程】存储型网关工作模式

首先需要明白的是串口的通讯速率是远低于网口&#xff0c;主机在请求RTU设备通常只处理几个寄存器&#xff0c;“存储型网关”就是应用于这种工作环境。 “存储型网关”对主机请求的读取指令进行存储&#xff0c;当主机再次请求或者其他主机请求相同设备&#xff08;地址码相同…

[渝粤教育] 南京工业职业技术大学 高职英语 参考 资料

教育 -高职英语-章节资料考试资料-南京工业职业技术大学【】 Exercises (Test) 1、【单选题】What is the headquarter of FORD? A、USA B、Germany C、England D、France 参考资料【 】 2、【单选题】What is the headquarter of WALMART? A、USA B、France C、England D、J…