[翻譯]squbs官網(wǎng)之15 業(yè)務(wù)流程 DSL

業(yè)務(wù)流程是服務(wù)的主要用例之一,你是否嘗試以盡可能多的并發(fā)方式編排多個(gè)服務(wù)調(diào)用,因此可以獲得盡可能好的響應(yīng)時(shí)間,或者你嘗試進(jìn)行多個(gè)業(yè)務(wù)操作,數(shù)據(jù)寫入,數(shù)據(jù)讀取, 服務(wù)調(diào)用等等,依賴于彼此等等。簡潔地描述您的業(yè)務(wù)邏輯的能力對(duì)于使服務(wù)易于理解和維護(hù)至關(guān)重要。業(yè)務(wù)流程DSL——squbs-pattern的一部分——將使異步代碼易于讀寫和推理。

依賴

業(yè)務(wù)流DSL是squbs-pattern的一部分。請(qǐng)?zhí)砑尤缦乱蕾嚕?/p>

"org.squbs" %% "squbs-pattern" % squbsVersion,
"com.typesafe.akka" %% "akka-contrib" % akkaVersion

開始

讓我們開始一個(gè)簡單但完整的業(yè)務(wù)流程示例。這個(gè)業(yè)務(wù)流程由3個(gè)相互關(guān)聯(lián)的異步任務(wù)構(gòu)成:

  1. 加載請(qǐng)求此業(yè)務(wù)流程的用戶。
  2. 加載項(xiàng)目。詳細(xì)信息依賴于查看的用戶。
  3. 生成項(xiàng)目視圖,基于用戶和項(xiàng)目數(shù)據(jù)。

讓我們深入到流程和細(xì)節(jié):

    // 1. Define the orchestrator actor.
class MyOrchestrator extends Actor with Orchestrator {

    // 2. Provide the initial expectOnce block that will receive the request message.
  expectOnce {
    case r: MyOrchestrationRequest => orchestrate(sender(), r)
  }

    // 3. Define orchestrate - the orchestration function.
  def orchestrate(requester: ActorRef, request: MyOrchestrationRequest) {

    // 4. Compose the orchestration flow using pipes (>>) as needed by the business logic.
    val userF = loadViewingUser
    val itemF = userF >> loadItem(request.itemId)
    val itemViewF = (userF, itemF) >> buildItemView

    // 5. Conclude and send back the result of the orchestration.    
    for {
      user <- userF
      item <- itemF
      itemView <- itemViewF
    } {
      requester ! MyOrchestrationResult(user, item, itemView)
      context.stop(self)
    }

    // 6. Make sure to stop the orchestrator actor by calling
    //    context.stop(self).
  }

    // 7. Implement the orchestration functions as in the following patterns.
  def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
    val itemPromise = OPromise[Option[Item]]

    context.actorOf(Props[ItemActor]) ! ItemRequest(itemId, seller.id)

    expectOnce {
      case item: Item => itemPromise success Some(item)
      case e: NoSuchItem => itemPromise success None
    }

    itemPromise.future
  }

  def loadViewingUser: OFuture[Option[User]] = {
    val userPromise = OPromise[Option[User]]
    ...
    userPromise.future
  }

  def buildItemView(user: Option[User], item: Option[Item]): OFuture[Option[ItemView]] = {
    ...
  }
}

依賴

添加如下依賴:

"org.squbs" %% "squbs-pattern" % squbsVersion

核心概念

業(yè)務(wù)流程
Orchestrator是由actor繼承用于支持業(yè)務(wù)流程功能的特質(zhì)。從技術(shù)上講, 它是聚合器的子特性, 并提供其所有功能。從技術(shù)上講, 它是Aggregator的子特質(zhì), 并提供其所有功能。此外, 它還提供了功能和語法, 允許有效的業(yè)務(wù)流程組合, 以及在下面詳細(xì)討論的創(chuàng)建業(yè)務(wù)流程功能時(shí)經(jīng)常需要的實(shí)用工具。要使用業(yè)務(wù)流程, actor可以簡單地繼承Orchestrator特質(zhì)。

import org.squbs.pattern.orchestration.Orchestrator

class MyOrchestrator extends Actor with Orchestrator {
  ...
}

與Aggregator類似, 業(yè)務(wù)流程通常不聲明Akka actor接收塊, 但允許expect/expectOnce/unexpect 塊來定義在任何點(diǎn)上預(yù)期的響應(yīng)。這些預(yù)期代碼塊通常從內(nèi)部業(yè)務(wù)流程函數(shù)中使用。

業(yè)務(wù)流程的Future和Promise
業(yè)務(wù)流程的promise和future與scala.concurrent.Future 和 scala.concurrent.Promise 非常相似,這里描述的指示一個(gè)名字的變化到OFuture和OPromise,
標(biāo)志著它們應(yīng)當(dāng)在actor里用于業(yè)務(wù)流程。業(yè)務(wù)流程版本通過它們與沒有并發(fā)行為的工件版本區(qū)分開來。它們?cè)诤灻胁皇褂?(隱式) ExecutionContext, 也不用于執(zhí)行。它們還缺少一些顯式異步執(zhí)行閉包的函數(shù)。在演員內(nèi)部使用, 他們的回調(diào)絕不會(huì)超出Actor的范圍之外。這將消除由于回調(diào)而從不同線程同時(shí)修改Actor狀態(tài)的危險(xiǎn)。此外, 它們還包括性能優(yōu)化, 假設(shè)它們總是在Actor內(nèi)部使用。

注意:不要傳遞一個(gè)OFuture 到actor外面。提供了隱式轉(zhuǎn)換以在 scala.concurrent.Future 和 OFuture 之間進(jìn)行轉(zhuǎn)換。

import org.squbs.pattern.orchestration.{OFuture, OPromise} 

異步的業(yè)務(wù)流程函數(shù)
業(yè)務(wù)流程函數(shù)是一個(gè)被稱為orchestration 流的函數(shù),用來執(zhí)行單一業(yè)務(wù)流程任務(wù),例如調(diào)用一個(gè)服務(wù)或者函數(shù)。業(yè)務(wù)流程功能必須符合以下準(zhǔn)則:
1,它必須以非Future參數(shù)作為輸入。根據(jù) Scala 的當(dāng)前限制, 參數(shù)的數(shù)目最多可達(dá)22。在所有情況下, 這些函數(shù)不應(yīng)該有那么多參數(shù)。
2,Scala函數(shù)可以柯里化,從 piped (future)輸入中直接分離輸入。管道輸入必須是在柯里化函數(shù)中最后一組參數(shù)。
3,它必須導(dǎo)致異步執(zhí)行。異步執(zhí)行通常通過發(fā)送消息給不同的actor來實(shí)現(xiàn)。
4,Scala實(shí)現(xiàn)必須返回一個(gè)OFuture (orchestration future)。

以下是業(yè)務(wù)流程函數(shù)的例子:

def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  val itemPromise = OPromise[Option[Item]]

  context.actorOf(Props[ItemActor]) ! ItemRequest(itemId, seller.id)

  expectOnce {
    case item: Item => itemPromise success Some(item)
    case e: NoSuchItem => itemPromise success None
  }

  itemPromise.future
}

這個(gè)例子,函數(shù)是柯里化的。itemId 參數(shù)是同步傳遞的, 而發(fā)送方是異步傳遞的。

下面的示例在邏輯上與第一個(gè)示例相同, 只是使用 ask 而不是tell:

private def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {

  import context.dispatcher
  implicit val timeout = Timeout(3 seconds)

  (context.actorOf[ItemActor] ? ItemRequest(itemId, seller.id)).mapTo[Option[Item]]
}

此例中,ask ? 操作返回了一個(gè)scala.concurrent.Future。Orchestrator 特質(zhì)提供了scala.concurrent.Future 和OFuture的隱式轉(zhuǎn)換,所以ask ?的結(jié)果轉(zhuǎn)換為這個(gè)函數(shù)申明的返回類型 OFuture 且無需顯示調(diào)用轉(zhuǎn)換。

ask 或者?似乎需要編寫更少的代碼, 但它既不性能, 也不像expect/expectOnce那樣靈活 。預(yù)期塊中的邏輯也可用于結(jié)果的進(jìn)一步轉(zhuǎn)換。同樣可以通過ask返回的future使用回調(diào)來實(shí)現(xiàn)。但是, 由于以下原因, 無法輕松地補(bǔ)償性能:
1,Ask將創(chuàng)建一個(gè)新的actor作為響應(yīng)接收者。
2,從 scala.concurrent.Future 到 OFuture 的轉(zhuǎn)換以及 Java API 的填充操作需要將消息發(fā)送回orchestrator, 從而添加一個(gè)消息跳轉(zhuǎn), 同時(shí)增加了延遲和 CPU。

測(cè)試顯示,當(dāng)使用ask,與expect/expectOnce相對(duì)時(shí),有更高的延遲和CPU利用。

組合
管道,或者>>符號(hào)使用一個(gè)或多個(gè)業(yè)務(wù)流程future OFuture ,并使其結(jié)果作為業(yè)務(wù)流程功能的輸入。當(dāng)所有表示對(duì)函數(shù)輸入的 OFutures 都被解析時(shí), 實(shí)際的業(yè)務(wù)流程函數(shù)調(diào)用將異步發(fā)生。

管道是業(yè)務(wù)流程 DSL 的主要組件, 它允許根據(jù)其輸入和輸出組成業(yè)務(wù)流程功能。業(yè)務(wù)流程流是由業(yè)務(wù)流程聲明隱式地確定的,或者通過管道來聲明業(yè)務(wù)流程流。

當(dāng)多個(gè) OFutures 被輸送到一個(gè)業(yè)務(wù)流程函數(shù)時(shí), OFutures 需要以逗號(hào)分隔并括在圓括號(hào)中, 構(gòu)造一個(gè) OFutures 的元組作為輸入。元組中的元素,它們的 OFuture 類型必須與函數(shù)參數(shù)和類型匹配, 或者是在柯里化的情況下的最后一組參數(shù), 否則編譯將失敗。此類錯(cuò)誤通常也由 IDE 捕獲。

下面的例子展示了一個(gè)簡單的業(yè)務(wù)流程聲明以及使用前面章節(jié)申明的loadItem 業(yè)務(wù)流程函數(shù):

val userF = loadViewingUser
val itemF = userF >> loadItem(request.itemId)
val itemViewF = (userF, itemF) >> buildItemView

上面的代碼可以如下描述:

  • 首先調(diào)用loadViewingUser (不帶輸入?yún)?shù))
  • 當(dāng)查看用戶可用的時(shí)候,使用查看用戶作為調(diào)用loadItem 的輸入(在這種情況下前面有個(gè)itemId可用)。本例中,loadItem遵循上面業(yè)務(wù)流程函數(shù)聲明中確切的簽名。
  • 當(dāng)用戶和項(xiàng)目可用時(shí),調(diào)用buildItemView。

業(yè)務(wù)流程實(shí)例生命周期

業(yè)務(wù)流程通常是一次性actor。它們接收初始請(qǐng)求, 然后根據(jù)調(diào)用的業(yè)務(wù)流程函數(shù)發(fā)出的請(qǐng)求進(jìn)行多個(gè)響應(yīng)。

為了允許一個(gè)業(yè)務(wù)流程服務(wù)多個(gè)業(yè)務(wù)流程請(qǐng)求,業(yè)務(wù)流程必須結(jié)合每個(gè)請(qǐng)求的輸入和響應(yīng), 并將它們與不同的請(qǐng)求隔離。這將大大使其發(fā)展復(fù)雜化,并且可能不會(huì)按照這些示例中看到的清晰的業(yè)務(wù)流程表示中得到最終結(jié)果。創(chuàng)建一個(gè)新的actor是足夠廉價(jià)的,我們能夠容易地為每個(gè)業(yè)務(wù)流程請(qǐng)求創(chuàng)建一個(gè)新的業(yè)務(wù)流程actor。

業(yè)務(wù)流程回調(diào)的最后部分是停止actor。在Scala中,通過調(diào)用context.stop(self)或者context stop self (如果中綴表示法是首選)。

完成業(yè)務(wù)流程流

在這里, 我們把上述所有的概念放在一起。用更完整的解釋來重復(fù)上面的例子:

    // 1. Define the orchestrator actor.
class MyOrchestrator extends Actor with Orchestrator {

    // 2. Provide the initial expectOnce block that will receive the request message.
    //    After this request message is received, the same request will not be
    //    expected again for the same actor.
    //    The expectOnce likely has one case match which is the initial request and
    //    uses the request arguments or members, and the sender() to call the high
    //    level orchestration function. This function is usually named orchestrate.
  expectOnce {
    case r: MyOrchestrationRequest => orchestrate(sender(), r)
  }

    // 3. Define orchestrate. Its arguments become immutable by default
    //    allowing developers to rely on the fact these will never change.
  def orchestrate(requester: ActorRef, request: MyOrchestrationRequest) {

    // 4. If there is anything we need to do synchronously to setup for
    //    the orchestration, do this in the first part of orchestrate.

    // 5. Compose the orchestration flow using pipes as needed by the business logic.
    val userF = loadViewingUser
    val itemF = userF >> loadItem(request.itemId)
    val itemViewF = (userF, itemF) >> buildItemView

    // 6. End the flow by calling functions composing the response(s) back to the
    //    requester. If the composition is very large, it may be more readable to
    //    use for-comprehensions rather than a composition function with very large
    //    number of arguments. There may be multiple such compositions in case partial
    //    responses are desired. This example shows the use of a for-comprehension
    //    just for reference. You can also use an orchestration function with
    //    3 arguments plus the requester in such small cases.

    for {
      user <- userF
      item <- itemF
      itemView <- itemViewF
    } {
      requester ! MyOrchestrationResult(user, item, itemView)
      context.stop(self)
    }

    // 7. Make sure the last response stops the orchestrator actor by calling
    //    context.stop(self).
  }

    // 8. Implement the asynchronous orchestration functions inside the
    //    orchestrator actor, but outside the orchestrate function.
  def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
    val itemPromise = OPromise[Option[Item]]

    context.actorOf[ItemActor] ! ItemRequest(itemId, seller.id)

    expectOnce {
      case item: Item    => itemPromise success Some(item)
      case e: NoSuchItem => itemPromise success None
    }

    itemPromise.future
  }

  def loadViewingUser: OFuture[Option[User]] = {
    val userPromise = OPromise[Option[User]]
    ...
    userPromise.future
  }

  def buildItemView(user: Option[User], item: Option[Item]): OFuture[Option[ItemView]] = {
    ...
  }
}

業(yè)務(wù)流程函數(shù)重用

業(yè)務(wù)流程功能通常依賴于Orchestrator特質(zhì)提供的工具, 無法獨(dú)立運(yùn)行。但是, 在許多情況下, 需要在多個(gè)協(xié)調(diào)中重用業(yè)務(wù)流程功能, 以不同的方式進(jìn)行編排。在這種情況下, 重要的是將業(yè)務(wù)流程功能分成不同的特質(zhì), 這些特質(zhì)將被混合到每個(gè)業(yè)務(wù)流程中。該特性必須具有對(duì)業(yè)務(wù)流程功能的訪問權(quán)限, 并且需要對(duì)Orchestrator的自我引用。下面顯示了這樣一個(gè)特質(zhì)的示例:

trait OrchestrationFunctions { this: Actor with Orchestrator =>

  def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
    ...
  }
}

上面例子Actor with Orchestrator 是一個(gè)類型自應(yīng)用。它告訴Scala編譯器,這個(gè)特質(zhì)只能夠混入到一個(gè)同樣也是Orchestrator 的Actor,因此可以訪問Actor和Orchestrator提供的工具。

要使用業(yè)務(wù)流程中的 OrchestrationFunctions特質(zhì), 你只需將這個(gè)特質(zhì)混合到一個(gè)業(yè)務(wù)流程中, 如下所示:

class MyOrchestrator extends Actor with Orchestrator with OrchestrationFunctions {
  ...
}

確保響應(yīng)的唯一性

當(dāng)使用expect 或 expectOnce時(shí),我們受到單個(gè)期望塊的模式匹配功能的限制, 它的范圍有限, 無法區(qū)分多個(gè)業(yè)務(wù)流程函數(shù)中的多個(gè)預(yù)期塊之間的匹配。在同一業(yè)務(wù)流程函數(shù)中聲明預(yù)期之前, 收到的請(qǐng)求消息中沒有邏輯鏈接的。對(duì)于復(fù)雜的業(yè)務(wù)流程, 我們可能會(huì)遇到消息混淆的問題。響應(yīng)沒有與正確的請(qǐng)求關(guān)聯(lián), 也沒有正確處理。解決這個(gè)問題有幾個(gè)策略:

如果初始消息的接收者,因此響應(yīng)消息的發(fā)送者是唯一的,那么匹配可能包括對(duì)消息的發(fā)送方的引用,就像在下面的示例模式匹配中一樣。

def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  val itemPromise = OPromise[Option[Item]]

  val itemActor = context.actorOf(Props[ItemActor])
  itemActor ! ItemRequest(itemId, seller.id)

  expectOnce {
    case item: Item    if sender() == itemActor => itemPromise success Some(item)
    case e: NoSuchItem if sender() == itemActor => itemPromise success None
  }

  itemPromise.future
}

或者, Orchestrator 特質(zhì)提供了一個(gè)消息 id 生成器, 它在與參與者實(shí)例結(jié)合時(shí)是唯一的。我們可以使用這個(gè) id 發(fā)生器生成一個(gè)唯一的消息 id。接受此類消息的Actor只需要將此消息 id 作為響應(yīng)消息的一部分返回。下面的示例顯示了使用消息 id 生成器的業(yè)務(wù)流程函數(shù)。

def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  val itemPromise = OPromise[Option[Item]]

  // Generate the message id.
  val msgId = nextMessageId  
  context.actorOf(Props[ItemActor]) ! ItemRequest(msgId, itemId, seller.id)

  // Use the message id as part of the response pattern match. It needs to
  // be back-quoted as to not be interpreted as variable extractions, where
  // a new variable is created by extraction from the matched object.
  expectOnce {
    case item @ Item(`msgId`, _, _) => itemPromise success Some(item)
    case NoSuchItem(`msgId`, _)     => itemPromise success None
  }

  itemPromise.future
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容