LoginSignup
14
15

More than 5 years have passed since last update.

Model View Streamのご提案

Last updated at Posted at 2015-05-26

Model View StreamというUIアーキテクチャーパターンのご提案。

短い結論

Model View StreamというUIアーキテクチャーパターンを考えて、やってみたら結構よかったです。

  • 良い点:新しいViewの追加が既存Viewを破壊しない
  • 悪い点:実装が面倒

背景

リアクティブプログラミングとかunidireciton data flowとか流行っていてめっちゃかっこいいです。

でもReact.jsは敷居が高いです。なんと言ってもVirtual DOMで表現できないものが扱えないのが痛いです。jsPlumb.jsという曲線コネクタを表示するライブラリがあります。svgで描画し、Drop & Dropのイベントも自前で処理します。

React.jsと既存ライブラリを擦り合わせるファサード(層)を作るのも面倒です。jsPlumb.js向けのプレゼンテーションモデルを作るのだったら、Virtual DOMと二重管理になるのが馬鹿らしいです。

React.jsを使いたいわけじゃなくてunidirection data flowをやりたいだけなので、それだけやります。ModelとViewが今まで通りでControllerがStreamになったら素敵じゃないですか。さらにStreamのAPIが比較的メジャーなAPIだったら最高ですよね。

補足

個人的なunidirection data flowの理解はunidirection data flow にしびれる、憧れる理由に書きました。何か気になる点があれば、ご教授願います。

実装方針

モジュール構成

大きく以下の三つに分かれます。

  • Model:ドメインロジック
  • View:プレゼンテーションロジック
  • Stream:アクションを処理する流れ

Streamの種類

  • ActionStream:プレゼンテーションロジックのうち入力をアクションに変換してStreamに流す
  • ModelStream:入力アクションに応じてModelを操作する。操作結果によってアクションを増やす
  • RenderStream:入力アクションに応じてViewを操作する。アクションによってはアクションを増やす

Streamのつなぎ方

ActionStreamはViewごとにあります。複数ある場合は一つのStreamへアクションを流します。
Model以降のStreamは一列につなぎます。

ActionStream1 ┐
ActionStream2 ┼ ModelStream1 - ModelStream2 - RenderStream1 - RenderStream2
ActionStream3 ┘

アクション操作のルール

次のルールを設けます。

  • 受け取ったアクションは必ず次のStreamに送る
  • 同時に、新規のアクションを次のStreamに送っても良い
  • 受け取ったアクションにプロパティを追加してもよい(変更、削除は不可)

ModelStream1とModelStream2などのStreamのインスタンスレベルでは、順序を入れ替えられるようにです。

アクションのプロパティ

以下のプロパティが必須です。

  • target:アクションが影響を与える相手
  • type:アクションの種類

{
  target: 'input-node',
  type: 'input'
}

他のプロパティは自由に追加します。

StreamのAPI

StreamのAPIはNode.jsのAPIを使います。
RxJSも検討しました。UIのイベントより一段抽象度を上げたかったのでNode.jsのAPIを採用しました。

プロトタイプ実装

実装例

デモ

Demo Site

main処理

次のようにStreamをpipeで繋ぎます。

index.js
import polyfill from 'babel/polyfill'

import actionStream from './lib/stream/actionStream'
import modelStream from './lib/stream/modelStream'
import renderStream from './lib/stream/renderStream'

actionStream
  .pipe(modelStream)
  .pipe(renderStream)

ActionStreamの例

UIのイベントハンドラーでアクションを作ってStreamにpushします。

inputNodeActionSteram.js
export default class extends ActionReadable {
  constructor(selector) {
    super()

    let component = inputNodeComponent(selector), // Viewを初期化
      container = delegate(component.component)

    container.on('click', '.button', e => onSubmit(component, 'node', 'create', this))

    let inputHandler = e => this.push({ // アクションをpush
      target: 'input-node',
      type: 'input'
    })

    container.on('input', '.label', inputHandler)
    container.on('input', '.url', inputHandler)
  }
}

ModelStreamの例

dataModelSteram.js
export default class extends ActionTransform {
  constructor(nodes, edges) {
    super()
    this._nodes = nodes // Modelへの参照を保持
    this._edges = edges
  }
  _transformAction(action, push) {
    if (action.target === 'node') { // アクションのtargetに応じてdispatch
      let nodes = this._nodes
      switch (action.type) {
        case 'create':
          createNode(nodes, action, push) // アクションのtypeに応じてmodelを操作
          break
        case 'update':
          updateNode(nodes, action, push)
          break
        case 'select':
          push(extend(action, nodes.get(action.id))) // 場合によってアクションを拡張
      }
    }

    if (action.target === 'edge') {
      handleEdge(this._edges, this._nodes, action, push)
    }
  }
}

RenderSteramの例

NodeRenderSteram
export default class extends ActionTransform {
  constructor(selector) {
    super()

    this._component = graphComponent(selector) // viewの参照を保持
  }
  _transformAction(action, push) {
    if (action.target === 'node') {
      handleNode(this._component, action, push)
    }
  }
}

function handleNode(component, action, push) {
  switch (action.type) {
    case 'create': // アクションのtypeに応じてviewを操作
      component.createNode(action.id, action.label, action.url)
      break
    case 'update':
      component.updateNode(action.id, action.label, action.url)
      break
    case 'select':
      component.selectNode(action.id)
      break
    case 'unselect':
      component.unselectNode()
  }
}

全ソースコード

graph-editor - A sample of Model View Stream

所感

  • Viewを追加したときに、既存のViewを破壊しないのが画期的
  • ES6のclass構文は便利
  • 入力バリデーションの実装が面倒。「動かしながらのUI設計」がやりづらい
  • debugしづらい。どのStreamでの操作が間違っているか特定するための地道なデバッグプリントが必要

反省と課題

  • 共通部分はライブラリが作れそう
  • アクション名は定義モジュールで一括定義すると、アクションの設計がやりやすそう
  • アクションを増やす責務を(fluxの)Dispatcherのようなものにまとめる必要ないのか?
  • Node.jsのStreamの実装をそのまま使っている。BufferingやByteArrayを扱う機能は不要
14
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15