FluxのRuby実装をした

この記事は2016年11月21日に社内ブログに掲載したものの公開版となります。

ソースコードは以下のリポジトリにアップロードしています。

github.com

Flux概要

  • Facebookが提唱したUI構築用のアーキテクチャ

    • 特定のライブラリや実装ではない
  • Reactとの併用が推奨されている

    • React.jsはMVCモデルのV(View)の部分の役割に特化している
  • データが単方向に流れる f:id:kuranari_tm:20171215233654p:plain

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で実装すればいいと思うが…)

参考文献