モデル・ビュー・コントローラ (MVC)

カーソン・グロス

htmxとハイパーメディアの使用に対するよくある反論は、次のようなものです。

サーバーからHTML(JSONではなく)を返すことの問題点は、モバイルアプリにもサービスを提供したい場合、APIを複製したくないということです。

私はすでに別のエッセイで、JSON APIとハイパーメディアAPIを別々のコンポーネントに分割する必要があると考えていることを概説しました。

そのエッセイでは、HTMLを返す「変化の激しい」WebアプリケーションAPIエンドポイントと、安定した、規則的で表現力豊かなJSONデータAPIを分離するために、APIを(ある程度)「複製」することを明示的に推奨しています。

このアイデアについて人々とした conversations を振り返ってみると、多くの人が私ほど馴染みのないパターン、つまりモデル・ビュー・コントローラ (MVC) パターンを熟知していることを前提としていたと思います。

#MVC入門

私は最近のポッドキャストで、多くの若いWeb開発者がMVCの経験があまりないことを知り、少しショックを受けました。これはおそらく、シングルページアプリケーションが標準になったときに発生したフロントエンド/バックエンドの分割が原因かもしれません。

MVCはWeb以前から存在するシンプルなパターンであり、ユーザーにグラフィカルインターフェイスを提供するほぼすべてのプログラムで使用できます。

大まかな考え方は以下のとおりです。

多くのバリエーションがありますが、それが考え方です。

Web開発の初期段階では、多くのサーバーサイドフレームワークがMVCパターンを明示的に採用しました。私が最もよく知っている実装はRuby on Railsです。これは、データベースに永続化されるモデル、HTMLビューを生成するためのビュー、および2つの間を調整するコントローラのそれぞれについてドキュメントを提供しています。

Railsにおける大まかな考え方は次のとおりです。

Railsは、基礎となるHTML、HTTPリクエスト/レスポンスライフサイクルの上に構築された、MVCパターンのかなり標準的な(やや「浅い」かつ簡略化された)実装を備えています。

#ファットモデル/スキニーコントローラ

Railsコミュニティでよく取り上げられた概念の1つは、“ファットモデル、スキニーコントローラ”の概念です。ここでの考え方は、コントローラは比較的シンプルで、モデルのメソッドを1つまたは2つ呼び出してから、すぐに結果をビューに渡すだけにする必要があるということです。

一方、モデルは、多くのドメイン固有のロジックを持つ、はるかに「厚い」可能性があります。(これが神オブジェクトにつながるという反論がありますが、ここではそれは置いておきましょう。)

MVCパターンの簡単な例と、それがなぜ役立つのかを説明する際に、このファットモデル/スキニーコントローラの考え方を覚えておきましょう。

#MVCスタイルのWebアプリケーション

例として、私の好きなものの1つであるオンライン連絡先アプリケーションを見てみましょう。これは、HTMLページを生成することによって連絡先の特定のページを表示する、そのアプリケーションのコントローラメソッドです。

@app.route("/contacts")
def contacts():
    contacts = Contact.all(page=request.args.get('page', default=0, type=int))
    return render_template("index.html", contacts=contacts)

ここでは、ハイパーメディアシステムズの本で使用しているため、PythonFlaskを使用しています。

ここでは、コントローラが非常に「薄い」ことがわかります。 `Contact`モデルオブジェクトを介して連絡先を検索し、リクエストから`page`引数を渡すだけです。

これは非常に典型的です。コントローラの仕事は、HTTPリクエストを何らかのドメインロジックにマッピングし、HTTP固有の情報を引き出して、ページ番号など、モデルが理解できるデータに変換することです.

次に、コントローラは連絡先のページングされたコレクションを`index.html`テンプレートに渡して、HTMLページにレンダリングし、ユーザーに送り返します。

一方、`Contact`モデルは、内部的には比較的「ファット」である可能性があります。その`all()`メソッドは、内部的にデータベース検索を実行し、何らかの方法でデータをページングし、変換やビジネスルールを適用するなど、多くのドメインロジックを持つ可能性があります。そして、それは問題ありません。そのロジックはContactモデル内にカプセル化されており、コントローラはそれを処理する必要はありません。

#JSONデータAPIコントローラの作成

そのため、ドメインをカプセル化するこの比較的よく開発されたContactモデルがあれば、同様のことを行うが、HTMLドキュメントではなくJSONドキュメントを返す*別の* APIエンドポイント/コントローラを簡単に作成できます

@app.route("/api/v1/contacts")
def contacts():
    contacts = Contact.all(page=request.args.get('page', default=0, type=int))
    return jsonify(contacts=contacts)

#しかし、あなたはコードを複製しています!

この時点で、これら2つのコントローラ関数を見ると、「これはばかげている、メソッドはほとんど同じだ」と思うかもしれません。

そして、あなたは正しいです。現在、それらはほとんど同じです。

しかし、システムに2つの潜在的な追加を検討してみましょう。

#JSON APIのレート制限

まず、DDOSまたは不適切に記述された自動クライアントがシステムを圧倒するのを防ぐために、JSON APIにレート制限を追加しましょう。Flask-Limiterライブラリを追加します

@app.route("/api/v1/contacts")
@limiter.limit("1 per second")
def contacts():
    contacts = Contact.all(page=request.args.get('page', default=0, type=int))
    return jsonify(contacts=contacts)

簡単です。

しかし、注意してください。Webアプリケーションにその制限を適用したくありません。JSONデータAPIにのみ適用したいのです。そして、2つを分割したので、それを実現できます。

#Webアプリケーションへのグラフの追加

別の変更を考えてみましょう。HTMLベースのWebアプリケーションの`index.html`テンプレートに、1日あたりに追加された連絡先の数のグラフを追加したいとします。このグラフの計算にはコストがかかることがわかりました。

グラフの生成で`index.html`テンプレートのレンダリングをブロックしたくないため、代わりに遅延読み込みパターンを使用します。これを行うには、その遅延読み込みコンテンツのHTMLを返す新しいエンドポイント`/graph`を作成する必要があります

@app.route("/graph")
def graph():
    graphInfo = Contact.computeGraphInfo(page=request.args.get('page', default=0, type=int))
    return render_template("graph.html", info=graphInfo)

ここでも、コントローラはまだ「薄い」ことに注意してください。モデルに委任してから、結果をビューに渡すだけです。

見落としがちですが、WebアプリケーションHTML APIに新しいエンドポイントを追加しましたが、* JSONデータAPIには追加していません*。したがって、UIのニーズによって完全に駆動されているこの(特殊な)エンドポイントが永遠に存在するということを、他のWeb以外のクライアントにコミットしていません。

このデータが`/graph`で永遠に利用可能になることを*すべて*のクライアントにコミットしていないため、HTMLベースのWebアプリケーションでアプリケーション状態のエンジンとしてのハイパーメディアを使用しているため、後でこのURLを削除またはリファクタリングできます。

おそらく、データベースの最適化によってグラフの計算が高速になり、`/contacts`への応答にインラインで含めることができるでしょう。このエンドポイントは他のクライアントに公開していないため、削除できます。Webアプリケーションをサポートするためだけにあります。

そのため、ハイパーメディアAPIに必要な柔軟性と、JSONデータAPIに必要な機能が得られます。

MVCに関して最も重要なことは、ドメインロジックがモデルに収集されているため、これらの2つのAPIを柔軟に変更しながら、かなりの量のコード再利用を実現できることです。はい、JSONコントローラとHTMLコントローラには最初は多くの類似点がありましたが、時間の経過とともに分岐しました。

同時に、モデルロジックを複製しませんでした。どちらのコントローラも比較的「薄い」ままで、ほとんどの作業をモデルオブジェクトに委任しました。

2つのAPIは分離されていますが、ドメインロジックは一元化されたままです。

(これはまた、私がコンテンツネゴシエーションを使用せず、同じエンドポイントからHTMLとJSONを返す傾向がある理由にも関係しています。)

#MVCフレームワーク

SpringASP.NET、Railsなどの多くの古いWebフレームワークは、この方法でロジックを非常に効果的に分割できる、非常に強力なMVC概念を備えています。

Djangoは、MVTと呼ばれるアイデアのバリエーションを持っています。

MVCに対するこの強力なサポートは、これらのフレームワークがhtmxと非常に相性が良く、これらのコミュニティがそれを高く評価している理由の1つです。

また、上記の例は明らかにオブジェクト指向プログラミングに偏っていますが、同じ考え方は関数型プログラミングにも適用できます。

#結論

MVCという概念を初めて知った方に、この概念をよく理解していただければ幸いです。また、Webアプリケーションでこの構成原則を採用することで、APIを効果的に分離し、同時にコードの重複を大幅に回避できることを示しています。

</>