MVCにおけるViewモデルの考察

CRUD(作成、読み取り、更新、削除)パターン

大抵の業務アプリの画面は、CRUD(作成、読み取り、更新、削除)パターンとなると思いますが、単純なマスタテーブルのメンテナンス画面とは異なりViewモデルそのものについて何がいいのか考えさせられることが多々あります。
ということで、アクションの立場に立ってCRUDの各処理を実行する際どのようなインターフェースが必要になるか整理してみます。

なお、なんのデータかは具体的ではないので、とある「案件のデータ」という表現をしていきます。

案件データの表示

GETメソッドで、クライアントは案件の識別子(更新時)またはパラメータ無し(新規作成時)でリクエストを行い、サーバーは案件のデータを読み取り結果をクライアントにレスポンスし表示します。レスポンスするデータ(Viewモデル)は、単純なマスタテーブルのメンテナンス画面の場合は、一つのテーブルのデータの場合もありますが、複数のテーブルのデータの表示だったり、コンボやラジオ、チェックボックスなどのコンテンツの選択可能データさえもViewモデルに格納する必要があります。

つまり、ここでサーバーのアクションメソッドは、

入力としては、案件の識別子(更新時は必須)
出力としては、画面を表示するために必要なデータと案件データを保持したViewモデル

というインターフェースを扱うこととなります。

案件データの作成・更新・削除に関してGETメソッドで画面が表示される場合も、クライアント側での入力可能かどうかの違いだけで、さほど大きなインターフェースの違いはないのかと思います。なので以下、画面表示のGETに関しては省略です。

案件データの作成・更新

クライアントは、POSTメソッドで案件の作成または更新されたデータをサーバーにリクエストします。実際にサーバー側が必要とするViewモデルの内容はPOSTされるデータのみがほとんどで、コンボやラジオ、チェックボックスなどのコンテンツをレンダリングするためのデータはViewモデルに必要ありません。

ここでサーバーのアクションメソッドは、

入力としては、作成または更新された案件のデータ
出力としては、
・次の画面を表示するために必要なViewモデル(他の画面にリダイレクトの場合は不要)
・検証エラー時に遷移元画面に戻る場合は画面を表示するために必要なViewモデル(案件データの表示と同じものだがデータは入力された値)
・更新確認画面などに遷移する場合は画面を表示するために必要なViewモデル(案件データの表示と同じものだがデータは入力された値)

というインターフェースを扱うこととなります。

案件データの削除

POST(いまだにDELETEの場合もあり?)で、案件データの識別子を送信すればよいだけです。

ここでサーバーのアクションメソッドは、

入力としては、案件の識別子
出力としては、
・次の画面を表示するために必要なViewモデル(他の画面にリダイレクトの場合は不要)
・検証エラー時の画面に遷移するならばその画面を描画するのに必要なデータ

というインターフェースを扱うこととなります。

一見、1つの案件データに対するCRUD処理ですが微妙に必要となるインターフェースに相違がありますね。

なんとなく、大は小を兼ねるViewモデルパターン

自分も気が付くと最終的にやっちゃっていることが多い「大は小を兼ねるViewモデルパターン」。これ、やっぱりだめパターンのような気がします。

前出のインターフェースで、先ず頭に浮かぶのは扱う案件データの構造に倣ったViewモデルですよね。で、当たり前のことなんですが、実装の大抵は「案件データの表示」画面から行っていきます。その際出来上がるViewモデルは、当然「案件データの構造に倣ったデータ」+「コンボやラジオ、チェックボックスなどその他のコンテンツを表示するためのデータ」を保持するものとなりますよね。んで、その次に「案件データの作成・更新」画面に取り掛かると...あれ?必要なデータは「案件データの構造に倣ったデータ」ってことは、さっきのViewモデルが使えるな?ってんで、このアクションのあるコントローラでやたら同じViewモデルを使い始める「大は小を兼ねるViewモデルパターン」が始まります。

こうすることでの大きな弊害は、

  • クライアントが何をPOSTしてくるのかアクションを見ただけでわからなくなります。
  • コンボやラジオ、チェックボックスなどその他のコンテンツを表示するためのデータと肝心な案件のデータの境界がわからなくなります。

「共通化だからいいんじゃない?」って声も上がりそうですが、これは共通化でもなんでもなくてやっぱりただの「大は小を兼ねるViewモデルパターン」です。

じゃぁどうすればいいの?大が小を使うViewモデルパターン

「案件データの作成・更新」時にPOSTしてくるのは、大概は「案件データの構造に倣ったデータ」だけでいいはずです。なので、先ずこのモデルを1つ目のViewモデルとします。これを「案件データの構造に倣ったデータViewモデル」としましょうか?

だけど、これだけだと「案件データの表示」ができません。なので、「案件データの構造に倣ったデータViewモデル」の派生クラスとして「案件データの表示Viewモデル」を作りましょう。ここは人それぞれなので、派生クラスではなくフィールドに「案件データの構造に倣ったデータViewモデル」を持ったクラスでもいいのかもしれません。が、データの境界をわかりやすくするには継承の方が良いのではないかと感じます。

そして、「案件データの表示Viewモデル」には、案件のデータ以外で画面を描画するのに必要な情報を追加していきます。

この二つのViewモデルを使って先ほど整理した内容を弄ってみましょう!

案件データの表示(読み取り)

入力としては、案件の識別子(更新時は必須)
出力としては、「案件データの表示Viewモデル」

案件データの作成・更新

入力としては、「案件データの構造に倣ったデータViewモデル」
出力としては、
・次の画面を表示するために必要なViewモデル(他の画面にリダイレクトの場合は不要)
・検証エラー時に遷移元画面に戻る場合は「案件データの表示Viewモデル」
・更新確認画面などに遷移する場合は「案件データの表示Viewモデル」

一つのデータに対するCRUD処理でViewモデルが2つになってしまいましたが、例外はあるとして、単純なCRUD処理であれば大体この2つのViewモデルがあればどうにかなりそうですね。とはいえ「大が小を使うViewモデルパターン」が決して必殺パターンではありません。昨今WEBアプリも画面がかなり複雑化してきており、実際の案件のデータ以外にUIコンテンツを描画するために必要なデータなどをViewモデルが抱え込むことになってきています(注1)また一つの画面で複数の構造化データを更新しなければならないケースもあります。非常に単純で実装しやすいMVCではありますが、「Scaffold」はあくまで「=足場づくり」。Viewモデルが一つだけとかにはとらわれず色々と考えてわかりやすい実装を試みてみたいものです(汗)

注1.MVCの中には完全にデータだけをクライアントに渡し、画面の遷移や画面描画をJavascriptで実装するアーキテクチャも存在します。ここでは、そのようなアーキテクチャではなくあくまで、ASP.NET CoreのMVC(クライアントにレスポンスするのは基本HTML)だけを利用した場合のお話です。