22
すべてのアクター プログラマーが知るべき 単一責務原則とは何か 安田裕介 Reactive Shinjuku meetup #2 LT

すべてのアクター プログラマーが知るべき 単一責務原則とは何か

Embed Size (px)

Citation preview

Page 1: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

すべてのアクター プログラマーが知るべき 単一責務原則とは何か

安田裕介Reactive Shinjuku meetup #2 LT

Page 2: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

自己紹介

• Twitter: @TanUkkii007

• Akka大好き

Page 3: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

アクターは一つの責務に特化すべき An actor must be specialized to just ONE

responsibility

http://www.slideshare.net/ktoso/zen-of-akka

ScalaMatsuri 2016 “Zen of Akka" by @ktosopl

Page 4: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

責務が多いアクターの問題

• 責務の拡大とともに処理が複雑化する

• 責務の拡大とともに起きうる障害がついてくる

• 並列・分散して処理することが困難になる

Page 5: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

アクターの責務が多い場合にコードに表れる兆候

1.receive関数のcase節が長い

2.メッセージが汎化しすぎている

3.1つのアクターが担う処理が多すぎる

4.親アクター直下の子アクターが多い

これらの問題点を把握し、単一責務に改善していきましょう!

Page 6: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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節で多くのことをしすぎている

Page 7: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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節が長い

Page 8: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

効果• case節が単純になり、各段階で考慮すべきスコープが狭まり保守性が向上した

• 処理を分割したことで、スレッドを専有しなくなり、その間他の処理に回すことができる

• →スループットが向上する

• →CPU使用率が向上する

• どのように実行するかDispatcherで調整可能になる (parallelism

• -**, throuput etc.)

Page 9: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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節が長くなる • 子アクター化による集約ができない(後述)

問題:

コンストラクタではなくメッセージで依存する情報を取得するとどうだろう?

Page 10: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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. メッセージが汎化しすぎている

Page 11: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

コンストラクタで初期化時に依存する情報を取得したほうが並列化できる

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は使わないほうがよい

• 依存する情報をアクターの初期化時に取得するのでメッセージごとにアクターを作る必要がある

• 各メッセージを並列に処理できる • ※処理し終えたアクターを殺すことを忘れずに

Page 12: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

教訓• メッセージに情報を乗せて汎用的にすると様々な問題が発生する

• case節の膨張

• 処理を分割する際に振る舞いの変更を伴う

• 並列化をしにくい

• メッセージに乗せる動的な情報は絞り、特定の用途に特化させる

• アクター初期化時に依存する情報を決定し、特定の処理に特化させる

Page 13: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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が複雑になる

問題

Page 14: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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. アクターが担う処理が多すぎる

Page 15: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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. アクターが担う処理が多すぎる

Page 16: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

効果

• 親アクターが果たす機能は今までと変わらない

• 親アクターの責務は今までよりも縮小:メッセージの制御だけ

• 親アクターの障害は限定的:子アクターが対処できずEscaleteされるもののみ

Page 17: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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関数が大きくなる

Page 18: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

解決策:アクターヒエラルキーの階層を増やす

|-SequentialComputationActor |-Compute1Actor |-Compute2Actor |-Compute3Actor |-Compute4Actor |-Compute5Actor

|-SequentialComputationActor |-Compute123Actor |-Compute1Actor |-Compute2Actor |-Compute3Actor |-Compute4Actor |-Compute5Actor

before after

例としてcompute1,compute2,compute3に意味のある単位を見出した場合、 それらをまとめる親アクターを作る

Page 19: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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. 親アクター直下の子アクターが多い

Page 20: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

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. 親アクター直下の子アクターが多い

Page 21: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

効果• 親は直下の子アクターの障害とメッセージを制御するだけでよい

• 直下の子の数を減らせば親の責務が減る

• receive関数を小さくできる

• 直下の子の数を減らしつつ、ヒエラルキーを深くすることで責務を減らしつつ機能は保つことができる

Page 22: すべてのアクター プログラマーが知るべき 単一責務原則とは何か

まとめ

アクタープログラミングの設計手法は

純粋関数型プログラミングよりも簡単!怖くない!