最近の私の最悪の仕事のひとつはフロントエンド開発者向けのAPIを設計することです。それは次のような会話になります。
開発者 – この画面にはxとyとzの要素がある。… だから{x: , y:, z: }というフォーマットのレスポンスを返すAPIを作ってくれないかな。
Me – はい。
もうこれ以上議論したくありません。プロジェクトは最終的には頻繁に変わる画面に紐付くたくさんのAPIが生まれ、知らないうちに“デザイン”によってAPIが変わり、気がつけばたくさんのAPIとそれぞれのAPIに必要なフォームの要素とプラットフォーム変数が出来上がります。Sam Newmanはこのような方法をBFFパターンとして明確化を始めています。このパターンはデバイスごと、プラットフォームごと、そしてもちろんアプリのバージョンごとにAPIを開発することを認めています。Daniel Jacobsonによれば、Netflixは同社の“Experience API”に”Ephemeral(はかない)”という新しい名前をつけようとしています。なんということでしょう…
数ヶ月前、私はなぜここにたどり着き、何が可能かを理解する旅に出ました。この旅は、私にアプリケーションアーキテクチャ、MVCという強烈な宗教に対する疑いをもたらしました。そして、リアクティブ、関数型プログラミングの真の実力に触れたのです。また、シンプルさに集中する旅でもあり、私たちの産業はうまくやっているという考えを捨てる旅でもありました。どんなことを見つけたか興味がある方もいるでしょう。
私たちの見ている画面の背後にあるパターンはMVC –Model-View-Controllerです。まだウェブがなくソフトウエアアーキテクチャも分厚いクライアントが単一のデータベースに原始的なネットワークでアクセスするのがせいぜい、という時代にMVCは生まれました。そして数十年後、MVCはまだ現役であり、衰え知らずでオムニチャネルアプリケーションの開発に使われています。
Angular2のリリースの前にMVCパターンの利用、つまり、MVCというフレームワークがアプリケーションアーキテクチャにもたらした価値を再考するのは良いタイミングです。
私が始めてMVCに出会ったのは1990年、NeXTがInterface Builderをリリースした後(この時代のソフトウエアの一部分がまだ今も残っていると考えると驚きです)でした。当時、Interface BuilderとMVCは大きな進歩のように感じられました。90年代の後半、MVCパターンはHTTP経由でも導入され(Strutsを覚えていますか)、今日のMVCはアプリケーションアーキテクチャの鍵です。
MVCとは大きく違うReact.jsでさえ一度だけ自己紹介に“Reactは単なるMVCのViewです”という婉曲的な表現をせざるを得なかったのです。
私が昨年Reactを使い始めたとき、だいぶ異なる印象を受けました。どこかでデータの一部を変更するとビューとモデルの間での明示的な操作なしですぐにUI全体が変更されます(フィールドやテーブル内の値だけではない)。そうはいっても、私はReactのプログラミングモデルにはがっかりしました。私だけではないはずです。Andre Medeirosは次のように書いています。
Reactはいくつかの点で私をがっかりさせましたが、主に貧弱なAPIが残念でした。これだとプログラマは複数の関心事をひとつのコンポーネントにまとめようとしてしまいます。
サーバサイドのAPIの設計者として、私はReactのフロントエンドにAPI呼び出しをうまく入れる方法はないという結論に達しました。Reactはビューだけにフォーカスしており、プログラミングモデルの中にコントローラがないからです。
Facebookは今まで、フレームワークレベルでこのギャップを修正することに抵抗しています。ReactチームはFluxパターンを導入しましたが、これも同じようにがっかりさせるものでした。そして最近、Dan AbramovがReduxという別のパターンを推進しています。これは適切な方向に進んでいますが、私が下で示すようなフロントエンドへ接続するAPIを実装する適切な方法は提供されていません。
GWTとAndroid SDK、そしてAngularの間で、Googleのエンジニアはフロントエンドアーキテクチャについて確固たる考えを持っていると考える人もいるかもしれませんが、Angular2の設計を読むと、Googleは自分たちがしていることを理解しているのだ、という暖かい気持ちになる必要がないことがわかります。
Angular 1はコンポーネントという概念を使って構成されてはいません。そのかわり、コントローラをページのさまざまな要素に、カスタムロジックと共にアタッチします。スコープをカプセル化する方法を指定するカスタムによって、スコープがアタッチされます。
コンポーネントベースのAngular2はもっとシンプルなのでしょうか。いいえ。Angular 2のコアパッケージだけで180の構文があり、フレームワーク全体だと500近い構文があります。HTML5とCSS3の上で動作します。ウェブアプリを作るのにこれだけのことを覚える時間がある人はいるのでしょうか。Angular3が出たらどうなるのでしょう。
Reactを使い、Angular2を見て、私はがっかりしました。これらのフレームワークはシステム的にBFFパターンを使うことを強制し、すべてのサーバサイドのAPIが画面のデータセットに適合するようになります。
このときが“to hell with it”と思った瞬間でした。ビューと背後のAPIの間のより良い設計を探るため、ReactともAngularもMVCも使わないでウェブアプリを作ろうと思ったのです。
Reactについて私が優れていると感じたのはモデルとビューの関係でした。Reactがテンプレートベースではなくビュー自体がデータを要求する方法をもっていないという点は探究してみる価値があると感じました(ビューへデータをパスするだけ)。
よく調べてみるとReactの唯一の目的はビューを一連の(純粋な)関数へ分解することだということがわかります。 次のJSX構文をご覧ください。
これは下の記述と何の違いもありません。
V = f( M )
例えば、今、私が携わっているウェブサイトのひとつであるGliiphは、次のような関数で作られています。
(クリックして拡大)
fig 1. サイトのスライダーコンポーネントHTMLを生成する関数
この関数はモデルから作られます。
(クリックして拡大)
fig 2. スライダーの背後にあるモデル
普通の従来のJavaScriptの関数がこのような仕事を問題なくこなすことに気づくと、なぜReactを使う必要があるのかという疑問が生まれます。
仮想DOMを使いたいからでしょうか。そうなら(そのような人が多いのかどうかはわかりませんが)、React以外の選択肢もあります。今後、選択肢は増えるでしょう。
GraphQLを使いたいからでしょうか。まさか。Facebookで多くの実績があるのだから自分にとっても使えるものだろうと考えるようなバカな真似はやめてください。GraphQLはビューモデルを作るための宣言的な方法にすぎません。ビューに適合するモデルを強制されるのは問題であって解決策ではありません。Reactチームはどのようにして“クライアントに固有のクエリ”でデータを問い合わせるて問題ないと考えたのでしょうか。
GraphQLはビューとビューを書くフロントエンドエンジニアの要件を満たすために作られています。[…]GraphQLのクエリはクライアントが求めたもののみ取得します。
GraphQLのチームが見逃したのはJSXの構文では、関数でモデルをビューから分離するというのが微妙な変更だということです。テンプレートや“フロントエンドエンジニアによって書かれたクエリ“とは違い、関数はビューにフィットするモデルを求めません。
ファンクション(テンプレートやクエリではなく)がビューを作るとき、モデルの形の制約なしでビューにもっともよく適合するようにモデルを必要に応じて変換します。
例えば、ビューがvという値と、この値が、素晴らしい、良い、悪いのどれなのかを表す指標を持つ場合、モデルに指標の値を持つ必要はありません。関数でモデルが提供するvを使って指標の値を計算すればよいのです。
ビューにこのような計算を直接埋め込むのは良いアイディアではありませんが、ビューモデルを純粋な関数にするのは難しくありません。それゆえ、明示的なビューモデルを必要とする場合、GraphQLを使う良い理由は特にありません。
V = f( vm(M) )
長年、モデル駆動開発を実践してきた私としては、GraphQLのような複雑な問い合わせ言語やテンプレートのようなメタデータよりもコードを書いたほうがうまくいくことを保証します。
関数を使った方法にはいくつかの利点があります。まず、Reactのようにビューをコンポーネントに分割します。その自然なインターフェースでウェブアプリやウェブサイトの“テーマ”を設定したり、ネイティブなど異なる技術のビューを描画できます。関数の実装はレスポンシブなデザインを実装する方法を強化する可能性があります。
例えば、次の数ヶ月のうちにコンポーネントベースのJavaScriptの関数でHTML5のテーマを提供し始めたとしても驚きません。最近私が自分のウェブサイトでやっていることだからです。テンプレートをピックアップしてJavaScriptの関数で包みます。WordPressは使いません。WordPressを使うのと同じくらい(あるいは少ない)の労力でHTML5とCSS3の力を引き出せます。
この方法はデザイナーと開発者の間に新しい関係を求めます。誰もがJavaScriptの関数を書きます。特にテンプレート設計者は。学習するべき“バインディング”の構文はありません。JSXもAngularのテンプレートもありません。昔ながらのJavaScriptの関数があるだけです。
興味深いことにリアクティブの流れからみると、これらのファンクションはサーバでもクライアントでももっとも適合しやすいところに配置できます。
しかし、もっとも重要なのはこの方法によってビューはモデルの最小限の規約を宣言し、ビューにデータを持ち込む方法はモデルに任せることができる点です。キャッシュや遅延ロード、オーケストレーション、一貫性といった側面はモデルによって管理されます。テンプレートやGraphQLとは違い、ビューからの要求を直接処理することはありません。
これで、モデルからビューを分離する方法を確立しました。ここからアプリケーションのモデルをどのように作るか、が課題です。“コントローラ”はどうすればよいか。この課題を考えるためにMVCに立ち戻ってみます。
80年代初頭、AppleがXerox PARCからMVCを盗んだとき、彼らはこのパターンについていくつか学び、それ以降、信心深くMVCを実装してきました。
fig.3. MVCパターン
問題は、Andre Medeirosが言うようにMVCパターンは“インタラクティブ” (リアクティブではなく)だということです。従来のMVCでは、アクション(コントローラ)はモデルの更新メソッドを呼び、成功したか失敗したかでビューの更新方法が変わります。Andreが指摘するように、この方法が絶対というわけではありません。アクションは単にモデルの値を渡すだけで、モデルが更新されるかどうかを決めるものではないと考えると、同等の有効でリアクティブな方法も考えられます。
すると、リアクティブなフローにどのようにアクションを統合するか、が課題になります。アクションについて理解したければ、TLA+を見てみるといいでしょう。TLAは“Temporal Logic of Actions”という意味でLamport博士が考えました。博士はこの業績でチューリング賞を受賞しています。TLA+ではアクションは純粋な関数です。
data’ = A (data)
私はTLA+の考えがとても好きです。関数は単なるデータセットの変換であるという事実を示しているからです。
この点を念頭に置くと、リアクティブなMVCは次のようになるでしょう。
V = f( M.present( A(data) ) )
この式が規定しているのは、アクションが動くとき、アクションが入力されたデータセット(ユーザの入力など)を計算し、それがモデルが表現して、モデルが自分を更新する方法を決めます。更新が完了すると、ビューは新しいモデルの状態を描画します。これでリアクティブのループが閉じます。モデルの永続化とデータの検索はリアクティブなフローとは無関係で、絶対に、絶対に“フロントエンドエンジニアが書いてはなりません”。
アクションは純粋な関数で、状態も副作用もありません。
リアクティブMVCパターンは興味深いです。モデル以外はすべて純粋な関数だからです。Reduxはこのパターンを実装しているように思いますが、Reactの不要な作法があり、Reducer内のモデルとアクションの間に結合があります。モデルとアクションの間のインターフェースは純粋なメッセージパッシングです。
とはいえ、リアクティブMVCパターンはこのままでは不完全であり、Dan Abramovが言うように現実世界のアプリケーションは作れません。簡単な例で理由を説明します。
ロケットの発射装置を制御するアプリケーションを開発するとします。カウントダウンが始まったらシステムはカウンターを減少させ、カウンターが0になったら、モデルのすべてのプロパティを名目的な値のままにして、ロケットの発射を開始します。
アプリケーションのステートマシンは単純です。
fig.4. ロケット発射装置のステートマシン
減少も発射も“自動的”なアクションであり、カウントの状態に入る(または再入する)と、遷移ガードが評価されカウンターが0以上なら減少アクションが動き、0になったら、起動アクションが動きます。中断アクションはどの時点でも発生する可能性があり、制御システムを中断状態に遷移させます。
MVCでは、このようなロジックはコントローラで実装されます。ひょっとしたらビューのタイマーが引き金で動くのかもしれません。
この段落はとても重要なので注意深く読んでください。TLA+では、アクションは副作用がなく結果の状態は計算され、モデルがアクションを処理しモデル自体を更新します。これはアクションが結果状態を定義する、つまり、結果状態はモデルからは独立しているという従来のステートマシンの意味論から根本的に離れるようになります。TLA+では、有効なアクションあり、それゆえ、状態の表現(例えばビュー)から起動できるアクションは状態の変更を起動するアクションとは直接繋がっていません。言い換えれば、ステートマシンは従来通りのふたつの状態(S1, A, S2)を連結するタプルとして定義するべきではない、それよりもフォーム(Sk, Ak1, Ak2,…)のタプルで可能なアクションを定義し、例えば、状態Skの場合、アクションがシステムに適用された後、結果状態が計算され、モデルが更新処理を行います。
“状態”オブジェクトを導入し、アクションとビュー(単なる状態の表現)を分離するとき、TLA+の意味論はシステムを概念化するための優れた方法を提供します。
私たちの例でのモデルは以下の通りです。
model = {
counter: ,
started: ,
aborted: ,
launched: }
システムのこの4つの(制御)状態は次のモデルの値に関連します。
ready = {counter: 10, started: false, aborted: false, launched: false }
counting = {counter: [0..10], started: true, aborted: false, launched: false }
launched = {counter: 0, started: true, aborted: false, launched: true}
aborted = {counter: [0..10], started: true, aborted: true, launched: false}
モデルはシステムのすべてのプロパティと取りうる値で定義され、状態が可能なアクションを定義します。ビジネスロジックはどこかに実装しなければならなりません。しかし、ユーザがアクションが可能かどうかを知っているということは期待できません。これは簡単な回避策はありません。ビジネスロジックは書くのもデバッグもメンテナンスも難しく、MVCにはビジネスロジックを記述する意味論はないのです。
ロケット起動装置の例でいくつかのコードを書いてみましょう。TLA+の視点では、次のアクションは状態の描画に論理的に従います。現在の状態が表現されると、次のステップでは次のアクションの述語を実行します。この述語は次のアクションを計算し実行します。データがあればモデルに渡して、モデルは新しい状態の表現の描画を実行します。
(クリックして画像を拡大)
fig.5. ロケット発射装置の実装
クライアント/サーバアーキテクチャでは、自動アクションが実行されたら、WebSocket(WebSocketが使えないならポーリング)のようなプロトコルを使って状態を適切に描画する必要がある。
私はとても小さいオープンソースのライブラリをJavaとJavaScriptで書きました。このライブラリを適切なTLA+の意味論を使って状態オブジェクトを構造化します。また、WebSocketとポーリング、キューイングを使ってブラウザとサーバのやりとりを実装したサンプルも提供しています。ロケット発射装置の例で見た通り、このライブラリを利用しなければならないと感じる必要はありません。状態の実装は、一度書き方がわかってしまえば、比較的簡単に実装できます。
今、MVCの代替となる新しいパターンを導入するためのすべての要素が手に入りました。新しいパターンとは、SAMパターン(State-Action-Model)です。リアクティブで関数的なReact.jsとTLA+をルーツに持つパターンです。
SAMパターンは次の式で表現されます。
V = S( vm( M.present( A(data) ) ), nap(M))
この式はアクションAが適用された後、システムのビューVが計算される、という純粋な関数のモデルです。
SAMではA(アクション)、vm(ビューモデル)、nap(次のアクションの述語)、S(状態)はすべて純粋な関数です。SAMでは、一般的に“状態”(システムの属性の値)と呼ばれるものはモデルに閉じ込められ、これらの値を変更するロジックはモデルの外部からは見えません。
次の状態の述語であるnap()は状態の表現が作られてたときに呼ばれるコールバックでです。
fig.6. State-Action-Mode (SAM)パターン
このパターン自体はプロトコルから独立しています(HTTPに関する難しさとは無関係に実装できます)。どのようなクライアント/サーバの位置関係でも実現できます。
SAMはビューからコンテンツを取り出すのにステートマシンを使わなければならないということを意味しません。アクションがビューから単独で発動された場合、次のアクションの述語はnull関数になります。けれども、基底にあるステートマシンの制御状態を明らかにするのは、良いやり方かもしれません。ビューは これはよいやり方かもしれません。制御状態ごとにビューが違って見えるかもしれませんから。
一方、ステートマシンが自動アクションを含む場合、アクションもモデルも次のアクションの述語がなければ純粋にはならない。アクションが状態を持たざるを得ないか、モデルが本来は役割ではないアクションの起動をしなければなりません。ちなみに、直感的ではないのですが、状態オブジェクトどんな“状態”も持っていません。ビューの描画と次のアクションの述語の計算をするのは純粋な関数です。
この新しいパターンの利点はアクションからCRUDの操作を明確に分離することです。モデルは自身の永続化に責任を持ち、CRUDの操作を持ちます。ビューからアクセスはできません。特に、ビューはデータを“フェッチ”する位置にはなりません。ビューがするのは現在の状態の表現を要求し、アクションを引き金にしたリアクティブなフローを起動することです。
アクションは単にモデルへ変化を提案するための認められた経路でしかありません。アクション自体は副作用を持ちません。必要があれば、アクションはサードパーティーのAPIを呼び出します(モデルへの副作用はありません)。例えば、住所を変更するというアクションは住所の検証サービスを呼び、そのサービスから戻されたアドレスをモデルに渡す、ということがありえます。
アドレス検証APIを呼び出す“住所変更”アクションは以下の通りです。
(クリックして拡大)
fig.7. “アドレス変更”の実装
パターン、アクション、モデルの要素は次のように構成されます。
関数の構成
data’ = A(B(data))
ピアの構成 (ふたつのモデルに渡された同じデータ)
M1.present(data’)
M2.present(data’)
親-子の構成 (親のモデルが子供のデータセットを制御する)
M1.present(data’,M2)
function present(data, child) {
// perform updates
…
// synch models
child.present(c(data))
}
発行/購読の構成
M1.on(“topic”, present )
M2.on(“topic”, present )
Or
M1.on(“data”, present )
M2.on(“data”, present )
エンゲージメントのためのシステムと記録のためのシステムという観点で考えるアーキテクトにとって、このパターンで記録のためのシステムとのやりとりに責任を持つモデルを定義して、ふたつの層のインターフェースを明確にできます。
fig 8. SAM 構成モデル
パターン自体は構成可能であり、ブラウザ上で動くSAMインスタンスを実装して、不思議な動き(ToDoアプリケーションのような)をサポートし、サーバサイドのSAMインスタンスとやりとりすることができます。
fig. 9 SAMインスタンスの構成
内部のSAMインスタンスは外部のインスタンスによって生成された状態の表現の一部としてデリバリされます。
セッションの再水和はアクションを起こす前に行われます(下図参照)。SAMは興味深い構成を実現します。モデルにデータを渡す前に、ビューがトークンを提供するサードパーティのアクションと、その呼び出しを検証するためのシステムのアクションを指し示すコールバックを呼び出します。
fig. SAMでのセッション管理
CQRSの視点から考えてみます。このパターンはクエリとコマンドに特別な区別をしないものの、基底となる実装では区別する必要があります。検索、または、問い合わせ“アクション”では、モデルにパラメータを設定しています。規約(例えばアンダースコアのプレフィックス)を導入してクエリとコマンドに違いをつけることもできますし。ふたつの別々のメソッドをモデルに付けてもよいでしょう。
{ _name : ‘/^[a]$/i’ } // Names that start with A or a
{ _customerId: ‘123’ } // customer with id = 123
モデルはクエリに適合する必要な操作をし、内容を更新してビューの描画を行います。同じような規約のセットが、モデルの要素の作成、更新、削除に使われます。モデルにアクションの出力を渡すための実装はいろいろと考えられます(データセットやイベント、アクション…)。それぞれの方法に良い点、悪い点があり、最終的には好みの問題かもしれません。私はデータセットを使った方法が好きです。
例外の観点から見ると、Reactと同じようにモデルが対応する例外を属性値(アクションから渡された値であれ、CRUD操作の結果であれ)として持ちます。これらのプロパティは例外を表示するために状態を描画しているときに使われます。
キャッシュについて言えば、SAMは状態の表現のレベルでキャッシュを提供します。状態を表現する関数の結果をキャッシュすると高いヒット率になることは直感的にわかります。アクション/レスポンスレベルではなく、コンポーネント/状態レベルでキャッシュを使うからです。
このパターンのリアクティブで関数的な構造はリプレイや単体テストにも影響を与えます。
SAMはフロントエンドアーキテクチャのパラダイムを完全に変えます。ビジネスロジックはTLA+の基礎の上に明確に次の3つの仕方で実現されます。
- 純粋な関数としてのアクション
- モデルでのCRUD操作
- 自動アクションを制御する状態
API設計者として見ると、このパターンはAPI設計の責務をサーバ側に渡しているように見えます。ビューとモデルの規約も可能な限り小さいです。
純粋関数としてのアクションはモデルが対応するアクションの出力を受け入れる限り、モデルをまたいで再利用可能です。アクションのライブラリ、テーマ(状態の表現)、そして、モデルが活躍するでしょう。それぞれ独立して構成できるからです。
SAMはマイクロサービスにも適合します。Hivepod.ioのようなフレームワークも使えるでしょう。
そして、もっとも重要なのはこのパターンがReactと同じように、データバインディングやテンプレートを必要としないということです。
SAMは仮想DOMをブラウザの恒久的な機能にすることに貢献し、新しい状態の表現は専用のAPIで直接処理されるでしょう。
革新的な旅になりました。数十年のオブジェクト指向の世界は過去のものになっているようです。私はリアクティブ、関数的という言葉なしでは考えられません。私がSAMで構築したものとその構築スピードは前代未聞でした。
この記事のレビューを快く引き受けてくださった、Jean Bezivin博士、Joëlle Coutaz博士、Braulio Diez氏、Adron Hall氏Edwin Khodabackchian氏、Guillaume Laforge氏、Pedro Molina氏、Arnon Rotem-Gal-Oz氏に感謝いたします。
著者について
Jean-Jacques Dubray はxgen.ioとgliiphの創立者。サービス指向アーキテクチャ、APIプラットフォームの開発に15年従事。以前はHRLの研究員を勤め、Prologが生まれたプロヴァンス大学でPh.Dを取得。BOLTという方法論の発案者でもある。