2対のMVC (Double MVC)


[Flux](http://facebook.github.io/flux/) は [Smalltalk](https://ja.wikipedia.org/wiki/Smalltalk) の MVC を実装したフレームワークと言われています。
[MVC の流れ](http://qiita.com/hirokidaichi/items/0de5ca336de862cc91bd)では GUI で利用される Smalltalk の MVC が原点です。
また、[MVC の歴史](https://ja.wikipedia.org/wiki/Model_View_Controller)では Web で利用される サーバーサイド の MVC(Model 2) が登場します。

この GUI-MVC(クライアントサイド) と Web-MVC(サーバーサイド) の実装を個別に説明して
さらに 2つの MVC を コラボレーション するまでを紹介します。

----

## Agenda

1. GUI-MVC は Smalltalk の MVC を React と Flux(簡易) で説明
2. Web-MVC は Rails の MVC を API only apps で説明
3. GUI-MVC と Web-MVC の接続を client-server(C/S) モデル で説明
4. GUI-MVC と Web-MVC の依存を Action Cable で説明

(クラス図やコンポーネント図などの構造図は [PlantUML](http://qiita.com/ogomr/items/0b5c4de7f38fd1482a48) を利用しています。 )

----

## Smalltalk

Smalltalk の MVC は情報(Model), 表現方法(View), 制御(Controller)の3つに責務を分離する設計方針です。
また、Observer Pattern が認識される数十年前から Observer が実装されていました。

----

### Observer Pattern

[Observer Pattern](http://www.techscore.com/tech/DesignPattern/Observer.html) は *観察対象(Subject)* の状態の変化を *観察者(Observer)* に通知をする仕組みです。
*Observer Pattern* の別名は *Dependents* や *Publish-Subscribe* と呼ばれます。

![observer](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/observer.png)

```PlantUML
@startuml

class Observer {
  +update()
}

class ConcreteObserver {
  +update()
}

class Subject {
  state
  observers
  +addObserver(observer)
  +notifyObservers()
  +getState()
}

class ConcreteSubject {
  +action()
}

Observer <|.. ConcreteObserver
Subject <|-- ConcreteSubject
Observer <-o Subject

@enduml
```

----

## GUI-MVC

GUI-MVC は `C -> M -> V` と Flow が一方向です。 *Observer Pattern* で実装されています。

* Controller が Model のメッセージを実行して Model を更新します。
* Model は 状態の変更を View に通知します。
* View はモデルの状態を取得して再表示します。

![mvc1](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/mvc1.png)

```PlantUML
@startuml

package "GUI-MVC" {
  boundary View
  control Controller
  entity Model

  [Browser] ...> Controller : 1.Key Down

  Controller ...> Model : 2.Update
  View ...> Model : 4.Get Data
  View <... Model : 3.Notify

  [Browser] <... View : 5.Display
}

@enduml
```

----

### CounterView

これから [Redux のサンプル](https://github.com/reactjs/redux/tree/master/examples/counter)にもあるカウンターを作成します。
ユーザの入力で数値の増加や減少を表示するシンプルなアプリケーションです。

CounterView は Smalltalk の MVC を擬似的に JavaScript で実装しました。
Smalltalk の後継の Squeak の[サンプルコード](https://www.ogis-ri.co.jp/otc/hiroba/technical/Squeak5/index.html)を参考にしています。

----

#### Example

* [Frontend Boilerplate](https://github.com/tj/frontend-boilerplate) を利用するか個別にライブラリをインストールをして環境を作成します。

```bash
$ mkdir react-counter-view-example
$ cd react-counter-view-example
$ npm init --force
$ npm install webpack webpack-dev-server --save-dev
$ npm install babel-core babel-loader --save-dev
$ npm install babel-preset-react babel-preset-es2015 --save-dev
$ npm install react react-hot-loader react-dom --save-dev
```

```bash
$ tree
.
├── app
│   ├── controllers
│   │   └── CounterKeyboardController.js
│   ├── models
│   │   ├── Counter.js
│   │   └── Subject.js
│   ├── views
│   │   └── CounterTextView.js
│   └── index.js
├── .babelrc
├── index.html
├── package.json
└── webpack.config.js
```

----

#### Model

##### Subject

* Subject クラスには状態と Observer のリストが保持されます。
* Observer を追加するメソッドと通知するメソッドが実装されます。
* 状態を取得するメソッドが実装されます。

(簡易な Flux ですが [Redux のコード](https://github.com/reactjs/redux/blob/master/src/createStore.js)を参考にしています。)

```app/models/Subject.js
class Subject {
  constructor() {
    this.state = undefined
    this.observers = []
  }

  addObserver(observer) {
    this.observers.push(observer)
  }

  notifyObservers() {
    this.observers.forEach(observer => observer())
  }

  getState() {
    return this.state
  }
}

export default Subject
```

----

##### ConcreteSubject

* Subject クラスを継承して、ConcreteSubject クラスを作成します。
* Counter クラスに Model の状態を変更する action メソッドが実装されます。

```app/models/Counter.js
import Subject from './Subject'

class Counter extends Subject {
  constructor() {
    super()
    this.state = 0
  }

  action(type) {
    switch (type) {
    case 'increase':
      this.state += 1
      break
    case 'decrease':
      this.state -= 1
      break
    }

    this.notifyObservers()
  }
}

export default Counter
```

----

#### Controller

* キーの入力と Model のアクションを設定します。
* u(up)のキーで増加して、d(down)のキーで減少する仕様です。

```app/controllers/CounterKeyboardController.js
export default function CounterKeyboardController(model) {
  return {
    u: () => model.action('increase'),
    d: () => model.action('decrease')
  }
}
```

----

#### View

* React の Component クラスを継承して、ConcreteObserver クラスを作成します。
* componentDidMount メソッドで、キーの入力と Controller を設定します。
* render メソッドで、表示をする要素の構成を設定します。

```app/views/CounterTextView.js
import React, { Component } from 'react'

class CounterTextView extends Component {
  componentDidMount() {
    window.document.addEventListener(
      'keydown',
      this.handleKeyDown.bind(this)
    )
  }

  handleKeyDown(e) {
    this.props.controller[e.key]()
  }

  render() {
    return (
      <div>
        <h1>Counter</h1>
        <p>Key Downed: {this.props.count} times</p>
        <div>Increase in the key of u (up)</div>
        <div>Decrease in the key of d (down)</div>
      </div>
    )
  }
}

export default CounterTextView
```

----

#### Container

* MVC を結びつけます。Model を Controller に設定します。
* View に Controller と Model の状態の取得を設定します。
* View を Observer に追加します。

```app/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Counter from './models/Counter'
import CounterTextView from './views/CounterTextView'
import CounterKeyboardController from './controllers/CounterKeyboardController'

const model = new Counter()
const controller = CounterKeyboardController(model)

function render() {
  ReactDOM.render(
    <CounterTextView
      controller={controller}
      count={model.getState()}
    />,
    document.getElementById('root')
  )
}

render()
model.addObserver(render)
```

----

#### Demo

![counter_view](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/counter_view.png)

サンプルで分かるように MVC は Model, View, Controller が疎結合になり、責務の分離が実現できます。

----

## Web-MVC

Web の MVC も情報(Model), 表現方法(View), 制御(Controller)の3つに責務を分離する設計方針です。
しかし、GUI と Web はアプリケーションのドメインが違うので MVC の役割は同じではありません。
もちろん Model と View に *Observer Pattern* は実装されていません。

Web-MVC は `C -> M -> C -> V -> C` と Flow に Controller が何度も登場します。

* Controller が Request を受けて Model を操作してデータを取得します。
* Controller が View に Model のデータをセットして、コンテンツを作成します。
* Controller が Response を返します。

![mvc2](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/mvc2.png)

```PlantUML
@startuml

package "Web-MVC" {
  boundary View
  control Controller
  entity Model

  [Browser] <... Controller : 6.Response
  [Browser] ...> Controller : 1.Request

  Controller ...> Model : 2.Get Data
  Controller ...> Model : 3.Set Data

  Controller <... View : 5.Render
  Controller ...> View : 4.Set Data
}

@enduml
```

----

### CounterViewApi

CounterViewApi は Ruby on Rails の [API only apps](http://edgeguides.rubyonrails.org/5_0_release_notes.html#api-applications) を利用して実装しました。
CounterView と同じくカウンターのシンプルなアプリケーションです。

* `rails new` コマンドに `--api` オプションで API だけの Web アプリケーションが作成できます。
* `rails generate scaffold` コマンドで counter の MVC が作成できます。

----

#### Example

```bash
$ rails new counter-view-api --api
$ mv counter-view-api rails-counter-view-example
$ cd rails-counter-view-example
$ edit Gemfile
$ rails generate scaffold counter state:integer
```

```bash
$ tree
.
├── app
│   ├── controllers
│   │   ├── application_controller.rb
│   │   └── counters_controller.rb
│   ├── models
│   │   ├── application_record.rb
│   │   └── counter.rb
│   └── views
│       └── counters
│             └── show.json.jbuilder
├── bin
├── config
├── db
├── lib
├── public
├── config.ru
├── Gemfile
└── Rakefile
```

----

#### Model

* ApplicationRecord クラスを継承して、Counter クラスを作成します。
* カンターの状態を保持し、増加と減少のメソッドを実装します。

```app/models/counter.rb
class Counter < ApplicationRecord
  def initialize
    super
    self.state = 0
  end

  def increase
    self.state = self.state + 1
  end

  def decrease
    self.state = self.state - 1
  end
end
```

----

#### Controller

* ApplicationController クラスを継承して、CounterController クラスを作成します。
* カンターの状態を表示するメソッドと増加と減少のメソッドを実装します。

(Rails は Controller のメソッドをアクションと呼びます。)

```app/controllers/counters_controller.rb
class CountersController < ApplicationController
  before_action :set_counter

  def show
  end

  def create
    @counter.increase
    @counter.save!
  end

  def destroy
    @counter.decrease
    @counter.save!
  end

  private
    def set_counter
      @counter = Counter.last || Counter.new
    end
end
```

----

#### View

* Model のデータを View のテンプレートに設定します。

```app/views/counters/show.json.jbuilder
json.extract! @counter, :state
```

----

#### Router

* URL の ルーティング を設定します。
* コントローラーとアクションを結びつけます。

```config/routes.rb
Rails.application.routes.draw do
  resource :counter, only: [:show, :create, :destroy]
end
```

----

#### Demo

* カウンターを表示する `curl` のコマンドです。

```bash
$ curl -X GET http://localhost:3000/counter
{"state":8}
```

* カウンターを増加するコマンドです。

```bash
$ curl -X POST http://localhost:3000/counter
```

* カウンターを減少するコマンドです。

```bash
$ curl -X DELETE http://localhost:3000/counter
```

REST API なので エンドポイントは同じでも、メソッドで振る舞いが変わります。

----

## GUI-MVCとWeb-MVCの接続

Web も Web-browser と Web-server の client-server(C/S) なのですが
GUI-MVC(client) と Web-MVC(server) を結びつけてデータを連携します。

C/S は プレゼンテーション層(ユーザインターフェース)、アプリケーション層(ビジネスロジック)、
データ層(データベース) で3階層モデルと呼ばれます。

![cs](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/cs.png)

```PlantUML
@startuml

package "GUI-MVC" {
  boundary View
  control Controller
  entity Model

  [Browser] ...> Controller : 1.Key Down

  Controller ...> Model : 2.Update
  View ...> Model : 10.Get Data
  View <... Model : 9.Notify

  [Browser] <... View : 11.Display
}

package "Web-MVC" {
  boundary View2
  control Controller2
  entity Model2

  Model <.. Controller2 : 8.Response
  Model ..> Controller2 : 3.Request

  Controller2 ...> Model2 : 5.Set Data
  Controller2 ...> Model2 : 4.Get Data

  Controller2 <... View2 : 7.Render
  Controller2 ...> View2 : 6.Set Data
}

@enduml
```

----

### GUI-MVC(client)

* action メソッドに [fetch](https://github.com/matthew-andrews/isomorphic-fetch) で CounterViewApi と結びつけます。

----

#### Model

```app/models/Counter.js
import Subject from './Subject'
import fetch from 'isomorphic-fetch'

const endpoint = 'http://localhost:3000/counter'

class Counter extends Subject {
  constructor() {
    super()
    this.state = 0
  }

  action(type) {
    switch (type) {
    case 'show':
      fetch(endpoint, {headers: {accept: 'application/json'}})
        .then(response => response.json())
        .then(json => {
          this.state = json.state
          this.notifyObservers()
        })
      break
    case 'increase':
      fetch(endpoint, {method: 'post'})
        .then(response => this.action('show'))
      break
    case 'decrease':
      fetch(endpoint, {method: 'delete'})
        .then(response => this.action('show'))
      break
    }
  }
}

export default Counter
```

----

#### Container

* View を Observer に追加した後に、表示のアクションを実施します。

```app/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Counter from './models/Counter'
import CounterTextView from './views/CounterTextView'
import CounterKeyboardController from './controllers/CounterKeyboardController'

const model = new Counter()
const controller = CounterKeyboardController(model)

function render() {
  ReactDOM.render(
    <CounterTextView
      controller={controller}
      count={model.getState()}
    />,
    document.getElementById('root')
  )
}

render()
model.addObserver(render)
model.action('show')
```

----

### Web-MVC(server)

* client が `No Access-Control-Allow-Origin` を表示するので CORS に対応します。
* Rails 5 から `gem 'rack-cors'` が標準で設定されているので CORS の対応が簡単です。

```config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

----

### Demo

![counter_view](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/counter_view.png)

[ドメイン駆動設計](http://www.shoeisha.co.jp/book/detail/9784798121963) では *レイヤアーキテクチャ* で責務に応じた4つの層が説明されています。
そのプレゼンテーション層とアプリケーション層で MVC を利用することができました。

----

## GUI-MVCとWeb-MVCの依存

client-server(C/S) にも Observer Pattern を実装することができます。
GUI-MVC(client) と Web-MVC(server) を依存させて状態変更を通知します。

![pubsub](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/pubsub.png)

```PlantUML
@startuml

package "GUI-MVC" {
  boundary View
  control Controller
  entity Model

  [Browser] ...> Controller : 1.Key Down

  Controller ...> Model : 2.Update
  View ...> Model : 11.Get Data
  View <... Model : 10.Notify

  [Browser] <... View : 12.Display
}

package "Web-MVC" {
  boundary View2
  control Controller2
  entity Model2

  Model <.. Controller2 : 8.Response
  Model <.. Controller2 : 9.Notify
  Model ..> Controller2 : 3.Request

  Controller2 ...> Model2 : 5.Set Data
  Controller2 ...> Model2 : 4.Get Data

  Controller2 <... View2 : 7.Render
  Controller2 ...> View2 : 6.Set Data
}

@enduml
```

----

### GUI-MVC(subscribe)

* [ActionCable(WebSocket)](https://www.npmjs.com/package/actioncable) で CounterViewApi と結びつけます。
* Subscribe を追加するメソッドが実装されます。

----

#### Model

```app/models/Counter.js
import Subject from './Subject'
import fetch from 'isomorphic-fetch'
import ActionCable from 'actioncable'

const endpoint = 'http://localhost:3000/counter'
const cable = ActionCable.createConsumer('ws://localhost:3000/cable')

class Counter extends Subject {
  constructor() {
    super()
    this.state = 0
  }

  action(type) {
    switch (type) {
    case 'show':
      fetch(endpoint, {headers: {accept: 'application/json'}})
        .then(response => response.json())
        .then(json => {
          this.state = json.state
          this.notifyObservers()
        })
      break
    case 'increase':
      fetch(endpoint, {method: 'post'})
        .then(response => this.action('show'))
      break
    case 'decrease':
      fetch(endpoint, {method: 'delete'})
        .then(response => this.action('show'))
      break
    }
  }

  addSubscribe(channel) {
    cable.subscriptions.create(
      channel,
      {
        connected() {
          this.handleShow()
        },
        received() {
          this.handleShow()
        },
        handleShow: () => this.action('show')
      }
    )
  }
}

export default Counter
```

----

#### Container

* View を Observer に追加した後に Subscribe を追加します。

```app/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Counter from './models/Counter'
import CounterTextView from './views/CounterTextView'
import CounterKeyboardController from './controllers/CounterKeyboardController'

const model = new Counter()
const controller = CounterKeyboardController(model)

function render() {
  ReactDOM.render(
    <CounterTextView
      controller={controller}
      count={model.getState()}
    />,
    document.getElementById('root')
  )
}

render()
model.addObserver(render)
model.addSubscribe('CounterChannel')
```

----

### Web-MVC(publish)

* Rails [Action Cable(WebSocket)](http://railsguides.jp/action_cable_overview.html) で CounterView と結びつけます。
* `rails generate channel` コマンドで counter の ActionCable が作成できます。

----

#### Example

```bash
$ rails generate channel counter
```

```bash
$ tree
.
├── app
│   ├── channels
│   │   ├── application_cable
│   │   │   ├── channel.rb
│   │   │   └── connection.rb
│   │   └── counter_channel.rb
│   ├── controllers
│   ├── models
│   └── views
```

----

#### Channel

* `subscribed` に `counter` を設定します。

```app/channels/counter_channel.rb
class CounterChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'counter'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
```

----

#### Controller

* 状態を変更した後に `ActionCable.server.broadcast` で `counter` を設定します。

```app/controllers/counters_controller.rb
class CountersController < ApplicationController
  before_action :set_counter

  def show
  end

  def create
    @counter.increase
    @counter.save!
    ActionCable.server.broadcast 'counter', nil
  end

  def destroy
    @counter.decrease
    @counter.save!
    ActionCable.server.broadcast 'counter', nil
  end

  private
    def set_counter
      @counter = Counter.last || Counter.new
    end
end
```

* 接続を許可する エンドポイント を設定します。

```config/application.rb
module CounterView
  class Application < Rails::Application
    config.api_only = true
    config.action_cable.allowed_request_origins = ['http://localhost:4000']
  end
end
```

----

#### Demo

![counter_view](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/counter_view.png)

![counter_view](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/counter_view.png)

[実践ドメイン駆動設計](http://www.shoeisha.co.jp/book/detail/9784798131610) では *ドメインイベント* で出版-購読型モデルが説明されています。
メッセージングミドルウェアには RabbitMQ などの AMQP を利用することを勧められています。

----

## Async

* Rails は [Active Job](http://railsguides.jp/active_job_basics.html) を利用すると Publish を非同期にすることができます。
* `rails generate channel` コマンドで counter の ActionCable が作成できます。

----

### Example

```bash
$ rails generate job counter
```

```bash
$ tree
.
├── app
│   ├── channels
│   ├── controllers
│   ├── jobs
│   │   ├── application_job.rb
│   │   └── counter_job.rb
│   ├── models
│   └── views
```

----

#### Job

* `ActionCable.server.broadcast` の `counter` を設定します。

```app/jobs/counter_job.rb
class CounterJob < ApplicationJob
  queue_as :default

  def perform(*args)
    ActionCable.server.broadcast 'counter', nil
  end
end
```

----

#### Controller

* 状態を変更した後に `CounterJob.perform_later` を設定します。

```app/controllers/counters_controller.rb
class CountersController < ApplicationController
  before_action :set_counter

  def show
  end

  def create
    @counter.increase
    @counter.save!
    CounterJob.perform_later
  end

  def destroy
    @counter.decrease
    @counter.save!
    CounterJob.perform_later
  end

  private
    def set_counter
      @counter = Counter.last || Counter.new
    end
end
```

----

### Demo

![counter_view](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/counter_view.png)

Active Job は ミドルウェアに Redis を利用することができます。

----

## Redux

Redux は Flux の流れなので最初に説明をした GUI-MVC の影響を受けています。
さらに Redux は [CQRS](http://www.minato.tv/cqrs/cqrs_documents_jp.pdf) や [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) の影響を受けて [三原則](http://redux.js.org/docs/introduction/ThreePrinciples.html) に反映されています。

![redux](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/framework/dmvc/redux.png)

```PlantUML
@startuml

package "Redux" {
  boundary View
  entity Store
  control Action
  control Reducer

  [Browser] ..> Action

  Action ..> Store
  Store <.. Reducer
  Store ..> Reducer
  View <.. Store

  [Browser] <.. View
}

@enduml
```

----

*アプリケーションソフトウェア* は要求によって複雑になります。
その複雑には様々なアーキテクチャを駆使して立ち向かいましょう。