この記事は2016年11月21日に社内ブログに掲載したものの公開版となります。
ソースコードは以下のリポジトリにアップロードしています。
github.com
Flux概要
fluxのメリット
イベント発生に合わせてどんな処理をどこの部品として実装すべきかが明確
→ コードの見通しが良くなる
非同期イベントに伴うデータの流れが明確になる
→ デバッグが容易になる
各部品は疎結合かつ入出力が明確
→ 再利用性やテスト容易性を確保できる
Rubyで実装してみる
- アーキテクチャであれば言語やフレームワークに依存しないはず
- 作るもの: CUIのカウンター
- 1ファイルにまとめると以下のようなコード
- これぐらいの規模だとわざわざfluxを採用する意味はないが、学習用として目をつぶる
count = 0
print 'input: '
while op = gets.chomp
case op
when '+'
count += 1
when '-'
count -= 1
end
puts count
print 'input: '
end
リポジトリ
https://github.com/kuranari/flux-ruby
EventEmitter
- いわゆるオブザーバーパターン
- イベントを登録しておくと、特定のイベントが発火したときにコールバックが実行される
class EventEmitter
def initialize
@handlers = Hash.new { |h, k| h[k] = [] }
end
def on(type, &handler)
@handlers[type] << handler
end
def off(type)
@handlers[type].clear
end
def emit(type, data = nil)
@handlers[type].each do |handler|
handler.call(data)
end
end
end
require './event_emitter'
event_emitter = EventEmitter.new
event_emitter.on('click') do
puts 'hello world'
end
event_emitter.on('input') do |params|
puts params
end
event_emitter.emit('click')
# => hello world
event_emitter.emit('input', 42)
# => 42
Dispatcher
Action → Dispatcher → Store
ActionをStoreに配送する。
実装
EventEmitterそのものをDispatcherとして使用。
実際に使う場合は実行順序の制御機構などもう少し複雑になる。
require './event_emitter'
class Dispatcher < EventEmitter
end
Store
Dispatcher → Store → View
- MVCでいうModelに相当。
- Storeが管理するデータはStore自身のみが更新できる
- Storeの状態に変更があった場合EventEmitter経由でStoreに通知
require './event_emitter'
class Store < EventEmitter
attr_reader :count
def initialize(dispatcher)
super()
@count = 0
dispatcher.on('UPDATE_COUNTER') do |payload|
on_update_counter(payload[:value])
end
end
private
def on_update_counter(count)
@count += count
emit('CHANGE')
end
end
ActionCreator
View → Action → Dispatcher
- MVCでいうControllerに相当
Dispatcherに対しActionを作成して配信する。
Actionは
UPDATE_COUNTER
のようなアクションの識別子
{value: 1}
など入力値(パラメータ)に相当するオブジェクト
のペアで構成される。
class ActionCreator
def initialize(dispatcher)
@dispatcher = dispatcher
end
def increment_counter
@dispatcher.emit('UPDATE_COUNTER', value: 1)
end
def decrement_counter
@dispatcher.emit('UPDATE_COUNTER', value: -1)
end
end
View(Component)
Store → View → Action
- ユーザーイベントの受付
- clickされたら◯◯するといった動作
- イベントハンドラで対応するActionを呼ぶ
- Storeの変更を検知して再描画
簡易版ReactComponent
state=
で状態が変わったら再度render
する。
雑に実装すると、ReactComponentがやってることは以下のようなことだと思う。
class ReactComponent
def initialize
@state = {}
end
# stateに変更があったら再描画
def state=(state)
@state = state
render
end
def render
raise NotImplementedError.new
end
end
class Component < ReactComponent
def initialize(store, action)
@store = store
@action = action
@state = { count: @store.count }
@store.on('CHANGE') do
on_change
end
end
def increment
@action.increment_counter
end
def decrement
@action.decrement_counter
end
private
def on_change
self.state = { count: @store.count }
end
def render
puts "count: #{@state[:count]}"
end
end
Main
require './event_emitter'
require './action_creator'
require './store'
require './component'
dispatcher = EventEmitter.new
store = Store.new(dispatcher)
action = ActionCreator.new(dispatcher)
component = Component.new(store, action)
print 'input: '
while command = gets.chomp
case command
when '+'
component.increment
when '-'
component.decrement
end
print 'input: '
end
イベントの末端にスタックトレースを仕込んでみる
Component#on_change
まとめ
- Store, Action, Viewを各々EventEmitter(Dispatcher)で監視することでユーザーの入力から画面表示までの1サイクルが回ることを確認した。
- fluxを理解するには実装するのが一番早いと思う(普通にjsで実装すればいいと思うが…)
参考文献