MVCモデルにおけるモデルとコントローラーの境界とその扱いを考える


こんにちは。 @MasashiFujiike です。
Makuake Product Team Advent Calendar 2018 19日目の記事です。

前説

僕個人はジシバリという小さな開発会社の代表をしていまして、Rails歴5年くらいの生粋のRailsエンジニアです。今はMakuakeの技術スタックの一つであるFuelPHPを主に扱っています。
いまは業務委託のパートナー企業として、Makuakeのプロダクト開発に関わらせていただいています。

さて、Makuakeでも新しい技術スタックとしてGoやVue.jsを取り入れておりまして、

などトレンドに乗った技術の記事も目新しいところです。

Makuakeは社内でのLT大会や勉強会ではマイクロサービスアーキテクチャやDDDなどの個別技術ではない包括的なアーキテクチャについての議論も盛んで、技術を追い求めたい、それを実際のプロダクトで試したいという気持ちがある開発者にとってとても魅力的な場だと感じています。

そんな最中ではありますが、MVCの特にscaffold系超高速ゼロイチ開発大好きエンジニアとして、今日は枯れた(?)技術となりつつあるMVCでよく議論に上がるモデルとコントローラーの境目について、考えていければと思っています。

なお、今回はWebMVC、とりわけRailsやFuelPHPなどのフレームワークに準拠した話をするつもりで、それ以外の事例(iOS/AndroidアプリにおけるMVCなどはまた特徴が異なるように思います)は扱いません。

disclaimer

一般的に、とよく書いていますが、「書き手が見てきた限りの平均値」ぐらいに見てください。
そして書き終わってみたら、コードが全然出てきませんでした・・・。

想い: MVC? 2018年だぜ?

MVCはトレンドとしてはクリーンアーキテクチャやマイクロサービス、DDDなどの開発手法も含めた新しい概念に押され、「使えて当たり前、それで作ってもすごいことない」と思われるフレームワークになりつつある、と近々感じることが増えました。

誰もが当たり前のようにMVCフレームワークでの開発経験をしていますし、RailsやCakePHPといったMVCフレームワークの代表ともいえるフレームワークたちもモダンとは呼ばれなくなりつつあります。(とはいえLaravelは身の周りでも引き続き評判を上げているように感じますが…)

ビルトインのORM重すぎ、意味不明なミドルウェアのせいでスケーリングの際にボトルネックになる、などの評判を見た時期もあり、僕は「本当か? フレームワークのせいにして逃げてないか?」と思うことがありました。

MVCは決してそれで作ったから完璧というものではありません。使うソフトウェアエンジニアの力量によって開発を支えもすれば崩しもします。

近年特にDDDやクリーンアーキテクチャの話を見るたび、これは別のものではない(もちろん異なるところは多いけれど)、共存しうる概念だと思うことが増え、それを踏まえてMVCを見直してみたくなりました。

そもそもDDDやクリーンアーキテクチャ、マイクロサービスの導入前の多くの場には、前のトレンドであったMVCフレームワークがいます。

過去のユーザー体験を支えてきたソフトウェアと向き合わずに、次のアーキテクチャやフレームワークに移ることはできません。であれば、今こそコンテキストの境界や、ドメインを正しく知るために、MVCを見直すべき時期なのではないかと思います。

この記事ではそんな想いを踏まえて、自分の経験則マシマシでMVCを捉え直してみた記事になります。

モデルとコントローラーの境目について

さて、本題に入りましょう。

一般的に、MVCフレームワークは、

  • モデル(M)
  • ビュー(V)
  • コントローラー(C)

の三要素にソフトウェアの構成要素をざっくりと分けて開発する考え方を基盤としています。

MVCの原点を探ってみたところ、1979年ぐらいに遡る考え方のようですね。(以下のブログにまとめていただいており、非常にわかりやすいです)
https://ledsun.hatenablog.com/entry/2013/11/12/115924

わかりやすいようで定義が曖昧でもあるので、この記事における僕の理解をまとめ直すと、MVCとはそれぞれ以下のようになります。

  • モデル
    • 取り扱うデータの構造(≒ DDDにおけるドメインモデル)と、それに紐づくビジネスロジックを管理する
      • MVCは一般に言われるDDDなどと比べてフレームワークが提供する技術制約に準拠することが多いですが、ターゲットとしている概念は非常に近いものである(のが良い)と感じています
  • ビュー
    • HTTPリクエストにおける、エンドポイントの見え方の構造を定義する
      • HTML/JSON/etc のケースがあり、単純な「見た目」と呼ばない方が良いかと思っています
  • コントローラー
    • ビューとモデルの媒介を行い、必要に応じて横断的なアプリケーションのビジネスロジックの呼び出しを定義する(※)
      • アプリケーションサービス層の存在を是とする考え方で、本稿の一つのテーマでもあります

こういった仮の定義を踏まえた上で、それぞれの役割を見ていきましょう。

正確な分類ができているか、というよりも、実際にこういうことをよく見る、という目線でまとめています。

一般的にモデルに含まれるもの

MVCにおけるモデル層では、多くのプロジェクトで以下の要素が含まれます。

  • モデルオブジェクトのCRUD機能
    • user.resign! のような拡張した意義を持つものも含みます
  • モデルオブジェクトの値のsetter/getter
    • user.full_address のようなものも含みます
  • 他のモデルとのアソシエーション
  • モデルに紐づいたクエリビルダー群
    • Railsでいうところの scope
  • モデルオブジェクトの値のバリデーション
  • CRUD + Validationのライフサイクルに紐づくコールバック処理の定義と呼び出し
  • 状態の判定を行うメソッド
    • user.confirmed? のような表現に代表されるメソッドです

一般的にコントローラーに含まれるもの

チェックや受け渡し、整頓、呼び出しといったことが多く行われます。

  • ビューからの入力値の整形とバリデーション
  • ビューからの入力値のセキュリティチェック(strong parameters)
  • その他、CSRF対策などのセキュリティチェック
  • ユーザー等の認証処理
  • ビュー層への認可状況の定義と受け渡し
  • モデル層のデータの呼び出し
  • モデル層で完結しない一連の処理の呼び出し
    • 複数モデルにまたがる、多くはトランザクションを必要とする処理
    • 通知などを伴う処理
    • など
  • 外部APIの呼び出し
  • ビュー層へ受け渡すための情報の整形
  • 状態に応じたリダイレクト等の管理

さて、だいぶ出揃いました。モデルとコントローラーという名前に、自分がいくつかのプロジェクトを振り返りながらまとめただけでもこれだけの責務が任されています。

余談: Fat Controllerや Fat Modelは MVCしか使っていなければいつか発生する

Rubyのlinterとして有名なrubocop (なお、 RuboCop is a Ruby static code analyzer and code formatter. と公式では言っています)では、デフォルトの目安として1クラスの最大行数を100行としているぐらいなので、上記の責務をすべて引き受けていたらすぐにFat ModelやFat Controllerの問題が発生します。

200行ぐらいがちょうどいいという噂は聞きますが、大きめのテーブルに紐づいたモデルだとすぐに超えてきますので、そもそもまともに上記の責務をカバーしようとするとモデル単体に収まりきるものではない(一般に期待されている行数では)ということなのだと思います。

問題が起きるとき

「このcontrollerもういじるのきついな・・・」
「このモデル一体どうなってるんだ・・・」
「なんでこんなことになってるんだ・・・」
「ここ共通化したところでなあ・・・」

ある日唐突に、自分が、あるいは誰かが口にしたり、思ったりするこのフレーズたち。
多くは実装中に深く考えずに書き込んだ処理や、実装時に考慮すべきだった別の事情によって然るべきでない場所に処理が記載されていることを発端として、そこに場当たり的に修正や仕様追加が行われてコードの香ばしさが増していき、その芳しさが閾値を超えたときに発されます。

こういった事態が起きるケースについて、自分なりに経験則も踏まえて例示してみます。

モデルとコントローラーの境目が曖昧になり、不健全になる状況

かなり具体的(かつ、コードではないので抽象的・・・)ですが、以下のようなケースが多く思い当たります。

上記の前提を踏まえて対応しているケースでも、以下のような混合は発生しうると思うのです。

  • コントローラー側でもモデル側でも通知を発行してしまう
    • これはどちらに責務を持たせたいかの設計・方針の話だと思いますが、一番まずいのは一貫性がないパターン
  • 複数の異なる特性を持ったモデルにまたがる処理をモデル内で完結させようとする
    • 決済とユーザーステータスの更新と通知の実行、などを決済モデルにすべてまとめるなど
  • 同じ意図のエラーハンドリングをコントローラー側でもモデル側でもおこなってしまう
  • モデルオブジェクトで完結できる状態判定をモデルオブジェクト以外で行う
    • 例えば if user.role == "administrator" のようなことをコントローラーで行う
  • モデルのsetter/getterやvalidatorをカスタマイズすればよい部分を、コントローラー側でおこなってしまう

不健全になる状況 番外編

  • モデル同士の依存関係が正しく管理されなくなる
    • モデル内部で別のモデルに依存した処理を作成した際に発生します
      • 依存の方向性が「親モデル → 子モデル」のように定式化されている場合には問題ないですが、これが意識されずに循環する状態になっていたり、親も子もお互いに呼び出しあってCRUDするようになってくると、リファクタリングするのすら苦労する状態が発生します

不健全性の引き金とその解決案

多くの場合、上記のような不健全性は「複数のモデルや外部サービスに紐づくビジネスロジック」をモデルかコントローラーのどこかに集約させようとすることによって発生します。

MVCモデルにおいて、この「複数のモデルや外部サービスに紐づくビジネスロジック」は、どこかに集約することは基本的に難しいものです。コントローラーの役目である、と切り分けることもできますが、例えば決済に関連する処理などは複数箇所から呼ばれうるため、中途半端にモデル層に寄せたりすると、やはり上記の問題が発生します。

複数箇所から呼ばれるものは、アプリケーションサービスという概念で括り出して(よくサービスクラスと呼ばれるものです)管理するのが、個人的には良いと思っていて、コントローラー層はそのアプリケーションサービスを呼び出す媒体としてのみ機能するようにすることで、上記の「モデルに含まれるもの」と「コントローラーに含まれるもの」の住み分けを維持したまま複雑なロジックを適用することができます。

最後に

文字多めでお送りしましたが、概念として自分なりに今まで向き合ってきたことの切り出しができたように思います。MVCがとっ散らかってきたな、と思ったら、何かの参考になれば嬉しいです。

また、株式会社マクアケは各種エンジニアを募集しております。興味を持っていただけた方がおられましたら、ぜひ「各種エンジニア募集! クラウドファンディングMakuake」からご連絡ください。

残すところ後わずかですが、「Makuake Product Team Advent Calendar 2018」は毎日更新で続きます!
明日は @mnuma さんです。よろしくお願いします。