在akka类型的actor组之间实现模块边界的公认做法是什么?
这是下面示例的working repo。
我如何实现一个接收两个不同协议中定义的消息的pre-actor,类似于在OO中实现两个不同的接口。
边界是经典的OO接口边界:仅公开与其他模块相关的操作。
例如:考虑爱丽丝,鲍勃和查理。爱丽丝喜欢和鲍勃说话,查理经常想知道鲍勃的表现。查理不了解爱丽丝(也不应该),反之亦然。在每对之间存在一种协议,它们可以相互接收哪些消息:
trait Protocol[ From, To ]
object Alice
{
sealed trait BobToAlice extends Protocol[ Bob, Alice ]
case object ApologizeToAlice extends BobToAlice
case object LaughAtAlice extends BobToAlice
}
object Bob
{
sealed trait AliceToBob extends Protocol[ Alice, Bob ]
case object SingToBob extends AliceToBob
case object ScoldBob extends AliceToBob
sealed trait CharlieToBob extends Protocol[ Charlie, Bob ]
case object HowYouDoinBob extends CharlieToBob
}
object Charlie
{
sealed trait BobToCharlie extends Protocol[ Bob, Charlie ]
case object CryToCharlie extends BobToCharlie
case object LaughToCharlie extends BobToCharlie
}
这里的边界是Bob的两个面孔:与Alice交谈和与Charlie交谈是两种不同的协议。现在,每个人都可以与Bob交谈,而无需彼此了解。例如,爱丽丝喜欢唱歌,但不喜欢被她嘲笑:
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.Behaviors.same
import akka.actor.typed.{ ActorRef, Behavior }
class Alice( bob: ActorRef[ Protocol[ Alice, Bob ] ] )
{
import Alice._
import nl.papendorp.solipsism.protocol.Bob.{ ScoldBob, SingToBob }
val talkToBob: Behavior[ BobToAlice ] = Behaviors.receiveMessage
{
case LaughAtAlice =>
bob ! ScoldBob
same
case ApologizeToAlice =>
bob ! SingToBob
same
}
}
另一方面,查理只关心鲍勃此刻的感受:
import akka.actor.typed.scaladsl.Behaviors.{ receiveMessage, same }
import akka.actor.typed.{ ActorRef, Behavior }
class Charlie(bob: ActorRef[Protocol[Charlie,Bob]])
{
import Charlie._
import nl.papendorp.solipsism.protocol.Bob.HowYouDoinBob
val concerned: Behavior[BobToCharlie] = receiveMessage
{
case CryToCharlie =>
bob ! HowYouDoinBob
same
case LaughToCharlie =>
bob ! HowYouDoinBob
same
}
}
但是,爱丽丝对鲍勃情绪的影响会影响鲍勃与查理说话的方式。为此,我们需要通过BobsPersonalLife
统一这两个协议,以便能够在单个参与者中表示它们:
import akka.actor.typed.scaladsl.Behaviors._
import akka.actor.typed.{ ActorRef, Behavior }
import Alice.BobToAlice
import Charlie.BobToCharlie
object Bob
{
private[ Bob ] sealed trait BobsPersonalLife
sealed trait AliceToBob extends Protocol[Alice, Bob] with BobsPersonalLife
case object SingToBob extends AliceToBob
case object ScoldBob extends AliceToBob
sealed trait CharlieToBob extends Protocol[Charlie, Bob] with BobsPersonalLife
case object HowYouDoinBob extends CharlieToBob
}
class Bob( alice: ActorRef[BobToAlice], charlie: ActorRef[BobToCharlie] )
{
import Alice._
import Bob._
import Charlie._
private val happy: Behavior[ BobsPersonalLife ] = receiveMessage
{
case HowYouDoinBob =>
charlie ! LaughToCharlie
same
case ScoldBob =>
alice ! ApologizeToAlice
sad
case SingToBob =>
alice ! LaughAtAlice
same
}
val sad: Behavior[ BobsPersonalLife ] = receiveMessage
{
case HowYouDoinBob =>
charlie ! CryToCharlie
same
case ScoldBob =>
alice ! ApologizeToAlice
same
case SingToBob =>
alice ! LaughAtAlice
happy
}
}
到目前为止,太好了。我们可以使用ActorRef.narrow[ _X_ToBob ]
实例化Alice和Charlie。但是鲍勃呢?还是鲍勃·阿尔特斯?如果我们想用DorisToBob extends Protocol[ Doris, Bob ]
用鲍里斯(Boris)代替鲍勃(Boris),而不是向查理(Charlie)而是向多丽丝(Doris)抱怨,我们将无法再收到来自爱丽丝(Alice)的消息,因为没有AliceToBob
和{{ 1}}。突然,DorisToBob
是每个Bob Alice可以交谈的锁定对象。
用鲍里斯代替鲍勃的方式是什么?如果我们使用BobsPersonalLife
,我们将失去类型安全性。如果我们在共享状态下使用两个参与者,则会失去线程安全性。包装_X_ToBob(例如ActorRef.unsafeUpcast
或Dotty的速记联合类型)也不起作用,因为包装程序仅接管Either[ AliceToBob, CharlieToBob ]
的角色。当我们仅让BobsPersonalLife
继承自DorisToBob
时,我们最终将所有Bobs alter-ego的所有可能合作伙伴合并在一起,永远无法删除其中的任何一个。
我们如何在Bob内的Alice和Charlie之间实现真正的类型安全解耦?
答案 0 :(得分:1)
我认为这是一个X:Y问题(“如何在Akka中实现界面边界”与“如何在Akka中实现界面边界的目标”)。
object Protocol {
sealed trait Message
sealed trait LaughReply extends Message
sealed trait MoodReply extends Message
case class Apology(from: ActorRef[Singing]) extends Message
case class Singing(from: ActorRef[Laughing]) extends Message
case class Laughing(from: ActorRef[LaughReply]) extends Message with MoodReply
case class HowYouDoin(replyTo: ActorRef[MoodReply]) extends Message with LaughReply
case class Scolding(from: ActorRef[Apology]) extends Message with LaughReply
case class Crying(from: ActorRef[HowYouDoin]) extends Message with MoodReply
}
object Alice {
val talkToBob: Behavior[Message] = Behaviors.receive { (context, msg) =>
msg match {
case Apology(from) =>
from ! Singing(context.self)
Behaviors.same
case Laughing(from) =>
from ! Scolding(context.self)
Behaviors.same
case _ => // Every other message is ignored by Alice
Behaviors.same
}
}
}
object Charlie {
val concerned: Behavior[Message] = Behaviors.receive { (context, msg) =>
msg match {
case Crying(from) =>
from ! HowYouDoin(context.self)
Behaviors.same
case Laughing(from) =>
from ! HowYouDoin(context.self)
Behaviors.same
case _ =>
Behaviors.same
}
}
}
object Bob {
val happy: Behavior[Message] = Behaviors.receive { (context, msg) =>
msg match {
case HowYouDoin(replyTo) =>
replyTo ! Laughing(context.self)
Behaviors.same
case Scolding(from) =>
from ! Apology(context.self)
sad
case Singing(from) =>
from ! Laughing(context.self)
Behaviors.same
case _ =>
Behaviors.same
}
}
val sad: Behavior[Message] = Behaviors.receive { (context, msg) =>
msg match {
case HowYouDoin(replyTo) =>
replyTo ! Crying(context.self)
Behaviors.same
case Scolding(from) =>
from ! Apology(context.self)
Behaviors.same
case Singing(from) =>
from ! Laughing(context.self)
Behaviors.same
case _ =>
Behaviors.same
}
}
}
诀窍基本上是通过mixin对协议进行分解,并对消息中的协议状态(接受哪些消息)进行编码。只要没有人持有对ActorRef[Message]
的引用(ActorRef
是对立的,因此ActorRef[LaughReply]
不是ActorRef[Message]
),就无法发送目标尚未承诺接受。请注意,将ActorRef
保持在演员状态下会有效地解决此问题:如果您要让另一个ActorRef
保持在演员状态下,则这是一个很明显的信号,表明IMO不在所有有兴趣将它们分离的人。
一个替代而不是总体协议的方法是为每个Alice / Bob / Charlie / etc建立协议。使用仅在该参与者的上下文中定义的命令和答复,并使用例如类型化的询问模式来使目标参与者的答复协议适应请求参与者的命令协议。