天狗会議録
Posts Pages About
MVCからReduxまで
2025/08/29

はじめに

本記事はGUIアーキテクチャについて学習というか調査した記事になります。

私はこの数年間、ゲームやツールの設計をどのようにすべきか悩んで来ました。 自力で試行錯誤したり、ネットや本を調べたりしました。 しかし、自力の試行錯誤は正誤を判定できず、 「設計」と言うとDDDだのクリーンアーキテクチャだのWebサービスに特化したようなものばかりで溢れ、 「オブジェクト設計」と言うとオブジェクト指向だのデザインパターンだのミクロなテクニックばかりで溢れ、 「モジュール分割」と言うと各言語ごとの仕様ばかりで溢れ、 「アーキテクチャ」と言うと技術選定だのメンタルモデルだの求めているものでない情報ばかりで溢れ、 参考として信頼できそうなものは皆大規模なソフトウェアなのですから、参考になりませんでした。

当然の話です。 今更Windowsアプリを書いている奴なんて絶滅危惧種です。 フルスクラッチでゲームを作るなんて少数派です。 きっと、今は溢れかえっているWebサービスだって、強いフレームワークによって淘汰され、そのフレームワークの使い方で情報が溢れ返ります。 私が普段作っているアプリケーションは、既存のプログラムを組み合わせるだけで、あっと言う間に作れる程度のものなのです。 そんなもののための情報なんて、あるはずがありません。

そんなこんなで我が道を進んでいましたが、突然、少し考えが変わりました。 私は今年の2月頃から内定先の企業でiOSアプリ開発に携わっています。 そのアプリはTCAに基づいています。 ある程度TCAを理解していれば、そのアプリの開発には食いついていけます。 しかし、それで良いのか? そんな甘い理解で済ませて良いのか?  今更ながらそう思うようになり、GUIアーキテクチャを学ぶことにしました。 あわよくば、GUIアーキテクチャをゲームにも応用できるかもしれない、と思いながら。

この学習は当初一日や二日で終わる想定でした。 GUIアーキテクチャに関する情報は数多くネット上に存在し、すぐに理解できるものだと見積もっていたからです。 残念ながら、二日で終わることはありませんでした。 ご存知の通り、それら情報は皆言っていることがバラバラに交錯していたからです。

これは自分なりにでも良いから、自分のためだけでも良いから、情報を整理しないといけない。 そんなモチベーションがあって、本記事を書くことにしました。 つまり、私はフロントエンドのプロフェッショナルでも・アマチュアですらもありません。 しかし、一応根気を詰めて調べたつもりです。 もし、私のことを能く知らない人がこの記事を読む場合は、そのことをご容赦ください。

本記事では参考文献を[1][2]で、参考文献を提示できないがどう考えてもそうであることを[AFAIK]で、間違っているかもしれないが個人的に考えられることを[IMO]で示します。 原則、[*]のスコープは段落の始まりからそれが現れるまでのすべて文章とします。

MVC

概要

プレゼンテーションとドメインを分離するオブジェクト指向アーキテクチャ。

歴史が長いためか、環境の変化や・勘違いの積重ねによって、多様になっている。 何より、どんなに調べても知りたいことに対する正しいと確信できる情報が出て来ない。

MVCは多様である。 しかし、後述する理由によって、次の三種類に大別されると考えられる[IMO]。

Trygve ReenskaugのMVC

TrygveはMVCの創始者である(と言って良いだろう)。 TrygveのMVCはOriginal MVCと言われたりもする。 Trygveは当時のUIアーキテクチャを発展させてThing-Model-View-Editorというアーキテクチャを作った[1][3]。 ここでは次の四つの要素が導入されている[1]:

ただし、Thingはユーザの関心のあるものを意味する。 例えば大きな橋の建設や論文のアイデア等なんでも良い[1]。

ここで注目すべきはまだObserverパターンが使われていない点である。 そのため、ViewがModelとやりとりを行っている。 つまり、ユーザ→Controller→View→Model→Viewという流れを持つ[IMO]。

また、注意すべきは視覚的表現の責務はあくまでViewが負うのであり、Controllerではない点である。 つまり、ViewのライフサイクルはViewが管理する。 また、Controllerは視覚的表現について無知であるため、例えば、Viewを直接「この色になれ」「ここに線を引け」のように操作することは許されていない。 が、一方で、ControllerはViewを正しく配置する責務を負う[1]。 どうやら、ウィンドウのどこにViewがあるかはModelの視覚的表現ではないから、Controllerが担当するらしい[IMO]。……?

同様に、ユーザの入力についてViewは無知でなければならない。 「SPACEキーが押された」というメソッドを生やし、Controllerから呼ぶようなことは許されない[1]。

他に注意すべき点として、ViewとControllerが一対一でないことが挙げられる。 現代のMVCではしばしばViewとControllerが一対一で作成される[IMO]。

Smalltalk-80のMVC

TrygveがいないところでTrygveのMVCからSmalltalk-80のMVCが派生した[1]。 TrygveのMVCから次の点が異なっている[2]:

つまり、次の流れを持つことになる。

  1. ユーザが入力
  2. Controllerが入力を検知
  3. Controllerが意味のあるメッセージに変換
  4. ControllerがModelにメッセージを送信
  5. Modelがデータを変更
  6. ModelがViewに更新を通知
  7. ViewがModelのデータ取得のメッセージを送信
  8. ViewがModelのデータに基づいて視覚情報を更新

MVパターンのMVC

時代が経て、ウィジェット(ボタンやテキストフィールドといった単体UI)がユーザの入力を検知できるようになると、 実質的にControllerがViewに内蔵され、MVパターンとも呼ぶべき状態になった[4]。 注意深く思い返せば、上の2種類のMVCではユーザの入力はControllerによって監視されていた。 これは現代のGUI環境では考えにくいことである。 .NETであれSwingであれSwiftUIであれWebであれ、ウィジェットのオブジェクトは標準でイベントハンドラを持っている。 逆に言えば、かつてはこのイベントハンドラをControllerが担っていた。

この環境ではControllerは不要である。 しかし、MVCが有名であるせいか、ViewがModelを直接更新するのを嫌ってか、 わざわざControllerを定義し、Viewからの入力情報をControllerに転送するようになった。 これが現代のMVCである[IMO]。

ちなみに、現代でもゲームや一部のアプリはネイティブのウィジェットを使わないため、ユーザ入力を直接監視することはできる。 尚、次節に共通する話として、MVCはゲームに向かないとも言われている[5]。

MVCの弱点

Martin FowlerはSmalltalk-80のMVCの弱点として次を挙げている[3]:

  1. Observerパターンによって何が起こっているかどうなって動いているのかわかりづらくなる
  2. 視覚的表現の「状態」をどこに配置すべきかわかりづらい

1.は仕方のないものとしてスキップする。

2.の問題について。 例えば、あるテキストフィールドに入力された文字列によってそのテキストフィールド及びすぐ横のラベルの文字色を変えたいとする。 ViewはModelの視覚的表現である。 だとすれば、Modelに「どんな色になるべきか」を持ち、Viewがそれを反映するのが正解である。 ここで、Modelは視覚的表現について無知でなければならないから、文字列の評価を現代でいうenum型で持ち、 それと色との対応マップを持ったテキストフィールド・あるいはラベル・あるいはそれらの複合コンポーネントのサブクラスを定義するのが良い[4]。

しかし、例えば、あるリストボックスとその選択されたアイテムの詳細が別ウィンドウにあるとする。 ここで必要となる「どのアイテムが選択されているか」という「状態」をどこに配置するかが問題となる。 「どのアイテムが選択されているか」という情報は視覚的表現のために必要なドメイン情報ではない。 かといって、ウィンドウが別れているために、前述のようにサブクラスを定義して解決することもできない[4]。

つまり、視覚的表現の比重が大きいようなアプリケーションにMVCは適応しづらい。

参考文献

  1. MVC (Trygve Reenskaug)
  2. A Cookbook for Using the Model-View-Controller User Interface Paradigm in Smalltalk-80 (Glenn E. Krasner, Stephen T. Pope)
  3. GUI Architectures (Martin Fowler)

理解に役立った文献

  1. MVPVM Design Pattern - The Model-View-Presenter-ViewModel Design Pattern for WPF (Bill Kratochvil)
  2. MVCパターンの適用限界を考える(5) (ynomura)

MVP

概要

MVCの派生アーキテクチャ。 MVCが勘違いされる要因となったControllerが欠落した環境でのアーキテクチャである。

MVC同様歴史が長く、多様である。 ここでは次のMVPを正しいものとして扱う:

Dolphin SmalltalkによるMVP

MVPはMike Potelによって考案され、これに感化されたDolphin Smalltalkによって発展した[1]。 PotelのMVPでは更新するデータの範囲を表現するSelectionsやどのようにデータを変更するかを表現するCommandsによってModelを操作するよう主張しているが、 Dolphin Smalltalkはそのような機構を持っていない[1][2]。

MVPはそれぞれ次のようである[3]:

次のような流れを持つ:

なぜこのような仕様になっているかを理解するには、「MVC」への不満を理解する必要がある。 前述した通り、MVCには視覚的表現の比重が大きくなると厄介事が起こるという弱点がある。 例えば、リストボックスとその選択されたアイテムの詳細を表示する場合、どこに「どのアイテムが選択されているか」を配置するかわからない。 他にも、例えば、あるテキストフィールドに無効な文字列が入力されているときに限り保存ボタンを無効化したい場合、どこに「文字列のヴァリデーション」を配置するかわからない。 アニメーションをしたい場合も同様に、どこに状態や処理を配置するかわからない。 そこで先人VisualWorksの開発者らはApplication Modelを導入した。 これは視覚的表現にまつわる状態や処理が配置されるオブジェクトであり、MとVPの間に配置され、両者を仲介する。 以下のような流れで問題を解決する仕組みであった[1]:

  1. ユーザ入力→Controller→Application Modelと伝達される
  2. Application Modelで何らかの処理を行う
  3. Application ModelがProperty Objectを変更する
  4. Property ObjectがViewに変更を通知する
  5. ViewがProperty Objectからデータを取得する
  6. Viewが見た目を更新する

Dolphin Smalltalkの開発者らはこの段階のMVCを「MVC」と称して、次の不満を語っている[1][3]:

そのため、Viewと強く連結して動作するPresenterを導入し、Controllerを落としたのである。

Martin FowlerのMVP

Martin Fowlerはテストを容易にするために、Pasive ViewとSupervising Controllerの2種類のMVPを定義している[1]。 それぞれ、次の特徴を持つ:

本来Viewの責務であった操作をPresenterに委譲することで、Viewをテストダブルに差し替え、UIのテストを行えるようになるのである。

尚、両者の大きな違いとして、Passive ViewではViewがModelについて無知であるという点がある。

参考文献

  1. GUI Architectures (Martin Fowler)
  2. MVP: Model-View-Presenter The Taligent Programming Model for C++ and Java (Mike Potel)
  3. TWISTING THE TRIAD (Andy Bower, Blair McGlashan)

MVVM

概要

John Gossmanによって考案されたアーキテクチャ。 Martin FowlerのPresentation Modelから派生した[8]。 このPresentation ModelはMVCからの派生であるため、厳密にはMVVMはMVCの派生である[2]。

Presentation Model

Presentation Modelは視覚的表現の状態や処理をPresentation Modelという視覚的表現に関するModelに配置するアーキテクチャである。 Model、View、Presentation Modelの三つ組で構築する[1]。

ここで注意すべき点はPresentation ModelがModelである点である。 あくまでModelなので、Viewが主体的に視覚的表現に関する操作を行う。 そして、Viewは真のModelについて無知である[1]。 この点でMVPのアプローチとは異なる[IMO]。

また、Presentation ModelはObserverパターンではなく手動でViewとPresentation Modelを同期する。 この点でVisualWorksのApplication Modelとは異なる[IMO]。

[1]にはViewを超えたPresentation Modelの同期について記されていない。 例えば、あるボタンを押すと色が変わるラベルがあるとして、ボタン押下→Presentation Model更新→View更新→ラベル色更新の流れで実装できる。 しかし、このラベルが別のView (つまり、別フォームや別ウィンドウ)に配置されているとき、どのように更新するかわからない。 ViewがそのViewのインスタンスを参照し、更新(同期)メソッドを呼ぶことで愚直に解決できるが、私の経験上それはスパゲッティ化する。

WPFのMVVM

MVVMはWPFの文脈で誕生した。 WPFでは外観を非プログラマブルな言語であるXAMLで宣言的に記述するという特徴を持つ。 このXAMLへViewModelという視覚的表現のモデルをデータバインディングすることでアプリケーションを駆動する[2]。

Presentetion Modelとの違いは、WPFのシステムがViewとViewModelの同期を行う点である。 Presentation ModelではViewが主体となってPresentation Model上のデータを参照しView自身を更新する。 現代でも.NETで行われる手法である。 一方WPFではXAMLへのView ModelのデータバインディングがWPFによって自動で行われる。 つまり、ViewとViewModelは互いに無知である[2]。

このようにすることで、Model・View・ViewModelの分離を促進するため、視覚的表現に関するテストがしやすくなる。 また、ViewがXAMLによって記述されるため、非プログラマであるデザイナーが外観を記述できるという恩恵を受ける[2]。

Vue.jsのMVVM

Vue.jsでは<template>にHTML風の記法で外観を記述し、<script setup>内のデータやロジックをバインドする。 これは前節のWPFの手法と能く似ている[IMO]。

Vue.jsはMVVMに基づいているとしばしば言われるが、 Vue.js公式は次のようにバージョンごとに異なる声明を出している:

v1時代のVue.jsではHTML上のあるDOM要素とscript上のあるVueインスタンスを同期することでアプリケーションを駆動させていた[4]。 これは、言ってしまえばWeb版WPFであり、確かにMVVMと言って良い[IMO]。

一方、v2時代以降のVue.jsでは、本節の冒頭に書いた通り、単一のファイルに外観を表現する<template>とその状態・処理を記述する<script setup>を書く。 このSingle File Component (SFC)と呼ばれる形式は、言うなれば、ViewとViewModelが単一のファイルに書かれる形式である。 このために、あるViewModelに対して異なるViewを適応することができなくなっており、WPFとは少々異なる[IMO]。

また、WPFが双方向データバインディングであるのに対し、Vue.jsは単方向データバインディングであるという違いもある[IMO]。 例えば、テキストフィールドに文字列データをバインドする場合、 WPFではテキストフィールドに{Binding Foo}を指定すると、View上で値が変わればViewModel上でも値が変わるし、ViewModel上でFooを更新すればViewに反映される[AFAIK]。 一方、Vue.jsで同様のことをするにはv-model="foo"を指定するが、これは:value="foo" @input="event => foo = event.target.value"の糖衣構文である。

MVVMの弱点

ViewはModelについて無知である。 しかし、ViewはModelの視覚的表現である。 従って、ViewとModelの間に立つViewModelがModelへのアクセスAPIを持つことになる。 当然ながら、Viewが必要とするModelのデータが多ければ多いほど、そのAPIの数は増える。 APIの数が増えるほど、ボイラープレートコードが増えることになる[3]。

また、[3]ではデータバインディングの都合上メモリ効率が落ちるという弱点が語られている。

他にも、基本的にはViewModelはModelをプロキシプロパティとして持つことになるが、ViewにとってそれがModelを反映しているか保証しない。 つまり、ModelとViewModelが正しく同期されているか不明瞭であるという弱点がある[AFAIK]。

参考文献

  1. Presentation Model (Martin Fowler)
  2. Introduction to Model/View/ViewModel pattern for building WPF apps (John Gossman)
  3. Advantages and disadvantages of M-V-VM (John Gossman?)
  4. The Vue Instance (Vue.js)
  5. The Vue Instance (Vue.js)
  6. Form Input Bindings (Vue.js)

理解に役立った文献

  1. 起源から整理するGUIアーキテクチャパターン (中條剛)
  2. Patterns - WPF Apps With The Model-View-ViewModel Design Pattern (Josh Smith)

Elmアーキテクチャ

概要

Elm言語で自然と発生したアーキテクチャ[1]。 Elm言語は純粋関数型言語の一つである。 その点、SmalltalkあるいはC#のオブジェクト指向的なMVC系列とは異なる。

主に次の要素から構成される[1]:

次の流れを持つ[1]:

  1. ユーザが入力する
  2. ViewがMessageをランタイムへ発行する
  3. ランタイムがModelとMessageを伴ってUpdateを呼び出す
  4. Updateが新しいModelを作成してランタイムへ渡す
  5. ランタイムがModelを更新する
  6. ランタイムがModelを伴ってViewを呼び出す
  7. Viewが更新される

Updateは新しいModelを返すだけでなく、あるMessageを返すCommandを返すことができる。 CommandはElmの副作用を扱うための仕組みである。 UpdateではCommandを返す関数を実行し、その返戻値を返す。 するとランタイムはCommandがラッピングしていた本体を実行し、その返戻値をUpdateにディスパッチする。 つまり、Messageを連鎖できるのである[1]。

必ずMessageを介してUpdateを呼び出すことで、何が起きてModelが更新されたかがわかりやすくなる。 これにより、次の恩恵を受ける[AFAIK]:

MVVMとの違い

一見するとMVVMと違いがないように見えるが、いくつか違いがある。

WPFは外観をXAMLというプログラムコードとは異なる言語で記述する。 一方、Elmアーキテクチャでは外観の定義もプログラムコードと同じElm言語で記述する[1]。

まず、Modelは「アプリケーションの状態」と説明されている[1]。 MVC系列ではModelはドメインモデルであった。 そのため、ドメインとは関係のない視覚的表現に関わる状態や処理をどこに配置するかが論点になっていた。 一方、ElmアーキテクチャではModelは「アプリケーション」の「状態」である。 そのため、視覚的表現に関する処理はViewの責任である。 例えば、数値で管理されている金額「19800」を「$19,800」のように表示する場合、WPFではViewModelに書くが、ElmアーキテクチャではViewに書く[AFAIK]。

前述の通り、Elm言語は純粋関数型言語である。 UpdateはModelとMessageを引数に取りModelを返すカリー化された純粋関数である。 従って、UpdateができるのはModelの更新に限られる。 WPFと同じことをしていても、パラダイムが異なる。 尚、厳密にはUpdateの返戻型はModelそのものではない。 これは副作用を伴う処理に委譲するためである[1]。

ViewとUpdateとはMessageという抽象によって仲介される。 WPFでは双方向データバインディングによってView側でViewModel上のデータを直接更新する。 あるいはまた、Vue.jsでは<script setup>上のイベントハンドラをv-onによって呼び出してデータを更新する。 しかし、Elmアーキテクチャではデータを更新するためには必ず「何が起こるか」「何をすべきか」をMessageで表現しElmランタイムへ伝える[1]。 言ってしまえば、ViewはModelの更新について完全に無知である[IMO]。

参考文献

  1. Elm について (はじめに) (Elm-jp)

Flux/Redux

概要

FluxはFacebookによって考案されたアーキテクチャである。 同じくFacebookが手がけるReactの文脈で登場した。 現在は開発が終了している[1]。

また、ReduxはFluxの関数型版である。 公式にElmアーキテクチャにも影響を受けていることを表明している[2]。

Flux

主に次の要素で構成される[1]:

ユーザ入力→View→Action→Dispatcher→Store→(ランタイム)→Viewという流れを持つ。

Actionは必ずしもViewから発行されるわけではない。 サーバレスポンス・タイマーといった非同期処理やプッシュ通知・ブラウザイベント等の外部システムに起因して発行され、適切にStoreを更新することができる[AFAIK]。

Elmアーキテクチャとよく似ているが、Elm言語が純粋関数型言語であるのに対し、JavaScriptはマルチパラダイム言語である。 そのため、ReactではuseState()等を使ってコンポーネントに状態・処理を内包できる。 とすると、わざわざfluxというパッケージを導入する意味はないと思われるが、そうではない。 Storeは「アプリケーション」の「データ」を保持するものである。 逆に言えば、Reactコンポーネント内の「状態」はコンポーネントの状態に過ぎない。 従って、ReactのuseState()等だけに頼っていると、異なるコンポーネント間の連携のために地獄のようなPropsやContextを書くことになる。 それを嫌って、グローバル環境に安全に状態・処理を配置できるライブラリとしてfluxが作られたのである[IMO]。

また、言ってしまえば、StoreはMVC系列のModelであり、MVVMが抱えていたModelとViewModelの同期問題の解決となっている[3][IMO]。

Redux

主に次の要素で構成される[3][4]:

Fluxと決定的に異なるのはDispatcherが存在しない点である。 FluxではDispatcherというアプリケーション唯一無二のオブジェクトにStoreの更新処理を登録しておき、ここへ渡されたActionをすべての更新処理へ転送していた。 つまり、Storeとその更新処理は実質的に分離されており、Storeはあくまで状態置き場であった。 一方Reduxでは、Actionから更新処理へのディスパッチはプログラマの責務としてReducerという関数に書き起こし、それをStoreに持たせている。 これにより、各更新処理でのActionのバリデーションが不要になる他、Reducerを差し替えることでテストが容易になるという恩恵を受ける[3]。

また、Fluxでは更新処理内でStoreを直接更新していたが、ReduxではReducer内で新しく作成した状態をランタイムに渡して更新する。 これはElmアーキテクチャを倣う不変性を意識した仕様であり、次の恩恵を受ける[3][IMO]:

尚、Reducer内からActionを発行できない。 そのため、連鎖的なActionが必要な場合は、呼び出し側の責任となる[5]。

TCA

The Composable Architecture (TCA)はSwift向けのライブラリである。 ReduxとElmアーキテクチャに影響を受けている。 名前に反してアーキテクチャではなくライブラリであるため、章としては立項せずここに記載する[6]。

TCAは次の点でReduxと異なる[AFAIK]:

厳密にはTCAのReducerはReducerプロトコルを実装したオブジェクトである。 ReduxにおけるReducerに該当するのはReduceクロージャである。 このReduceの返戻型はEffect<Action>であり、ここに同一あるいは別のReducerのActionを指定することでActionを連鎖できる。 これはElmアーキテクチャのCommandの影響である[6][AFAIK]。

ReduxにおいてStoreのインスタンスはグローバル環境に配置されたアプリケーション唯一無二のシングルトンである。 従って、それを必要とするViewはどこからでもStoreにアクセスすることができ、そのStoreは同一のインスタンスである。 一方、TCAでは一つのビューインスタンスに対して個別のStoreインスタンスを与える。 これは実質的にViewとStoreがセットで定義されることを意味し、StoreはView専属の状態管理オブジェクトとなる。 もしグローバルなStoreが欲しい場合は、どこかでインスタンスを作成し、@Dependencyで参照する[AFAIK]。

参考文献

  1. flux (GitHub)
  2. Prior Art (Redux)
  3. Redux Essentials, Part 1: Redux Overview and Concepts (Redux)
  4. Redux Fundamentals, Part 4: Store (Redux)
  5. Reducing Boilerplate (Redux)
  6. swift-composable-architecture (GitHub)

おわりに

骨が折れました。 が、色々と調べて大変学びになりました。 最初の頃は「UIアーキテクチャの歴史はドメインとプレゼンテーションの分離の歴史だ」と思っていましたが、全然そんなことはありませんでした。 ではUIアーキテクチャとは何なのでしょう。 たぶん、一言では表せないのだと思います。 何かに不満を持った人が新しいものを生み出した。 そうして、どこか、少しずつ、進化してきた。 そういうものなのだと思います。

ただ、はっきりと気づいたことがあります。 私はこういう記事を読む度に「サンプルコードも書け! これじゃどう実装すれば良いかわからないだろうが!」と発狂する人生を歩んできたのですが。 読んでわかる通り、本記事には一切のサンプルコードも載せていません。 これは「どう実装すれば良いかわからない・実装してみたけど上手くいかない場合は環境やプロダクトが適していないだけ」という気づきのためです。 UIアーキテクチャは思想です。 どのオブジェクトを定義してどこでインスタンスを作って……というところまでは範囲していなくて良いのです。 確かに新しめのアーキテクチャはライブラリとして頒布されており、どう実装すれば良いかを示す文献が用意されています。 が、結局のところWPFなりReactなりに乗っかっているわけで、例えばC++やそれこそSmalltalkでも実装できるんですか、という話になります。 その土台となる環境ごと実装したならば、きっとそれは別のアーキテクチャになっているはずです。 環境に合わないアーキテクチャを選択する愚かしさは、MVCからCが欠落しても尚MVCを使い続けようとする愚かしさと同じです。 それを歴史は物語っています。

雑記