Upload
tanukkii
View
1.824
Download
0
Embed Size (px)
Citation preview
すべてのアクター プログラマーが知るべき 単一責務原則とは何か
安田裕介Reactive Shinjuku meetup #2 LT
自己紹介
• Twitter: @TanUkkii007
• Akka大好き
アクターは一つの責務に特化すべき An actor must be specialized to just ONE
responsibility
http://www.slideshare.net/ktoso/zen-of-akka
ScalaMatsuri 2016 “Zen of Akka" by @ktosopl
責務が多いアクターの問題
• 責務の拡大とともに処理が複雑化する
• 責務の拡大とともに起きうる障害がついてくる
• 並列・分散して処理することが困難になる
アクターの責務が多い場合にコードに表れる兆候
1.receive関数のcase節が長い
2.メッセージが汎化しすぎている
3.1つのアクターが担う処理が多すぎる
4.親アクター直下の子アクターが多い
これらの問題点を把握し、単一責務に改善していきましょう!
1. receive関数のcase節が長い
class SequentialComputationActor(replyTo: ActorRef, settings: Settings) extends Actor with Computation1 with Computation2 with Computation3 with Computation4 with Computation5 { def receive: Receive = { case Request(x0) => { val x1 = compute1(x0, settings) val x2 = compute2(x1, settings) val x3 = compute3(x2, settings) val x4 = compute4(x3, settings) val x5 = compute5(x4, settings) replyTo ! Result(x5) } }}
例:compute1 - compute5までの計算をして結果を返す
• 長い • 今後機能を追加するたびに長くなる
問題点:1つのcase節で多くのことをしすぎている
class SequentialComputationActor(replyTo: ActorRef, settings: Settings) extends Actor with Computation1 with Computation2 with Computation3 with Computation4 with Computation5 { import SequentialComputationActorProtocol._ def receive: Receive = { case Request(x0) => self ! ComputeRequest1(x0) case ComputeRequest1(x1) => self ! ComputeResult1(compute1(x1, settings)) case ComputeResult1(x2) => self ! ComputeResult2(compute2(x2, settings)) case ComputeResult2(x3) => self ! ComputeResult3(compute3(x3, settings)) case ComputeResult3(x4) => self ! ComputeResult4(compute4(x4, settings)) case ComputeResult4(x5) => replyTo ! ComputeResult(compute5(x5, settings)) }}
解決策:自分にメッセージを投げて複数の段階で処理する
1. receive関数のcase節が長い
効果• case節が単純になり、各段階で考慮すべきスコープが狭まり保守性が向上した
• 処理を分割したことで、スレッドを専有しなくなり、その間他の処理に回すことができる
• →スループットが向上する
• →CPU使用率が向上する
• どのように実行するかDispatcherで調整可能になる (parallelism
• -**, throuput etc.)
2. メッセージが汎化しすぎているclass SequentialComputationActor extends Actor with Computation1 with Computation2 with Computation3 with Computation4 with Computation5 { def receive: Receive = { case RequestWithSettings(x0, settings) => { val x1 = compute1(x0, settings) val x2 = compute2(x1, settings) val x3 = compute3(x2, settings) val x4 = compute4(x3, settings) val x5 = compute5(x4, settings) sender() ! Result(x5) } }}
コンストラクタではなくメッセージにsettingsを乗せて、動的に変えられるよう汎用化している
sender()を使って返信先を動的に変える
• 前述のような自分にメッセージを送り複数の段階で処理することができなくなった (settingsとsender()が別のcase節では参照できないため)
• → だからcase節が長くなる • 子アクター化による集約ができない(後述)
問題:
コンストラクタではなくメッセージで依存する情報を取得するとどうだろう?
class SequentialComputationActor extends Actor with Computation1 with Computation2 with Computation3 with Computation4 with Computation5 { import SequentialComputationActorProtocol._ def receive: Receive = { case RequestWithSettings(x0, settings) => { context.become(computing(sender(), settings)) self forward ComputeRequest1(x0) } }
def computing(replyTo: ActorRef, settings: Settings): Receive = { case ComputeRequest1(x1) => self ! ComputeResult1(compute1(x1, settings)) case ComputeResult1(x2) => self ! ComputeResult2(compute2(x2, settings)) case ComputeResult2(x3) => self ! ComputeResult3(compute3(x3, settings)) case ComputeResult3(x4) => self ! ComputeResult4(compute4(x4, settings)) case ComputeResult4(x5) => replyTo ! ComputeResult(compute5(x5, settings)) }}
1. メッセージで依存する情報を取得せず、アクター初期化時に取得する 2. context.becomeで振る舞いを変更し依存する情報を固定する
解決策:
settingsと返信先のActorRefがすべてのcase節から参照可能になった
√
√
2. メッセージが汎化しすぎている
コンストラクタで初期化時に依存する情報を取得したほうが並列化できる
val sequentialComputationActor = system.actorOf(SequentialComputationActor.props())
computeRequests.foreach { request => sequentialComputationActor ! RequestWithSettings(request.x, request.settings)}
computeRequests.foreach { request => val sequentialComputationActor = system.actorOf(SequentialComputationActor.props(probe.ref, request.settings)) sequentialComputationActor ! Request(request.x)}
• 依存する情報をメッセージから動的に取得できるので1つのアクターで複数の場合を処理できる。
• ただし各メッセージを並列には処理できない • ※context.becomeは使わないほうがよい
• 依存する情報をアクターの初期化時に取得するのでメッセージごとにアクターを作る必要がある
• 各メッセージを並列に処理できる • ※処理し終えたアクターを殺すことを忘れずに
教訓• メッセージに情報を乗せて汎用的にすると様々な問題が発生する
• case節の膨張
• 処理を分割する際に振る舞いの変更を伴う
• 並列化をしにくい
• メッセージに乗せる動的な情報は絞り、特定の用途に特化させる
• アクター初期化時に依存する情報を決定し、特定の処理に特化させる
3. アクターが担う処理が多すぎる
class SequentialComputationActor(replyTo: ActorRef, settings: Settings) extends Actor with Computation1 with Computation2 with Computation3 with Computation4 with Computation5 { import SequentialComputationActorProtocol._ def receive: Receive = { case Request(x0) => self ! ComputeRequest1(x0) case ComputeRequest1(x1) => self ! ComputeResult1(compute1(x1, settings)) case ComputeResult1(x2) => self ! ComputeResult2(compute2(x2, settings)) case ComputeResult2(x3) => self ! ComputeResult3(compute3(x3, settings)) case ComputeResult3(x4) => self ! ComputeResult4(compute4(x4, settings)) case ComputeResult4(x5) => replyTo ! ComputeResult(compute5(x5, settings)) }}
• ミックスインしているtraitが多すぎる • それに起因する例外も多くなる • →ライフサイクルが複雑になる (preRestart etc.) • →SupervisorStrategyが複雑になる
問題
class Compute1Actor(replyTo: ActorRef, settings: Settings) extends Actor with Computation1 { def receive: Receive = { case ComputeRequest1(x1) => replyTo ! ComputeResult1(compute1(x1, settings)) } override def preRestart(reason: Throwable, message: Option[Any]) = { replyTo ! ComputeResult1(defaultResult) }} object Compute1Actor { def props(replyTo: ActorRef, settings: Settings): Props = Props(new Compute1Actor(replyTo, settings))}
解決策:子アクターに処理を封じ込める
3. アクターが担う処理が多すぎる
class SequentialComputationActor(replyTo: ActorRef, settings: Settings) extends Actor { import SequentialComputationActorProtocol._ val compute1Actor = context.actorOf(Compute1Actor.props(self, settings)) val compute2Actor = context.actorOf(Compute2Actor.props(self, settings)) val compute3Actor = context.actorOf(Compute3Actor.props(self, settings)) val compute4Actor = context.actorOf(Compute4Actor.props(self, settings)) val compute5Actor = context.actorOf(Compute5Actor.props(self, settings)) def receive: Receive = { case Request(x0) => compute1Actor forward ComputeRequest1(x0) case ComputeResult1(x1) => compute2Actor ! ComputeRequest2(x1) case ComputeResult2(x2) => compute3Actor ! ComputeRequest3(x2) case ComputeResult3(x3) => compute4Actor ! ComputeRequest4(x3) case ComputeResult4(x4) => compute5Actor ! ComputeRequest5(x4) case ComputeResult5(x5) => replyTo ! ComputeResult(x5) }}
• トレイトがなくなった • 親アクターは実際の処理はしない • 子アクターを集約しメッセージの制御のみを行う
3. アクターが担う処理が多すぎる
効果
• 親アクターが果たす機能は今までと変わらない
• 親アクターの責務は今までよりも縮小:メッセージの制御だけ
• 親アクターの障害は限定的:子アクターが対処できずEscaleteされるもののみ
4. 親アクター直下の子アクターが多い
class SequentialComputationActor(replyTo: ActorRef, settings: Settings) extends Actor { import SequentialComputationActorProtocol._ val compute1Actor = context.actorOf(Compute1Actor.props(self, settings)) val compute2Actor = context.actorOf(Compute2Actor.props(self, settings)) val compute3Actor = context.actorOf(Compute3Actor.props(self, settings)) val compute4Actor = context.actorOf(Compute4Actor.props(self, settings)) val compute5Actor = context.actorOf(Compute5Actor.props(self, settings)) def receive: Receive = { case Request(x0) => compute1Actor forward ComputeRequest1(x0) case ComputeResult1(x1) => compute2Actor ! ComputeRequest2(x1) case ComputeResult2(x2) => compute3Actor ! ComputeRequest3(x2) case ComputeResult3(x3) => compute4Actor ! ComputeRequest4(x3) case ComputeResult4(x4) => compute5Actor ! ComputeRequest5(x4) case ComputeResult5(x5) => replyTo ! ComputeResult(x5) }}
問題:子アクターが多いとメッセージの制御が複雑になり 親アクターのreceive関数が大きくなる
解決策:アクターヒエラルキーの階層を増やす
|-SequentialComputationActor |-Compute1Actor |-Compute2Actor |-Compute3Actor |-Compute4Actor |-Compute5Actor
|-SequentialComputationActor |-Compute123Actor |-Compute1Actor |-Compute2Actor |-Compute3Actor |-Compute4Actor |-Compute5Actor
before after
例としてcompute1,compute2,compute3に意味のある単位を見出した場合、 それらをまとめる親アクターを作る
class Compute123Actor(replyTo: ActorRef, settings: Settings) extends Actor { import Compute123ActorProtocol._ import SequentialComputationActorProtocol._ val compute1Actor = context.actorOf(Compute1Actor.props(self, settings)) val compute2Actor = context.actorOf(Compute2Actor.props(self, settings)) val compute3Actor = context.actorOf(Compute3Actor.props(self, settings)) def receive: Receive = { case Compute123Request(x0) => compute1Actor forward ComputeRequest1(x0) case ComputeResult1(x) => compute2Actor forward ComputeRequest2(x) case ComputeResult2(x) => compute3Actor forward ComputeRequest3(x) case ComputeResult3(x) => replyTo ! Compute123Result(x) }}
Compute1Actor, Compute2Actor, Compute3Actorを まとめる中間アクターをつくる
4. 親アクター直下の子アクターが多い
class SequentialComputationActor(replyTo: ActorRef, settings: Settings) extends Actor { import SequentialComputationActorProtocol._ import Compute123ActorProtocol._ val compute123Actor = context.actorOf(Compute123Actor.props(self, settings)) val compute4Actor = context.actorOf(Compute4Actor.props(self, settings)) val compute5Actor = context.actorOf(Compute5Actor.props(self, settings)) def receive: Receive = { case Request(x0) => compute123Actor forward Compute123Request(x0) case Compute123Result(x3) => compute4Actor ! ComputeRequest4(x3) case ComputeResult4(x4) => compute5Actor ! ComputeRequest5(x4) case ComputeResult5(x5) => replyTo ! ComputeResult(x5) }}
•直下の子アクターが少なくなった • receive関数が小さく単純になった
4. 親アクター直下の子アクターが多い
効果• 親は直下の子アクターの障害とメッセージを制御するだけでよい
• 直下の子の数を減らせば親の責務が減る
• receive関数を小さくできる
• 直下の子の数を減らしつつ、ヒエラルキーを深くすることで責務を減らしつつ機能は保つことができる
まとめ
アクタープログラミングの設計手法は
純粋関数型プログラミングよりも簡単!怖くない!