シンプルなAkka Typed(Akka Actor Typed)のカウンターサンプルコード(Scalatestサンプルあり)

#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を使用する

```scala: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から分離する。
    * 大きいクラスは当然読みづらい。

```scala: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(コマンド/レスポンス)

```scala: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の呼び出し)

```scala: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)
```scala: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)
      }
    }
  }
}
```