Good bye Flux, welcome Bacon/Rx?の翻訳


Good bye Flux, welcome Bacon/Rx?の翻訳です。FRPを使う場合の一つの実装の仕方としてアリではないかと思います。

こういうのを見るとFRPのバズワードが一段落すると、やがて、この記事に出てくるビジネスコンポーネントのディスパッチャーの部分がプログラミング言語に隠蔽されるのでは、そんな予感すらします。オブジェクト指向とFRPの合流点が見えかけているというか、そんなイメージでしょうか。


さようなら、Flux。ようこそ、Bacon/Rx

FacebookはクライアントサイドのWebアプリケーション開発のためにFluxを約1年前に発表しました。以来、Webの開発シーンの中でもっともホットな技術のひとつになっています。

Fluxのタスクはビジネスロジックをユーザーインターフェースのロジックからディスパッチャーとストア、アクションを使うことで分離することです。中核となるアイデアは単方向のデータフローです。その意味するところは、ユーザーのインタラクションに応じてアクションはシステム全体に伝わりますが、アクションは何ら内部のデータモデルとバインディングされないということです。

はじめてFluxについて聞いた時、私は"とても良いアイデアだ"と思いました。私はまだその考えを持っています。特に単方向のデータフローがReactの仮想DOMと結びついて、アプリケーションの状態の変化について考える必要をなくし、ビューの実装を簡潔にしてくれる点については、私はまだその考えを変えていません。

React と FRP

Functional Reactive Programmming(FRP)はイベントをイベント・ストリームとしてモデルにするプログラミングのパラダイムです。イベント・ストリームは(イミュータブルな)配列のようなものです。イベント・ストリームはmap、filter、combine、mergeなどの操作がされます。(訳者注:それぞれ関数プログラミングで出てくる主要な操作)配列とイベント・ストリームの違いはイベント・ストリームの値(イベント)が非同期に発生するということです。イベントはストリームを通って伝わり、サブスクライバ(利用者)に消費されます。

その名前が意味するように、"React"ive プログラミングはReactが作られた理由です。アクションがイベント的に生じ、それがイベント・ストリームを伝わります。それらのイベント・ストリームが組み合わされ、アプリケーションの状態を作ります。イベントがシステムを通じて伝わった後、新しいアプリケーションの状態オブジェクトがサブスクライバに消費され、rootレベルのReactコンポーネントによってレンダリングされます。このことはデータフローを死ぬほど単純にします。

アプリケーションの状態を伝えるロジックは以下のコードで実装することができます。私はBacon.jsを自分のFRPライブラリとして使っていますが、例えばRxxでもなんでも良いです。

// app.js
const React   = require('react'),
      Bacon   = require('baconjs'),
      TodoApp = require('./todoApp'),
      todos   = require('./todos'),
      filter  = require('./filter')

const filterP = filter.toProperty(<initial filter>),
      itemsP  = todos.toItemsProperty(<initial items>, filterP)

const appState = Bacon.combineTemplate({
  items: itemsP,
  filter: filterP
})

appState.onValue((state) => {
  React.render(<TodoApp {...state} />,
               document.getElementById('todoapp'))
})

アクションとストアを分離する必要はない

私にはFluxのアクションとストアを分離するアイデアは理解しかねます。というのも、ビジネスの論理的な凝集性を低くしてしまうからです。幸運なことにイベント・ストリームはとても単純であり、そのためアクションとストアを分離する必要はありません。その代わりに、ただ"ビジネスコンポーネント"があるだけで良いのです。その場合の"ビジネスコンポーネント"には”ビジネスロジック”へローカルディスパッチャーを経由してやりとりをする"public API"を持っています。そんなビジネスコンポーネントがあれば良いのです。

// todos.js
const Bacon       = require('baconjs'),
      Dispatcher  = require('./dispatcher')

const d = new Dispatcher()

module.exports = {
  toItemsProperty: function(initialItems, filterP) {
    // "business logic"    
    const itemsP = Bacon.update(initialItems,
      [d.stream('remove')], removeItem,
      [d.stream('create')], createItem,
      ...
    )
    return Bacon
      .combineAsArray([itemsP, filterP])
      .map(setItemsDisplayStatusBasedOnFilter)

    function createItem(items, newItemTitle) {
      return items.concat([{<new item data>}])
    }

    function removeItem(items, itemIdToRemove) {
      return items.filter(it => it.id !== itemIdToRemove)
    }
    ...
  },

  // "public API"

  createItem: function(title) {
    d.push('create', title)
  },

  removeItem: function(itemId) {
    d.push('remove', itemId)
  },

  ... 
}

もしビジネスコンポーネントが他のコンポーネントに依存している場合、そのコンポーネントのイベントストリームはコンポーネントのイニシャライザを通じて渡すようにします。これらの依存性を明示して、循環依存性を完全に取り除きます。注意しておきましょう。もし二つのコンポーネントがお互いに依存していたら、悪い設計かコンポーネントの責務が正しくないことを意味していることでしょう。

しかし、ここで出てきた神秘的な"dispatcher"とは何なのでしょうか?。。。それは単にプッシュができる(そしてサブスクラブやコンシュームもできるような)イベント・ストリームのオブジェクトでしかありません。

// dispatcher.js
const Bacon = require('baconjs')

module.exports = function() {
  const busCache = {}  // Bus == Subject in Rx

  this.stream = function(name) {
    return bus(name)
  }
  this.push = function(name, value) {
    bus(name).push(value)
  }
  this.plug = function(name, value) {
    bus(name).plug(value)
  }

  function bus(name) {
    return busCache[name] = busCache[name] || new Bacon.Bus()
  }
}

おバカなビュー(Stupid views)

トップレベルからアプリケーションの状態がやってくるので、ビューは極めて簡単になります。ビューは単に与えられたものをレンダリングするだけです。そして、Reactの仮想DOMはその残った作業を全部やってくれます!コールバックもリスナーもありません。単にビジネスロジックの同期的にpublic APIを呼び出すだけです。

// todoItem.jsx
const React = require('react'),
      todos = require('./todos')

module.exports = React.createClass({
  render: function() {
    const item = this.props.item
    return (
      <li className={item.states.join(' ')}>
        <div className="view">
          <label>{item.name}</label>
          ...
          <button 
            className="destroy" 
            onClick={() => todos.removeItem(item.id)}
            />
        </div>
      </li>
    )
  }
})

結論

Fluxはとても素晴らしいフレームワーク/パターンですが、イベント・エミッター、リスナー、アクション、ストアの全てに過剰な決め事があり、不必要な複雑さがあります。イベント・ストリームとFunctional Reactive Programmingを使うことで同様の内容をより簡単な方法で実装できます。イベント・ストリームを使えば、undo/redoアクションのようなクールな機能も簡単に実現することができます。

例となるコードは、こちらのTodoMVCプロジェクトで見つかることでしょう。もし、Fluxを完全にやめてしまうことが怖い場合は、私の同僚の書いた、どうやってFluxをイベント・ストリームと一緒に使うかについての素晴らしい記事を一読されることをお勧めします。