Akka Actor Typedは何が良いのか?
・従来のAkkaの利点はほぼすべて引き継いでいる
・Actor内のmutableなコードが完全になくなる
・ClassicActorでできなかった型制約がきちんとかかっており、メッセージを含む場合も型誤りなくレスポンス可能
プロジェクトのセットアップをしよう
考慮事項
- Scala/Akkaバージョン
- Akkaは最新のバージョンを使用する
- Scalaは2.12系の最新を使用する(sbt-wartremoverとの競合を避けるため、2.13を使用しない)
- scalatest
- 最新のバージョンを使用する
- scalacticはプロダクションコードでも有用なので、"test"をつけない
- diagramsによりPOWERAssertが可能になる
- shouldmatchers/mustmatchersはマッチャの拡張が必要であれば使用する
- specの種類は何でもよい。他にテストクラスを継承していない場合は、classである~specを選択する
- 今回は他にテストクラス(ScalaTestWithActorTestKit)を継承しているので,~speclikeを使用する
build.sbt
name := "fakeactor"
version := "0.1"
scalaVersion := "2.12.13"
val ScalaTest: String = "3.2.2"
val AkkaActor: String = "2.6.12"
libraryDependencies ++= Seq(
"org.scalactic" %% "scalactic" % ScalaTest,
"org.scalatest" %% "scalatest-wordspec" % ScalaTest % "test",
"org.scalatest" %% "scalatest-diagrams" % ScalaTest % "test",
"org.scalatest" %% "scalatest-shouldmatchers" % ScalaTest % "test",
"com.typesafe.akka" %% "akka-actor-typed" % AkkaActor,
"com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaActor % Test,
)
Actor(ロジック部分)
考慮事項
- Actor/Command/Stateは、自分のパッケージ(counter)のみ可視とする。
- そうしなかった場合、同名のパッケージが複数参照可能になってしまい混乱を招く。特に、Commandの名づけを***Commandのようにしない場合に混乱を招きやすい
- Command/Stateは、極力クラスの大きさを抑えるため、可能ならばActorから分離する。
- 大きいクラスは当然読みづらい。
CounterActor.Scala
package counter.actor
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
/** 計算ロジック */
private[counter] object CounterActor {
/** 初期値 */
private val InitialCount: Int = 0
/** 初期値で初期化処理を行う */
def apply(): Behavior[Command] = changeCount(InitialCount)
/** nextCountに値を変更する */
private def changeCount(nextCount: Int): Behavior[Command] =
counter(nextCount)
/** Behavior(コマンドに対する振る舞い) */
private def counter(currentCount: Int): Behaviors.Receive[Command] =
Behaviors.receiveMessage {
case Command.Reset => changeCount(InitialCount)
case Command.Increase(value: Int) => changeCount(currentCount + value)
case Command.Decrease(value: Int) => changeCount(currentCount - value)
case Command.ReadCount(replyTo: ActorRef[Response.Count]) =>
replyTo ! Response.Count(currentCount)
Behaviors.same
}
}
Command(コマンド/レスポンス)
Command.Scala
import akka.actor.typed.ActorRef
/** Actrorへのコマンド */
sealed trait Command
private[counter] object Command {
/** 初期値で初期化処理を行う */
final case object Reset extends Command
/** カウンターの値をvalueだけ増やす */
final case class Increase(value: Int) extends Command
/** カウンターの値をvalueだけ減らす */
final case class Decrease(value: Int) extends Command
// Responseを持つコマンドはなるべく分けて定義する
/** その時点のカウンターの値を取得する */
final case class ReadCount(replyTo: ActorRef[Response.Count]) extends Command
}
/** Actrorからのメッセージ */
sealed trait Response
private[counter] object Response {
final case class Count(value: Int) extends Response
}
アプリケーション(Actorの呼び出し)
CounterApp.Scala
package counter
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorSystem, Scheduler}
import akka.util.Timeout
import counter.actor.{Command, CounterActor}
import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration.DurationInt
import scala.util.{Failure, Success}
object CounterApp {
def example(): Unit = {
val system: ActorSystem[Command] =
ActorSystem(CounterActor(), "typed-counter")
implicit val executionContext: ExecutionContextExecutor =
system.executionContext
implicit val timeout: Timeout = akka.util.Timeout(3.second)
implicit val scheduler: Scheduler = system.scheduler
system.tell(Command.Increase(255))
system.tell(Command.Increase(255))
system.ask(Command.ReadCount).map(_.value).onComplete {
case Success(result) => println(s"result:$result")
case Failure(exception) => exception.printStackTrace()
}
}
}
テスト(Scalatest)
CounterActorTest.Scala
package counter.actor
import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe}
import akka.actor.typed.ActorRef
import org.scalatest.diagrams.Diagrams
import org.scalatest.wordspec.AnyWordSpecLike
class CounterActorTest
extends ScalaTestWithActorTestKit
with AnyWordSpecLike
with Diagrams {
// ScalaTestWithActorTestKitを継承しているので、テストがすべて終わったら、ActorSystem はシャットダウンされる。
"コマンド単体テスト" when afterWord("初期値=0") {
val actor: ActorRef[Command] =
testKit.spawn(CounterActor())
val resProbe: TestProbe[Response.Count] =
testKit.createTestProbe[Response.Count]
"ReadCount" must {
"count=0" in {
actor ! Command.ReadCount(resProbe.ref)
resProbe.expectMessage(Response.Count(0))
// パターン2でもよい(複雑なアサーション向け)
// val res: Response.Count = resProbe.receiveMessage()
// assert(res.value === 0)
}
}
"Increase(n)" must {
val change: Int = 21
s"count+=$change" in {
actor ! Command.Reset
actor ! Command.Increase(change)
actor ! Command.ReadCount(resProbe.ref)
resProbe.expectMessage(Response.Count(21))
}
}
"Decrease(n)" must {
val change: Int = 21
s"count-=$change" in {
actor ! Command.Reset
actor ! Command.Decrease(change)
actor ! Command.ReadCount(resProbe.ref)
resProbe.expectMessage(Response.Count(-1 * change))
}
}
}
"サイクルテスト" when afterWord("1,000,000回加算したとき") {
val actor: ActorRef[Command] =
testKit.spawn(CounterActor())
val resProbe: TestProbe[Response.Count] =
testKit.createTestProbe[Response.Count]
"動作確認" must {
s"初期化できていること" in {
actor ! Command.Reset
actor ! Command.ReadCount(resProbe.ref)
resProbe.expectMessage(Response.Count(0))
}
}
"Increase(n)" must {
s"1,000,000回加算でき、結果も一致すること" in {
actor ! Command.Reset
actor ! Command.ReadCount(resProbe.ref)
resProbe.expectMessage(Response.Count(0))
(1 to 1000000).map(Command.Increase).foreach(actor.tell)
actor ! Command.ReadCount(resProbe.ref)
val res: Response.Count = resProbe.receiveMessage()
println(res)
assert(res.value === (1 to 1000000).sum)
}
}
}
}