Drools 業務規則引擎的完整教程

banq發表於2022-11-10

業務規則可以很好地代表某些領域的邏輯。它們工作得很好,因為它們的結果是直觀的,接近許多型別的領域專家的思維方式。其原因是它們允許將一個大問題分解成單個元件。透過這種方式,使用者不必處理所有單一規則的協調問題:這是業務規則引擎提供的附加價值。

在這篇文章中,我們將討論一個使用業務規則編寫應用程式的具體例子。我們將編寫規則來決定向通訊的訂閱者傳送哪封電子郵件。我們將看到不同型別的規則,以及我們如何使用Drools規則語言來表達它們。我們還將看到如何配置Drools(劇透:這很容易),並讓系統闡述規則以產生一個我們可以使用的結果。

我認為商業規則是非常有趣的,因為它允許我們以不同的方式來看待問題。作為開發者,我們非常習慣於使用命令式正規化或功能式正規化。然而,還有其他的正規化,比如狀態機和業務規則,這些正規化並不常用,而且在某些情況下可能更適合。

像往常一樣,我們在配套的資源庫中分享本教程中的程式碼。EmailSchedulingRules.。

我們要解決什麼問題
讓我們考慮一下電子郵件營銷這個領域。作為營銷人員,我們有一個對我們的內容感興趣的人的電子郵件列表。他們中的每一個人都可能對一個特定的主題表現出興趣,閱讀過我們的一些文章併購買過某些產品。考慮到他們所有的歷史和喜好,我們希望在每一次向他們傳送最合適的內容。這些內容可能是教育性的,也可能是建議一些交易。問題是,我們要考慮一些限制條件(例如,不在週日傳送電子郵件,或不向已經購買產品的人傳送促銷電子郵件)。

所有這些規則本身是簡單的,但其複雜性來自於它們的組合方式以及它們的互動方式。業務規則引擎將為我們處理這種複雜性,我們所要做的就是清楚地表達這些單一的規則。規則將用我們的領域資料來表達,所以讓我們首先關注我們的領域模型。

我們要解決什麼問題
讓我們考慮一下電子郵件營銷這個領域。作為營銷人員,我們有一個對我們的內容感興趣的人的電子郵件列表。他們中的每一個人都可能對一個特定的主題表現出興趣,閱讀過我們的一些文章併購買過某些產品。考慮到他們所有的歷史和喜好,我們希望在每一次向他們傳送最合適的內容。這些內容可能是教育性的,也可能是建議一些交易。問題是,我們要考慮一些限制條件(例如,不在週日傳送電子郵件,或不向已經購買產品的人傳送促銷電子郵件)。

所有這些規則本身是簡單的,但其複雜性來自於它們的組合方式以及它們的互動方式。業務規則引擎將為我們處理這種複雜性,我們所要做的就是清楚地表達這些單一的規則。規則將用我們的領域資料來表達,所以讓我們首先關注我們的領域模型。





Drools 業務規則引擎的完整教程

我們的系統應該做什麼
我們的系統應該執行所有的規則,使用Drools引擎,為每個使用者決定在某一天應該傳送哪封郵件。其結果可能是決定不傳送任何郵件,或者傳送一封郵件,在眾多可能的郵件中選擇一個。

需要考慮的一個重要問題是,這些規則可能會隨著時間的推移而演變。負責營銷的人可能想嘗試新的規則,看看它們對系統有什麼影響。使用Drools,他們應該很容易增加或刪除規則,或調整現有的規則。

讓我們強調這一點:

這些領域的專家應該能夠對系統進行實驗,並快速地進行嘗試,而不是總是需要開發人員的幫助。

規則
好了,現在我們知道了我們有哪些資料,我們可以根據這個模型來表達規則。

讓我們看看我們可能想寫的一些規則的例子:

  • 我們可能有電子郵件的序列,例如一個課程的內容。它們必須按順序傳送
  • 我們可能有時間敏感的電子郵件,要麼在特定的時間視窗內傳送,要麼根本就不傳送。
  • 我們可能希望避免在一週的特定日子裡傳送電子郵件,例如在使用者所在國家的公共假期。
  • 我們可能希望只向收到某些其他電子郵件的人傳送某些型別的電子郵件(例如提議交易)(例如至少有3封關於同一主題的資訊性電子郵件)。
  • 我們不希望向已經購買了某一產品的使用者推薦該產品的交易。
  • 我們可能想限制我們向使用者傳送電子郵件的頻率。例如,如果我們在過去5天內已經傳送過一封電子郵件,我們可能決定不傳送電子郵件給使用者。



設定drools
設定drools可以非常簡單。我們正在研究在一個獨立的應用程式中執行drools。根據你的情況,這可能是也可能不是一個可接受的解決方案,在某些情況下,你必須研究一下JBoss,支援Drools的應用伺服器。然而,如果你想開始工作,你可以忘掉這些,只需用Gradle(或Maven)配置你的依賴關係。如果你真的需要,你可以稍後再弄清楚那些無聊的配置部分。

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"
}



在我們的Gradle指令碼中,我們使用:
  • Kotlin,因為Kotlin很好用!
  • IDEA,因為它是我最喜歡的IDE
  • Kotlin StdLib,反射和測試
  • Drools



而這就是我們的計劃將被構建的方式:

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()
    }
}

  • 我們從檔案中載入規則。現在我們只是載入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 }
}

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和訂閱者進行操作。它不會只對任何一個人起作用,而是隻對滿足條件 !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 }
}


現在讓我們來看看我們的規則的 "然後 "部分。在這部分中,我們指定了規則被觸發時的情況。當滿足when部分的元素被找到時,該規則將被觸發。

在這種情況下,我們將建立一個EmailScheduling並將其新增到會話中。特別是我們希望在考慮的那一天向被考慮的人傳送序列中的第一封郵件。我們還指定了這封郵件的優先順序(在這種情況下是NORMAL)。當我們有多封郵件時,這對於決定有效地傳送哪一封是必要的。事實上,我們將有另一條規則來檢視這些值,以決定哪封郵件的優先順序(提示:它將是具有最高優先順序的郵件)。

一般來說,你可能想在子句中新增一些東西到會話中。另外,你可能想修改作為會話一部分的物件。你也可以呼叫有副作用的物件上的方法。雖然推薦的方法是限制自己對會話的操作,但你可能想為記錄新增副作用,比如說。這在學習Drools和試圖理解你的第一個規則時特別有用。

編寫一個規則來阻止電子郵件的傳送
我們將看到,我們有兩種可能的規則:安排新郵件的規則和阻止安排的郵件傳送的規則。我們之前已經看到了如何寫一個規則來傳送電子郵件,現在我們將看到如何寫一個電子郵件來阻止一個電子郵件的傳送。

在這個規則中,我們要檢查是否有一封電子郵件預定要傳送給一個在過去三天中已經收到電子郵件的人。如果是這種情況,我們要阻止該郵件的傳送。

rule "Prevent overloading"
   when
      scheduling : EmailScheduling(
            sending.subscriber.hasReceivedEmailsInLastDays(3, day),
            !blocked )

   then
      scheduling.setBlocked(true);
end


在when部分,我們指定這個規則將對一個EmailScheduling進行操作。因此,每當另一個規則將新增一個EmailScheduling時,這個規則就會被觸發,以決定我們是否必須阻止它的傳送。

這個規則將適用於所有針對在過去3天內收到郵件的使用者的排程。此外,我們將檢查該郵件排程是否已經被阻止。如果是這樣的話,我們就不需要應用這個規則。

我們使用排程物件的setBlocked方法來修改一個屬於會話的元素。

在這一點上,我們已經看到了我們將使用的模式。

當我們認為向使用者傳送電子郵件有意義時,我們將建立`EmailScheduling`:
我們將檢查我們是否有理由阻止這些郵件的傳送。如果是這樣的話,我們將把封鎖的標誌設定為真,有效地刪除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;


在這一點上,我們可以在所有的規則中引用這些球狀物。在我們的例子中,我們使用日值來知道我們正在考慮哪一天進行排程。通常情況下是明天,因為我們想提前一天進行排程。然而出於測試的原因,我們可以使用任何我們想要的日子。或者我們可能想使用未來的幾天來進行模擬。

全域性不應該被濫用。我個人喜歡用它們來指定配置引數。其他人更喜歡把這些資料插入會話中,這也是推薦的方法。我之所以使用globals(謹慎且很少),是因為我喜歡區分我正在處理的資料(儲存在會話中)和配置(為此我使用globals)。

編寫通用規則
現在讓我們看看我們所寫的整套通用規則。我們所說的通用規則是指可以應用於我們想要做的所有電子郵件排程的規則。為了補充這些規則,我們可能會有其他的規則用於我們正在推廣的特定產品或主題。

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


讓我們逐一審查所有這些規則。
  • Continue sequence:如果某人開始收到一個電子郵件序列,並且他還沒有收到最後一封郵件,那麼他應該收到該序列的下一封郵件。
  • Start sequence:如果某人還沒有收到序列中的第一封郵件,他應該收到。請注意,從技術上講,僅這一規則就會導致每個已經完成序列的人立即重新開始。由於從不重發同一封郵件的規則,這種情況不會發生。然而,你可以決定重寫這個規則,明確禁止已經收到某個序列的人被重新插入其中。
  • Prevent overloading:如果某人在過去三天內收到過一封郵件,那麼我們應該阻止任何針對該人的郵件排程。
  • Block on holiday:如果某人在節假日,我們不應該向他傳送電子郵件
  • Precedence to time sensitive emails優先考慮時間敏感的電子郵件:給定一對在同一日期指向同一個人的電子郵件排程,如果其中只有一個是時間敏感的,我們應該阻止另一個。
  • Precedence to higher priority emails優先考慮高優先順序的電子郵件:如果在同一日期有一對針對同一個人的電子郵件安排,並且都是時間敏感的或者都不是時間敏感的,我們應該阻止重要性較低的那封。
  • Limit to one email per day限制每天傳送一封郵件:我們不應該安排每天向同一個人傳送超過一封郵件。如果發生這種情況,我們必須以某種方式選擇一個。我們使用內部ID來區分這兩個人。
  • Never resend same email:如果某人已經收到了某封郵件,那麼他在未來就不應該再收到。



編寫書籍郵件的具體規則
我們的營銷專家可能想為特定的產品或主題編寫特定的規則。讓我們假設他們想建立一套電子郵件來推廣和銷售一本書。我們可以將這些規則寫在一個單獨的檔案中,也許由負責銷售該書的營銷專家維護。

為了編寫有關特定主題的規則,我們將利用標籤的優勢,這種機制將給我們帶來一定的靈活性。讓我們看看我們可以寫的規則。

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


讓我們審查一下我們的規則。
  • Send book offer only after at least 3 book presentation emails只有在至少3封圖書介紹郵件之後才傳送圖書報價:如果訂閱者沒有收到至少3封解釋圖書內容的郵件,我們要阻止任何銷售圖書的郵件。
  • Block book offers on monday阻止週一的圖書提供:我們想阻止在週一傳送圖書報價,例如,我們已經看到使用者在一週的那一天不太願意購買。
  • Block book offers for people who bought阻止向已購買的人提供圖書:我們不想向已購買的訂戶提出圖書交易。



測試業務規則
我們可能想寫不同型別的測試來驗證我們的規則是否符合預期。在光譜的一邊,我們可能希望有測試來驗證複雜的場景,並檢查規則之間的意外互動。這些測試將考慮複雜的資料集和整個業務規則集的執行。在光譜的另一邊,我們可能想寫簡單的單元測試來驗證單個規則。我們將看到這些單元測試的一個例子,但我們看到的大部分內容可以調整為測試整個規則集而不是單一規則。

我們在單元測試中想做什麼?

  1. 我們設定知識庫
  2. 我們想把一些資料載入到會話中
  3. 我們要執行規則業務引擎,只啟用一個我們要測試的業務規則
  4. 我們要驗證所產生的電子郵件排程是否與預期的一樣。
  5. 為了滿足第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 }

在這一點上,我們可以簡單地檢查結果。

一旦這個基礎設施到位,我們要寫的測試將看起來像這樣:

@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 }

        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將允許操作我們所看到的領域模型(訂閱者、電子郵件等),並執行營銷人員感興趣的兩個動作:安排和阻止電子郵件。我們可以提供一個簡單的編輯器,帶有自動完成和錯誤檢查功能,並在其中整合一個測試和模擬環境。在這種情況下,營銷人員將是完全獨立的,能夠快速設計和驗證他們的規則,而且所需的支援非常有限。



 

相關文章