这是一个关于如何模拟akka演员的更一般的设计问题。情况就一个非常简单的例子进行了解释,我想对可能性和方法及其优缺点进行更全面的回答。
有一个用户对象可以用来做某事。让我们说:doSomethingWithUser(user: User)
。比方说,用户有一个属性avatar: Option[String]
,这是一个网址。如果存在,则应在运行doSomethingWithUser
方法之前抓取网址后面的实际图像。在谈论Akka,我会创建一个可以收到两条消息的演员DoSomethingWithUserActor
:
case class NewUser(user: User)
case class NewUserWithImage(user: User, imageData: Array[Byte])
抓取图像数据实现为可以处理一条消息的Actor FetchImageActor
:
case class FetchImage(url: String)
并生成一条消息:
case class GotImage(imageData: Array[Byte])
MainActor
是根演员,只接收一条消息NewUser
,其处理方式如下:
def receive {
case newUser: NewUser => {
newUser.avatar match {
case Some(avatar) => {
// here I would like to send a message to the FetchImageActor,
// wait for the response (GotImage) and once it's there send a
// NewUserWithImage message to the DoSomethingWithUser actor.
//
// How can this be done?
// Is it a good idea to use a Future here, and if so, how can this
// be done?
//
// pseudocode:
val gotImage: GotImage = // get it somehow
doSomethingWithUserActor ! NewUserWithImage(newUser.user, gotImage.imageData)
}
case _ => doSomethingWithUserActor forward NewUser(newUser.user)
}
}
DoSomethingWithUserActor
处理邮件NewUser
和NewUserWithImage
。也许是这样的:
def receive {
case newUser: NewUser => doSomethingWithUser(newUser.user)
case newUserWithImage: NewUserWithImage => {
doSomethingWithImage(newUserWithImage.imageData)
doSomethingWithUser(newUser.user)
}
}
private def doSomethingWithUser(user: User) = { ... }
private def doSomethingWithImage(imageData: Array[Byte]) = { ... }
首先我不知道,如果用户有一个头像,如何进行异步调用,其次,我不知道这是否一般是以这种方式处理这个问题的好方法。
另一种方法可能是将NewUser消息转发到FetchImageActor。然后,该actor检查用户是否具有avatar属性集,如果是,则获取图像并将NewUserWithImage消息发送回MainActor,MainActor将此消息转发给DoSomethingWithUserActor,然后DoSomethingWithUserActor实际上对包含的用户对象和图像执行某些操作数据。我觉得这很糟糕,因为FetchImageActor需要有关用户的知识,但它只是用于获取图像。这是两个不同的独立方面,不应混合在一起。在这种情况下,FetchImage
消息对象也需要用户属性(我不喜欢如前所述)。
解决这个问题的正确或“好”策略是什么?
答案 0 :(得分:1)
如果您需要互连演员,您可能会发现SynapseGrid库很有用。
对于异步调用Future
是常用方法。
val gotImageFuture = new Future { fetchImageActor ? FetchImage(avatar) }
gotImageFuture.onSuccess( (gotImage: GotImage) =>
doSomethingWithUserActor ! NewUserWithImage(newUser.user, gotImage.imageData)
)
如果您添加相关令牌(User
或至少url
)以获取消息:
GotImage(user:User, imageData: Array[Byte])
FetchImage(url:String, user:User)
那么你可以简单地使用fire-forget。主要演员只需处理GotImage
:
...
case GotImage(user, imageData) =>
doSomethingWithUserActor ! NewUserWithImage(user, imageData)
这种方法是畅通无阻的。
P.S。一个小建议:
case class NewUser(user: User, imageData:Option[Array[Byte]])
可以使处理更容易。
答案 1 :(得分:1)
对您的用例非常有用的一种模式是使用ask-pipeTo。
使用ask模式获取请求的未来,然后(可选)对结果进行一些转换(下面的示例结合了几个响应,您也可以在将来调用map)并使用pipeTo模式将未来的结果发送给不同的演员。这与Arseniy在第2点中的建议非常相似,实际上pipeTo在未来使用onSuccess进行注册。
以下是优秀akka docs:
的示例import akka.pattern.{ ask, pipe }
import system.dispatcher // The ExecutionContext that will be used
case class Result(x: Int, s: String, d: Double)
case object Request
implicit val timeout = Timeout(5 seconds) // needed for `?` below
val f: Future[Result] =
for {
x ← ask(actorA, Request).mapTo[Int] // call pattern directly
s ← (actorB ask Request).mapTo[String] // call by implicit conversion
d ← (actorC ? Request).mapTo[Double] // call by symbolic name
} yield Result(x, s, d)
f pipeTo actorD // .. or ..
pipe(f) to actorD
我喜欢在这种情况下使用的另一种模式是使用临时演员。请记住,actor与线程不同,它们非常轻量级,创建它们不会带来太多开销。
你可以这样做:
val tempActor = context.actorOf(Props(classOf[DoSomethingWithUserActor], newUser.user)
fetchImageActor.tell(FetchImage(image), tempActor)
这样,DoSomethingActor已经有了对用户的引用,并且通过将其设置为发送者,FetchImageActor可以只回复该消息,并且您的临时actor将接收该图像。稍微使用这个想法,我发现它在正确使用时非常强大。