Akka — отличный инструмент для написания распределенных приложений. Однако меня всегда удивляло то, что, будучи базирующимся на Scala, который является языком, очень безопасным для типов, элементарная конструкция в Akka — актер — на самом деле не является безопасной для типов. Вы можете отправить любое сообщение любому актеру и получить в ответ любой объект.
Предстоящий выпуск 2.2 Akka будет содержать экспериментальную реализацию типизированных каналов , которые используют макросы для обеспечения безопасности типов при взаимодействии акторов; но, прежде чем это будет окончательно, вот более простой и менее мощный подход, чтобы добавить некоторую типизацию в шаблон запроса .
Вы можете посмотреть на шаблон запроса как на вид асинхронного вызова метода: вы
отправить сообщение (его класс соответствует имени метода) с некоторыми аргументами и ожидать ответа (результат метода). Точнее говоря, мы возвращаем Future
, которое в конечном итоге будет содержать ответ (если есть). Обратите внимание, что ответ также может иметь тип Unit
, соответствующий методам, не возвращающим результат. Зная, когда такие «методы» завершены, все же может быть полезно.
Очень простой пример использования базового шаблона запроса:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import akka.pattern.ask case class LookupUser(id : Int) // the actor impl should send back a message to the sender val userFuture = actor ? LookupUser( 10 ) // userFuture: Future[Any] userFuture onSuccess { case result = > { // do something with the result // result has type Any } } |
Не очень хорошая вещь здесь, что возвращаемый тип ?
(см. реализацию AskSupport ) является Future[Any]
, поскольку субъект может ответить любым сообщением. Однако в идеале при отправке LookupUser
мы хотели бы получить Future[Option[User]]
, при отправке UserCount
a Future[Int]
и так далее.
Это на самом деле довольно легко реализовать. Прежде всего, мы должны как-то вставить ожидаемый тип ответа в сообщение. Для этого мы можем использовать признак, который принимает тип ожидаемого ответа в качестве параметра типа:
1
|
trait Replyable[T] |
Это можно использовать в сообщениях, которые мы отправляем актеру:
1
2
|
case class LookupUser(id : Int) extends Replyable[Option[User]] case class UserCount() extends Replyable[Int] |
Теперь нам нужен вариант ?
который возвращает будущее с правильным параметром типа:
01
02
03
04
05
06
07
08
09
10
|
trait ReplySupport { implicit class ReplyActorRef(actorRef : ActorRef) { def ?[T](message : Replyable[T]) ( implicit timeout : Timeout, tag : ClassTag[T]) : Future[T] = { akka.pattern.ask(actorRef, message).mapTo[T] } } } package object reply extends ReplySupport |
Вы можете видеть, что мы просто повторно используем существующую реализацию Ask и сопоставляем получающееся будущее с правильным типом. ClassTag
аут является неявным параметром ask
и ClassTag
mapTo
, поэтому мы должны также включить их в сигнатуру.
Использование довольно простое, фактически оно почти такое же, как и раньше, за исключением импорта и того, что будущее имеет правильный тип:
01
02
03
04
05
06
07
08
09
10
11
12
|
import reply. _ val userFuture = actor ? LookupUser( 10 ) // userFuture: Future[Option[User]] userFuture onSuccess { case result = > { // do something with the result // result has type Option[User] } } |
Это сторона актера-пользователя. А как насчет самого актера? Как гарантировать, что если актер получит сообщение типа Replyable[T]
, он действительно ответит с помощью T
? Как обычно в Scala, ответом снова является черта, которую можно смешать в актера:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
trait ReplyingActor extends Actor { def receive = { case m : Replyable[ _ ] if receiveReplyable.isDefinedAt(m) = > { try { sender ! receiveReplyable(m) } catch { case e : Exception = > sender ! Failure(e) } } } def receiveReplyable[T] : PartialFunction[Replyable[T], T] } |
И пример использования:
1
2
3
4
5
6
|
class UserActor extends ReplyingActor { def receiveReplyable[T] = { case LookupUser(id) = > Some(User(...)) case UserCount() = > 512 } } |
Теперь все это хорошо проверено. Если бы мы попытались вернуть String
в ветке UserCount
, мы получили бы ошибку времени компиляции.