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