如何使用Akka实现类型安全、线程安全的边界?

jei2mxaa  于 2022-11-06  发布在  其他
关注(0)|答案(1)|浏览(166)

在akka类型中实现参与者组之间的模块边界的公认做法是什么?

TL/DR

下面是一个working repo的例子。
我如何实现一个接收两个不同协议中定义的消息(预定义的)的参与者,类似于在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的两个面:与爱丽丝交谈和与查理交谈是两种不同的协议。2现在,每一个人都可以与鲍勃交谈,而不需要知道另一个人。3例如,爱丽丝喜欢唱歌,但在唱歌时不要被嘲笑:

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

然而,Alice对Bob情绪的影响会影响Bob和Charlie的谈话方式,为此,我们需要通过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。但是Bob呢?或者更确切地说,Bob的另一个自我呢?如果我们想用Boris来代替Bob,Boris不会向Charlie抱怨,而是向Doris抱怨,使用DorisToBob extends Protocol[ Doris, Bob ],我们就再也不能接收来自Alice的消息了,因为AliceToBobDorisToBob没有共享的超级特性。突然,BobsPersonalLife是每个Bob Alice可以交谈的人的锁定。
用Boris替换Bob的方法是什么?如果我们使用ActorRef.unsafeUpcast,我们将失去类型安全。如果我们在共享状态上使用两个参与者,我们将失去线程安全。Wrapping _X_ToBob(例如Either[ AliceToBob, CharlieToBob ]或Dotty的速记联合类型)也不起作用,因为 Package 器只是接管了BobsPersonalLife的角色。当我们只是让DorisToBob继承BobsPersonalLife时,我们最终得到了所有Bobs alter-ego的所有可能伙伴的并集,永远不能删除它们中的任何一个。

问题

我们如何在Bob中实现Alice和Charlie之间真正的类型安全解耦?

vnjpjtjt

vnjpjtjt1#

我认为这几乎是一个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,这是一个非常强烈的信号,表明您根本不想将它们解耦。
替代性的,而不是支配性的协议,是具有用于Alice/Bob/Charlie等中的每一个的协议,其中命令和应答仅在该行动者的上下文中定义,并且使用例如键入的ask模式来使目标行动者的应答协议适应于请求行动者的命令协议。

相关问题