8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-14

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)
      }
    }
  }
}
8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?