有关Drools业务规则引擎的完整教程
與往常一樣,我們?cè)谂涮状鎯?chǔ)庫(kù)EmailSchedulingRules中共享本教程中提供的代碼。
業(yè)務(wù)規(guī)則很好地表現(xiàn)了某些領(lǐng)域的邏輯。 它們之所以有效,是因?yàn)?strong>它們可以直觀直觀地接近許多類型的領(lǐng)域?qū)<业乃季S方式 。 其原因在于它們?cè)试S分解單個(gè)組件中的大問(wèn)題。 這樣,用戶不必處理所有單個(gè)規(guī)則的編排:這是業(yè)務(wù)規(guī)則引擎提供的附加值。
在本文中,我們將討論一個(gè)使用業(yè)務(wù)規(guī)則編寫的應(yīng)用程序的特定示例。 我們將編寫規(guī)則,以確定將哪些電子郵件發(fā)送給新聞?dòng)嗛喺摺?我們將看到不同類型的規(guī)則,以及如何使用Drools規(guī)則語(yǔ)言表達(dá)它們。 我們還將看到如何配置Drools (擾流器:這很容易),并使系統(tǒng)詳細(xì)說(shuō)明規(guī)則以產(chǎn)生可使用的結(jié)果。
我認(rèn)為業(yè)務(wù)規(guī)則非常有趣,因?yàn)樗鼈冊(cè)试S以不同的方式看待問(wèn)題。 作為開(kāi)發(fā)人員,我們非常習(xí)慣命令式范式或功能式范式。 但是,還有其他范式,例如狀態(tài)機(jī)和業(yè)務(wù)規(guī)則,它們并不是很常用,在某些情況下可能更適合。
與往常一樣,我們?cè)谂涮状鎯?chǔ)庫(kù)EmailSchedulingRules中共享本教程中提供的代碼。
我們正在嘗試解決什么問(wèn)題
讓我們考慮一下電子郵件營(yíng)銷領(lǐng)域。 作為營(yíng)銷人員,我們有對(duì)我們的內(nèi)容感興趣的人員的電子郵件列表。 他們每個(gè)人都可能對(duì)某個(gè)特定主題表現(xiàn)出興趣,閱讀了一些文章并購(gòu)買了某些產(chǎn)品。 考慮到他們的所有歷史記錄和偏好,我們希望每次都向他們發(fā)送最合適的內(nèi)容。 此內(nèi)容可能具有教育意義或提出了一些建議。 問(wèn)題在于,我們要考慮一些限制因素(即,不于星期日發(fā)送電子郵件或不向已購(gòu)買產(chǎn)品的人發(fā)送促銷產(chǎn)品的電子郵件)。
所有這些規(guī)則本身都是簡(jiǎn)單的,但是復(fù)雜性是由它們?nèi)绾谓M合以及如何相互作用得出的。 業(yè)務(wù)規(guī)則引擎將為我們處理這種復(fù)雜性,我們要做的就是清楚地表達(dá)單個(gè)規(guī)則。 規(guī)則將以域數(shù)據(jù)的形式表達(dá),因此讓我們首先關(guān)注域模型。
我們領(lǐng)域的模型
在我們的域模型中,我們有:
- 電子郵件 :我們要發(fā)送的單個(gè)電子郵件,按其標(biāo)題和內(nèi)容進(jìn)行描述
- 電子郵件序列 :必須按特定順序發(fā)送的電子郵件組,例如代表教程或描述產(chǎn)品不同功能的一組電子郵件
- 訂戶 :郵件列表的單個(gè)訂戶。 我們將需要知道我們發(fā)送給他的電子郵件,他對(duì)哪些東西感興趣以及他購(gòu)買了哪些產(chǎn)品
- 產(chǎn)品 :我們出售的產(chǎn)品
- 購(gòu)買 :訂戶已進(jìn)行的購(gòu)買
- 電子郵件發(fā)送:我們?cè)谀硞€(gè)日期或?qū)⒁l(fā)送特定電子郵件給特定訂戶的事實(shí)
- 電子郵件計(jì)劃 :發(fā)送電子郵件的計(jì)劃,以及一些其他信息
與其他域元素相比,我們域模型的后兩個(gè)元素似乎不太明顯,但是我們會(huì)在實(shí)現(xiàn)中看到我們需要它們的原因。
我們的系統(tǒng)應(yīng)該做什么
我們的系統(tǒng)應(yīng)使用Drools引擎執(zhí)行所有規(guī)則,并確定每個(gè)用戶在特定日期應(yīng)發(fā)送的電子郵件。 結(jié)果可能是決定不發(fā)送任何電子郵件,或者發(fā)送電子郵件,從許多可能的電子郵件中選擇一個(gè)。
要考慮的重要一點(diǎn)是,這些規(guī)則可能會(huì)隨著時(shí)間的推移而發(fā)展。 市場(chǎng)營(yíng)銷負(fù)責(zé)人可能想嘗試新規(guī)則,看看它們?nèi)绾斡绊懴到y(tǒng)。 使用Drools,他們應(yīng)該可以輕松添加或刪除規(guī)則或調(diào)整現(xiàn)有規(guī)則。
讓我們強(qiáng)調(diào)一下:
這些領(lǐng)域?qū)<覒?yīng)該能夠?qū)ο到y(tǒng)進(jìn)行試驗(yàn)并快速嘗試,而無(wú)需始終需要開(kāi)發(fā)人員的幫助 。
規(guī)則
好的,現(xiàn)在我們知道我們擁有哪些數(shù)據(jù),我們可以基于該模型表達(dá)規(guī)則。
讓我們看一些我們可能要編寫的規(guī)則示例:
- 我們可能會(huì)有一系列電子郵件,例如課程內(nèi)容。 必須按順序發(fā)送
- 我們可能有時(shí)間敏感的電子郵件,應(yīng)該在特定的時(shí)間范圍內(nèi)發(fā)送,或者根本不發(fā)送
- 我們可能希望避免在一周的特定日期發(fā)送電子郵件,例如在訂戶所在國(guó)家/地區(qū)的公共假日
- 我們可能只想發(fā)送某些類型的電子郵件(例如,提議交易)給收到某些其他電子郵件的人(例如,至少3則關(guān)于同一主題的信息性電子郵件)
- 我們不想向已經(jīng)購(gòu)買該產(chǎn)品的訂戶提議某種產(chǎn)品的交易
- 我們可能希望限制向用戶發(fā)送電子郵件的頻率。 例如,如果我們?cè)谶^(guò)去5天內(nèi)已經(jīng)發(fā)送過(guò)一封電子郵件,我們可能決定不向用戶發(fā)送電子郵件
設(shè)置流口水
設(shè)置流口水可能非常簡(jiǎn)單。 我們正在研究在獨(dú)立應(yīng)用程序中運(yùn)行流口水。 根據(jù)您的情況,這可能是也可能不是一個(gè)可接受的解決方案,在某些情況下,您將不得不研究支持Drools的應(yīng)用服務(wù)器JBoss。 但是,如果您想入門,則可以忘記所有這些,而只需使用Gradle(或Maven)配置依賴項(xiàng)即可。 如果確實(shí)需要,您可以稍后找出無(wú)聊的配置位。
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 ,因?yàn)镵otlin搖滾!
- IDEA,因?yàn)樗俏易钕矚g的IDE
- Kotlin StdLib,反映和測(cè)試
- 流口水
這就是我們程序的結(jié)構(gòu):
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() } }很簡(jiǎn)單,很整潔。
我們?cè)谧鍪裁?#xff0c;細(xì)節(jié)是:
- 我們從文件加載規(guī)則。 現(xiàn)在,我們只加載文件rules/generic.drl
- 我們?cè)O(shè)置了一個(gè)新的會(huì)話。 將會(huì)話視為規(guī)則所看到的宇宙:他們可以訪問(wèn)的所有數(shù)據(jù)都在那里
- 我們將數(shù)據(jù)模型加載到會(huì)話中
- 我們執(zhí)行所有規(guī)則。 他們可以在會(huì)議中更改內(nèi)容
- 我們閱讀了修改后的數(shù)據(jù)模型(又稱會(huì)話),以確定我們今天應(yīng)該發(fā)送哪些電子郵件
編寫數(shù)據(jù)模型的類
前面我們已經(jīng)看到了數(shù)據(jù)模型的外觀,現(xiàn)在讓我們看一下它的代碼。
鑒于我們正在使用Kotlin,它將非常簡(jiǎn)潔明了。
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 } }這里不足為奇:我們有七個(gè)班級(jí)。 我們到處都有一些實(shí)用程序方法,但您無(wú)法自己解決。
編寫規(guī)則以安排電子郵件
現(xiàn)在是時(shí)候編寫我們的第一個(gè)業(yè)務(wù)規(guī)則了。 該規(guī)則將說(shuō)明,在給定序列和給定人員的情況下,如果該人尚未從該序列接收電子郵件,我們將安排該序列的第一封電子郵件發(fā)送給該人。
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在規(guī)則的標(biāo)題中,我們指定用于編寫子句的語(yǔ)言。 在本教程中,我們將僅考慮Java。 還有另一個(gè)可能的值: mvel 。 我們不會(huì)對(duì)此進(jìn)行調(diào)查。 同樣,盡管在此示例中,我們?cè)谝?guī)則中指定了方言,但也可以為整個(gè)文件指定一次方言。 甚至還有一個(gè)更好的選擇:根本不指定方言,因?yàn)镴ava仍然是默認(rèn)語(yǔ)言,不鼓勵(lì)使用mvel。
when部分確定我們的規(guī)則將對(duì)哪些元素進(jìn)行操作。 在這種情況下,我們聲明它將在EmailSequence和Subscriber上運(yùn)行 。 它不僅對(duì)任何人都有效,而僅對(duì)滿足!isInSequence(sequence)條件的人有效。 此條件基于對(duì)方法isInsequence的調(diào)用,我們將在下面顯示:
data class Subscriber(...) { fun isInSequence(emailSequence: EmailSequence) = hasReceived(emailSequence.first) && !hasReceived(emailSequence.last) fun hasReceived(email: Email) = emailReceived.any { it.email == email } }現(xiàn)在讓我們看一下規(guī)則的then部分。 在此部分中,我們指定觸發(fā)規(guī)則時(shí)會(huì)發(fā)生什么。 when找到滿足when部分的元素when將觸發(fā)該規(guī)則。
在這種情況下,我們將創(chuàng)建一個(gè)EmailScheduling并將其添加到會(huì)話中。 特別是,我們希望在考慮的當(dāng)天將序列的第一封電子郵件發(fā)送給考慮的人。 我們還指定了此電子郵件的優(yōu)先級(jí)(在這種情況下為NORMAL )。 當(dāng)我們有多個(gè)電子郵件時(shí),有必要決定有效發(fā)送的電子郵件。 的確,我們還有另一條規(guī)則將根據(jù)這些值來(lái)確定要優(yōu)先處理的電子郵件(提示:這將是優(yōu)先級(jí)最高的電子郵件)。
通常,您可能通常希望在then子句中將內(nèi)容添加到會(huì)話中。 或者,您可能要修改屬于會(huì)話一部分的對(duì)象。 您也可以在有副作用的對(duì)象上調(diào)用方法。 雖然建議的方法是限制自己來(lái)操縱會(huì)話,但是例如,您可能希望添加副作用以進(jìn)行日志記錄。 這在學(xué)習(xí)Drools并嘗試圍繞您的第一條規(guī)則時(shí)特別有用。
編寫規(guī)則以阻止發(fā)送電子郵件
我們將看到我們有兩種可能的規(guī)則類型:用于調(diào)度新電子郵件的規(guī)則和用于阻止調(diào)度電子郵件發(fā)送的規(guī)則。 之前我們已經(jīng)了解了如何編寫規(guī)則以發(fā)送電子郵件,現(xiàn)在我們將看到如何編寫電子郵件以防止發(fā)送電子郵件。
在此規(guī)則中,我們要檢查是否計(jì)劃將電子郵件發(fā)送給最近三天內(nèi)已收到電子郵件的人。 如果是這種情況,我們希望阻止該電子郵件的發(fā)送。
rule "Prevent overloading" when scheduling : EmailScheduling( sending.subscriber.hasReceivedEmailsInLastDays(3, day), !blocked ) then scheduling.setBlocked( true ); end在when部分,我們指定此規(guī)則將在EmailScheduling上EmailScheduling 。 因此,每當(dāng)另一個(gè)規(guī)則將添加EmailScheduling時(shí),都會(huì)觸發(fā)此規(guī)則來(lái)決定是否必須阻止其發(fā)送。
該規(guī)則將適用于所有計(jì)劃,這些計(jì)劃針對(duì)的是最近3天內(nèi)收到電子郵件的訂戶。 除此之外,我們還將檢查EmailScheduling是否尚未被阻止。 在這種情況下,我們將不需要應(yīng)用此規(guī)則。
我們使用調(diào)度對(duì)象的setBlocked方法來(lái)修改作為會(huì)話一部分的元素。
至此,我們已經(jīng)看到了將使用的模式:
- 當(dāng)我們認(rèn)為向用戶發(fā)送電子郵件有意義時(shí),我們將創(chuàng)建EmailScheduling
- 我們將檢查是否有理由阻止這些電子郵件。 如果是這種情況,我們將blocked標(biāo)志設(shè)置為true,從而有效地刪除EmailScheduling
使用標(biāo)記標(biāo)記元素以刪除/無(wú)效/阻止是業(yè)務(wù)規(guī)則中常用的模式。 剛開(kāi)始時(shí)聽(tīng)起來(lái)可能有點(diǎn)陌生,但實(shí)際上非常有用。 您可能認(rèn)為您可以只刪除會(huì)話中的元素,但是這樣做很容易創(chuàng)建無(wú)限循環(huán),在無(wú)限循環(huán)中您可以使用一些規(guī)則創(chuàng)建新元素,然后將其與其他規(guī)則一起刪除,然后繼續(xù)重新創(chuàng)建它們。 阻止標(biāo)志模式避免了所有這些情況。
會(huì)議
規(guī)則對(duì)作為會(huì)話一部分的數(shù)據(jù)進(jìn)行操作。 通常在初始化階段將數(shù)據(jù)插入會(huì)話中。 稍后,我們可以使用規(guī)則將更多數(shù)據(jù)插入到會(huì)話中,從而可能觸發(fā)其他規(guī)則。
這是我們可以用一些示例數(shù)據(jù)填充會(huì)話的方式:
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) } }當(dāng)然,在實(shí)際的應(yīng)用程序中,我們將訪問(wèn)某些數(shù)據(jù)庫(kù)或某種形式的存儲(chǔ),以檢索用于填充會(huì)話的數(shù)據(jù)。
全局對(duì)象
在規(guī)則中,我們不僅將訪問(wèn)作為會(huì)話一部分的元素,而且還將訪問(wèn)全局對(duì)象。
使用setGlobal將全局對(duì)象插入會(huì)話中。 我們?cè)趌oadDataIntoSession看到了一個(gè)示例:
在規(guī)則中,我們聲明全局變量:
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;在這一點(diǎn)上,我們可以在所有規(guī)則中引用這些全局變量。 在我們的示例中,我們使用day值來(lái)了解我們正在考慮進(jìn)行調(diào)度的日期。 通常是明天,因?yàn)槲覀兿胩崆耙惶彀才艜r(shí)間。 但是出于測(cè)試的原因,我們可以根據(jù)需要使用任何一天。 或者,我們可能希望將未來(lái)的日子用于模擬目的。
全球不應(yīng)濫用。 我個(gè)人喜歡使用它們來(lái)指定配置參數(shù)。 其他人則喜歡將此數(shù)據(jù)插入會(huì)話中,這是推薦的方法。 我使用全局變量的原因(謹(jǐn)慎而很少)是因?yàn)槲蚁矚g區(qū)分正在處理的數(shù)據(jù)(存儲(chǔ)在會(huì)話中)和配置(為此使用全局變量)。
編寫通用規(guī)則
現(xiàn)在讓我們看看我們編寫的整套通用規(guī)則。 通用規(guī)則是指可以應(yīng)用于我們要執(zhí)行的所有電子郵件計(jì)劃的規(guī)則。 為了補(bǔ)充這些規(guī)則,我們可能還會(huì)針對(duì)其他產(chǎn)品或主題進(jìn)行推廣。
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讓我們一一檢查所有這些規(guī)則:
- 繼續(xù)序列:如果某人開(kāi)始接收電子郵件序列,但他尚未收到最后一封電子郵件,則他應(yīng)該獲得序列中的下一封電子郵件
- 開(kāi)始序列:如果某人尚未收到序列的第一封電子郵件,則應(yīng)該發(fā)送。 請(qǐng)注意,從技術(shù)上來(lái)講,僅從規(guī)則上講,所有完成序列的人都會(huì)立即重新啟動(dòng)它。 由于從不重新發(fā)送同一電子郵件規(guī)則,因此不會(huì)發(fā)生這種情況。 但是,您可以決定重寫此規(guī)則,以明確禁止已收到特定序列的某人重新插入該規(guī)則。
- 防止超載:如果某人在過(guò)去三天內(nèi)收到了電子郵件,則我們應(yīng)阻止針對(duì)該人的任何電子郵件調(diào)度
- 禁止放假:如果某人放假,我們不應(yīng)該向他們發(fā)送電子郵件
- 對(duì)時(shí)間敏感的電子郵件的優(yōu)先順序:給定一對(duì)在同一日期定向到同一個(gè)人的電子郵件調(diào)度,如果兩者中只有一個(gè)對(duì)時(shí)間敏感,我們應(yīng)該阻止另一個(gè)
- 優(yōu)先級(jí)較高的電子郵件的優(yōu)先級(jí):給定一對(duì)在同一日期發(fā)給同一個(gè)人的電子郵件調(diào)度既對(duì)時(shí)間敏感又對(duì)時(shí)間不敏感,因此我們應(yīng)屏蔽重要性較低的電子郵件
- 每天最多只能發(fā)送一封電子郵件:我們不應(yīng)安排每天向同一個(gè)人發(fā)送多封電子郵件。 如果發(fā)生這種情況,我們必須以某種方式選擇一個(gè)。 我們使用內(nèi)部ID來(lái)區(qū)分兩者
- 永遠(yuǎn)不要重新發(fā)送同一封電子郵件:如果某人已經(jīng)收到了某封電子郵件,他以后就不會(huì)再收到該電子郵件
編寫特定于圖書電子郵件的規(guī)則
我們的營(yíng)銷專家可能希望針對(duì)特定產(chǎn)品或主題編寫特定規(guī)則。 假設(shè)他們想創(chuàng)建一組電子郵件來(lái)促銷和出售一本書。 我們可以將這些規(guī)則寫在一個(gè)單獨(dú)的文件中,該文件可能由負(fù)責(zé)銷售該書的營(yíng)銷專家維護(hù)。
為了編寫有關(guān)特定主題的規(guī)則,我們將利用標(biāo)簽,該標(biāo)簽將為我們提供一定程度的靈活性。 讓我們看看我們可以編寫的規(guī)則:
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讓我們檢查一下規(guī)則:
- 僅在至少3本書的介紹電子郵件之后發(fā)送圖書報(bào)價(jià):如果訂戶沒(méi)有收到至少3封解釋該書內(nèi)容的電子郵件,我們希望阻止銷售該書的任何電子郵件
- 禁止在星期一進(jìn)行預(yù)定:我們想禁止在星期一發(fā)送的預(yù)定,例如,因?yàn)槲覀円呀?jīng)看到訂戶在一周中的這一天不太愿意購(gòu)買
- 為購(gòu)買者提供大批量書籍報(bào)價(jià):我們不想向已購(gòu)買該書籍的訂戶提出交易建議
測(cè)試業(yè)務(wù)規(guī)則
我們可能需要編寫不同類型的測(cè)試來(lái)驗(yàn)證我們的規(guī)則是否符合預(yù)期。 一方面,我們可能需要進(jìn)行測(cè)試,以驗(yàn)證復(fù)雜的場(chǎng)景并檢查規(guī)則之間的意外交互。 這些測(cè)試將在考慮復(fù)雜數(shù)據(jù)集和整個(gè)業(yè)務(wù)規(guī)則的情況下運(yùn)行。 另一方面,我們可能想編寫簡(jiǎn)單的單元測(cè)試來(lái)驗(yàn)證單個(gè)規(guī)則。 我們將看到這些單元測(cè)試的示例,但是我們將看到的大多數(shù)內(nèi)容都可以用于測(cè)試整個(gè)規(guī)則集而不是單個(gè)規(guī)則。
我們要在單元測(cè)試中做什么?
為了滿足第1點(diǎn),我們加載了包含規(guī)則的所有文件,并確認(rèn)沒(méi)有問(wèn)題。
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 }我們?nèi)绾螌?shù)據(jù)加載到會(huì)話中? 我們通過(guò)加載一些默認(rèn)數(shù)據(jù),然后在每次測(cè)試中稍稍更改此數(shù)據(jù)來(lái)做到這一點(diǎn)。 在下面的代碼中,您將看到我們可以將一個(gè)函數(shù)作為dataTransformer參數(shù)傳遞。 在將數(shù)據(jù)加載到會(huì)話之前,此類功能可以對(duì)數(shù)據(jù)進(jìn)行操作。 這是我們調(diào)整每個(gè)測(cè)試中的數(shù)據(jù)的鉤子。
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) } }我們通過(guò)在要執(zhí)行的規(guī)則上指定一個(gè)過(guò)濾器來(lái)達(dá)到第3點(diǎn):
ksession.fireAllRules { match -> match.rule.name in rulesToKeep } ksession.fireAllRules { match -> match.rule.name rulesToKeep }此時(shí),我們可以簡(jiǎn)單地檢查結(jié)果。
將此基礎(chǔ)結(jié)構(gòu)放置到位后,我們將編寫的測(cè)試將如下所示:
@ 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) }在第一次測(cè)試中,我們希望Amelie能夠收到序列的第一封電子郵件,因?yàn)樗形词盏健?相反,在第二個(gè)測(cè)試中,我們?cè)跁?huì)話中將Amelie設(shè)置為已接收到該序列的第一封電子郵件,因此我們希望它不再再次接收到該電子郵件(根本不希望安排任何電子郵件)。
這是測(cè)試類的完整代碼:
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) } }結(jié)論
營(yíng)銷人員應(yīng)該能夠輕松地嘗試并嘗試他們的策略和想法:例如,他們是否想創(chuàng)建僅以每天20個(gè)訂閱者發(fā)送的特價(jià)商品? 他們是否要向特定國(guó)家/地區(qū)的訂戶發(fā)送特別優(yōu)惠? 他們是否想考慮訂戶的生日或國(guó)定假日向他發(fā)送特殊消息? 我們的領(lǐng)域?qū)<?#xff08;在這種情況下為營(yíng)銷人員)應(yīng)具有將這些想法倒入系統(tǒng)并加以應(yīng)用的工具。 由于有了業(yè)務(wù)規(guī)則,他們可以自行實(shí)現(xiàn)大多數(shù)規(guī)則。 不必經(jīng)歷開(kāi)發(fā)人員或其他“守門人”,就意味著可以自由地進(jìn)行試驗(yàn),嘗試并最終獲得業(yè)務(wù)利潤(rùn)。
需要考慮的事情:提供編寫業(yè)務(wù)規(guī)則的可能性還不夠。 為了使我們的領(lǐng)域?qū)<覍?duì)他們編寫的規(guī)則充滿信心,我們應(yīng)該給他們提供與他們一起玩耍并在安全的環(huán)境中進(jìn)行嘗試的可能性:應(yīng)該建立測(cè)試或模擬機(jī)制。 通過(guò)這種方式,他們可以嘗試嘗試并查看他們是否將想法正確地轉(zhuǎn)換為代碼。
當(dāng)然,與典型代碼相比,業(yè)務(wù)規(guī)則更容易編寫。 之所以如此,是因?yàn)樗鼈兙哂蓄A(yù)定義的格式。 這樣,我們可以選擇一個(gè)現(xiàn)有規(guī)則并進(jìn)行一些調(diào)整。 盡管如此,仍需要對(duì)領(lǐng)域?qū)<疫M(jìn)行一些培訓(xùn)以使其適應(yīng)他們。 他們需要發(fā)展將思想形式化的能力,根據(jù)他們的背景,這可能容易還是很難。 例如,對(duì)于營(yíng)銷人員而言,這是可行的,而對(duì)于其他專業(yè)人員而言,則可能需要更多的鍛煉。 為了簡(jiǎn)化他們的生活,使領(lǐng)域?qū)<腋痈咝?#xff0c;我們可以做的是在我們的業(yè)務(wù)規(guī)則之前放置特定于領(lǐng)域的語(yǔ)言 。
通過(guò)創(chuàng)建簡(jiǎn)單的DSL,我們可以簡(jiǎn)化營(yíng)銷人員的工作。 該DSL將允許操縱我們已經(jīng)看到的域模型(訂戶,電子郵件等),并執(zhí)行營(yíng)銷人員感興趣的兩個(gè)動(dòng)作:調(diào)度和阻止電子郵件。 我們可以提供具有自動(dòng)完成和錯(cuò)誤檢查功能的簡(jiǎn)單編輯器,并在其中集成測(cè)試和仿真環(huán)境。 在這種情況下,營(yíng)銷人員將完全獨(dú)立,并能夠在非常有限的支持需求下快速設(shè)計(jì)和驗(yàn)證其規(guī)則。
致謝
Mario Fusco(Java冠軍)和Luca Molteni都在RedHat從事Drools的工作,他們非常樂(lè)于評(píng)論本文并提出重大改進(jìn)建議。 我非常感謝他們。
謝謝!
翻譯自: https://www.javacodegeeks.com/2019/04/a-complete-tutorial-on-the-drools-business-rule-engine.html
總結(jié)
以上是生活随笔為你收集整理的有关Drools业务规则引擎的完整教程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 手机如何投屏到电脑上手机和和电脑如何投屏
- 下一篇: 电脑粘贴板在哪里找电脑粘贴板在哪里找?w