Webアプリ設計パターン|MVCとMVVMを理解して脱スパゲッティコード

目次

3000行のindex.phpと戦ったあの日から、今の設計論へ

正直に告白します。私がエンジニアになりたての頃(もう15年くらい前ですね)、設計パターンなんて言葉は「意識高い系の戯言」だと思っていました。

当時、私が書いていたコードは酷いものでした。
一つの index.php というファイルの中に、データベースへの接続情報、SQLクエリ、ビジネスロジック、そしてHTMLの出力まで全部詰め込んでいたんです。「動けば正義」だと思っていました。

でもある日、クライアントから「会員登録のフローを少し変えたい」と言われた瞬間、地獄を見ました。
どこを直せばいいのかわからない。一箇所直すと、全く関係ないはずの商品一覧ページが真っ白になる。変数名 $data1 $data2 が飛び交い、自分が先週書いたコードの解読に3日かかる…。

いわゆる「スパゲッティコード」の完成です。

深夜のオフィスでコーヒーを啜りながら、「なんでこんなに苦しいんだ?」と頭を抱えていた時に出会ったのが、「MVC」という設計パターンでした。そして時代が流れ、フロントエンドが複雑化した現在、私らの武器は「MVVM」へと進化・派生しています。

この記事では、かつての私のような「コードは書けるけど設計がわからない」「どこに何を書けばいいのか迷う」という人に向けて、Webアプリ設計の要であるMVCとMVVMについて、教科書的な定義だけでなく、現場の泥臭い経験則を交えて徹底的に解説します。

これを読み終える頃には、あなたの頭の中にある「コードの地図」が、驚くほどクリアになっているはずです。


なぜ、私たちは「設計パターン」を学ぶ必要があるのか?

具体的なMVCやMVVMの話に入る前に、もう少しだけ前提の話をさせてください。
なぜ、面倒なルール(設計パターン)に従ってコードを書く必要があるのでしょうか?

「関心ごとの分離」が全てを解決する

プログラミングには「Separation of Concerns(関心ごとの分離)」という大原則があります。
難しそうに聞こえますが、要は「混ぜるな危険」ということです。

私の失敗談で言うと:

  • データを取る処理(SQL)
  • データを計算する処理(ロジック)
  • 画面を表示する処理(HTML)

これらが混ざっていると、以下の問題が起きます。

  1. 可読性が死ぬ:HTMLのタグの中に複雑な計算式が埋め込まれていて、デザインの修正すら怖くてできない。
  2. 再利用できない:「この計算処理、他のページでも使いたい」と思っても、HTMLと癒着していて切り出せない。
  3. テストが書けない:画面を通さないとロジックの正しさを証明できず、自動テストが導入できない。

設計パターンとは、この「関心ごと」を綺麗に切り分けるための「先人たちが発明した整理整頓のルール」です。

散らかった部屋(スパゲッティコード)と整理整頓された部屋(MVC)を対比して悩んでいるエンジニアの線画イラスト

エンジニアとしてレベルアップするためには、言語の文法を覚えた次は、間違いなくこの「設計」を学ぶべきです。ここが、プロとアマチュアの分水嶺になります。


Web開発の王道にして原点「MVC」

まずは、Webアプリケーションフレームワーク(Ruby on Rails, Laravel, Djangoなど)の基本となっている MVC(Model-View-Controller) から見ていきましょう。

MVCの歴史と基本概念

MVCは、実はWebが生まれるずっと前、1979年にSmalltalkという言語のために考案されました。それがWebの仕組み(リクエスト&レスポンス)と非常に相性が良かったため、爆発的に普及しました。

役割分担は以下の3つです。

  1. Model(モデル):ビジネスロジック、データ処理。
  2. View(ビュー):表示、ユーザーインターフェース。
  3. Controller(コントローラー):司令塔。ModelとViewの橋渡し。

よくある例え話ですが、「レストラン」で考えるとわかりやすいです。

  • View = ウェイター(客にメニューを見せ、料理を運ぶ)
  • Controller = マネージャー(オーダーを受け、キッチンに指示を出し、出来上がった料理をウェイターに渡す)
  • Model = キッチン/シェフ(実際に料理を作る、食材=データを管理する)

客(ユーザー)は、キッチン(Model)の中身を知る必要はありませんし、ウェイター(View)が料理を作ることもありません。それぞれの役割が明確だから、店は回るのです。

コードで見るMVC(サーバーサイドの例)

概念だけだと眠くなるので、実際のコードイメージを見てみましょう。
「ユーザー一覧を表示する」という機能を、MVCを使って書くとこうなります(言語はPHPっぽい疑似コードです)。

Model (User.php)

ここは「データ」と「ルール」の番人です。

class User {
    // データベースから全ユーザーを取得する
    public function getAll() {
        return DB::query("SELECT * FROM users");
    }

    // 成人かどうか判定する(ビジネスロジック)
    public function isAdult($age) {
        return $age >= 20;
    }
}

Controller (UserController.php)

ここは「司令塔」です。ユーザーからのアクセスを受け取ります。

class UserController {
    public function index() {
        // 1. Modelに「データ取ってきて」と指示
        $userModel = new User();
        $users = $userModel->getAll();

        // 2. 必要ならデータを加工して、Viewに渡す準備
        $data = ['users' => $users];

        // 3. View(テンプレート)を呼び出して表示
        return View::make('user_list', $data);
    }
}

View (user_list.html)

ここは「表示」だけを行います。難しい計算はしません。

<ul>
    <!-- Controllerから渡されたデータ($users)を表示するだけ -->
    @foreach ($users as $user)
        <li>
            {{ $user->name }}
            @if ($user->isAdult()) (成人) @endif
        </li>
    @endforeach
</ul>

どうでしょう?
役割がパキッと分かれていますよね。「デザインを変えたい」ならViewだけ触ればいいし、「データの取得条件を変えたい」ならModelだけ触ればいい。これがMVCの威力です。

MVCが抱える「Fat Controller」問題

しかし、現実はそう甘くありません。開発現場で初心者が必ず陥る罠があります。それが「Fat Controller(太ったコントローラー)」問題です。

「Controllerは司令塔だから、あいつもこいつも呼び出して…」と考えているうちに、Controllerの中にif文やループ、複雑なデータ加工処理を全部書いてしまうのです。

// 悪い例:Fat Controller
class UserController {
    public function store($request) {
        // ここでバリデーションして…
        if ($request->name == '') { ... }

        // ここでメール送信の準備をして…
        $mail = new Mail();
        // ここでCSVの出力を生成して…
        // ここで外部API叩いて…

        // ↑全部Controllerに書くな!
    }
}

これでは、実質的に昔のスパゲッティコードと変わりません。
私も新人の頃、先輩に「Controllerは薄く保て! ロジックはModel(またはService層)に逃がせ!」と耳にタコができるほど言われました。

「Controllerは、リクエストを受け取って、適切な人に仕事を振って、結果を返すだけ」
これ、テストに出ます。覚えておいてください。

Fat Controllerになったコードを見て絶望するエンジニアと、それを諭すメンターの線画イラスト

フロントエンドの台頭と「MVVM」の登場

さて、Webの歴史は進みます。
jQuery全盛期を経て、React, Vue.js, Angularといった「モダンフロントエンドフレームワーク」が登場しました。

ここで問題になったのが、「画面(UI)の状態管理が複雑すぎる」ことです。

従来のMVC(サーバーサイド)は、ページを丸ごと読み込み直すのが基本でした。しかし、今のWebアプリ(SPA: Single Page Application)は、ページ遷移せずにボタン一つで画面の一部だけが変わったり、データがリアルタイムに更新されたりします。

これをjQueryでやろうとすると、「ボタンAが押されたら、DOMのID要素Bを探して、そのテキストを書き換えて、クラスCを追加して…」という、DOM操作の地獄が待っています。

そこで脚光を浴びたのが、MVVM(Model-View-ViewModel)です。

MVVMの正体と「データバインディング」

MVVMは、主にGUI(画面)を持つアプリケーションのために作られたパターンです。
構成要素は以下の3つ。

  1. Model:データとロジック(MVCと同じ)。
  2. View:画面の見た目(HTML/CSS)。
  3. ViewModelViewのためのModel。ViewとModelの仲介役。

これだけ見ると「ControllerがViewModelに名前変わっただけ?」と思うかもしれませんが、決定的な違いがあります。
それが「データバインディング(Data Binding)」です。

データバインディングという魔法

MVVMでは、ViewとViewModelが自動的に同期します。

  • ViewModelのデータが変わる → 勝手にView(画面)が書き換わる。
  • View(入力フォーム)の値が変わる → 勝手にViewModelのデータが書き換わる。

つまり、エンジニアは「DOM操作」を書く必要がないんです。ただ「データ」を更新すればいいだけ。これは革命でした。

コードで見るMVVM(Vue.jsの例)

Vue.jsはMVVMの影響を強く受けているので、例として最適です。
「検索ボックスに入力した文字で、リストをリアルタイムに絞り込む」機能を見てみましょう。

View (HTML)

<div id="app">
    <!-- v-modelでViewModelのsearchTextと双方向バインド -->
    <input type="text" v-model="searchText" placeholder="検索...">

    <ul>
        <!-- filteredListが更新されると、自動で画面も書き換わる -->
        <li v-for="user in filteredList">
            {{ user.name }}
        </li>
    </ul>
</div>

ViewModel (JavaScript/Vue)

const app = Vue.createApp({
    data() {
        return {
            // Viewの状態を保持する変数
            searchText: '',
            users: [
                { name: '佐藤' },
                { name: '鈴木' },
                { name: '田中' }
            ]
        }
    },
    computed: {
        // データが変わると自動的に再計算されるロジック
        filteredList() {
            return this.users.filter(user => 
                user.name.includes(this.searchText)
            );
        }
    }
});

見てください。
document.getElementById」も「innerHTML = ...」も一行もありません。
ただ searchText という変数が変われば、自動的に filteredList が再計算され、画面のリストが書き換わります。

これがMVVMの「宣言的UI」という考え方です。「どうやって書き換えるか(How)」ではなく、「どんな状態であるべきか(What)」を定義するのです。

手作業でDOMを書き換えるjQuery時代と、自動更新されるMVVM時代の対比イメージの線画イラスト

結局、MVCとMVVMはどう使い分けるのか?

ここまで読んで、「で、結局どっちを使えばいいの?」と思ったあなた。
答えは、「適材適所だし、今は両方使うことが多い」です。

現代のWeb開発における「ハイブリッド構成」

今のWeb開発現場では、以下のような構成が一般的です。

  1. バックエンド(サーバーサイド)MVC
    • 言語:PHP(Laravel), Ruby(Rails), Python(Django)など
    • 役割:APIとしてJSONデータを返す。データベース操作、セキュリティ、認証などを担当。
    • ここでは伝統的なMVCパターンががっつり使われます。
  2. フロントエンド(クライアントサイド)MVVM(的なコンポーネント指向)
    • 言語:JavaScript/TypeScript(Vue.js, React)
    • 役割:APIからデータを受け取り、リッチなUIを表示する。
    • ここではMVVM、あるいはそれに近い設計思想が使われます。

つまり、「MVC vs MVVM」と対立させて選ぶものではなく、「バックエンドはMVCで堅牢に作り、フロントエンドはMVVMで柔軟に作る」というのが、現代の最適解の一つなんです。

MVCを選ぶべきケース

  • シンプルなブログやコーポレートサイト。
  • SEO最優先で、初期表示速度を上げたい(サーバーサイドレンダリング)。
  • フロントエンドに複雑な動きが不要。
  • チームメンバーがサーバーサイドエンジニア中心。

MVVM(SPA)を選ぶべきケース

  • ダッシュボードや管理画面など、操作性が重要なアプリ。
  • ネイティブアプリ(スマホアプリ)のようなサクサクした体験が必要。
  • 画面遷移させずにデータを次々と更新したい。
  • フロントエンドエンジニアとバックエンドエンジニアが分業している。

現場で直面する「設計の落とし穴」と対策

ここからは、私がメンターとして多くの初心者のコードを見てきて感じた、現場でありがちな「設計の失敗パターン」とその回避策をシェアします。

1. Modelにロジックを書かず、全部Controller/ViewModelに書く

これ、一番多いです。
「Modelはデータベースと通信するだけの箱」だと思っている人が多い。

例えば、「ユーザー登録時に、年齢が20歳未満ならエラーを出し、かつ親の同意フラグをチェックし、問題なければDBに保存し、ウェルカムメールを送る」という処理。

これをController(またはViewModel)にダラダラ書くと、修正が大変になります。
「ビジネスロジックはModel(または専用のDomain Serviceクラス)に書く」
Controllerはあくまで「Modelのメソッドを呼ぶだけ」にしてください。

悪い例:

// ViewModelの中で計算しちゃってる
if (user.age < 20 && !user.hasParentConsent) {
    alert('エラーです');
}

良い例:

// Model(Userクラス)にメソッドを持たせて、ViewModelはそれを呼ぶだけ
if (!user.canRegister()) {
    alert('エラーです');
}

こうすれば、「登録条件が変わった」時に、あちこちの画面(ViewModel)を修正せず、Model一箇所を直すだけで済みます。

2. Viewにロジックが漏れ出す

HTMLの中に複雑な条件分岐を書きすぎるパターンです。
LaravelのBladeテンプレートや、Vueのテンプレート内で、if 文が5重ネストになっていたり…。

Viewは「馬鹿」であれ。これは褒め言葉です。
Viewは渡された変数を「表示するだけ」の状態が理想です。条件分岐などの判断が必要なら、それはControllerやViewModel、あるいはPresenterと呼ばれる層で処理して、Viewには isShown みたいな単純なフラグだけを渡すべきです。

複雑なロジックが入り組んだHTMLを見て頭を抱えるデザイナーとエンジニアの線画イラスト

3. 「なんでもViewModel」病

MVVMを使っていると、ViewModelが巨大化しがちです。
API通信の処理、データの加工、バリデーション、画面の制御フラグ…全部ViewModelに入れると、3000行のファイルが出来上がります。

これもMVCのFat Controllerと同じ。

  • API通信は「Repository層」や「API Client層」に切り出す。
  • 複雑なデータ加工は「Model」や「Helper」に切り出す。
  • 状態管理が複雑なら、後述するStoreパターン(Vuex/Redux/Pinia)を使う。

ファイルを分けることを恐れないでください。「1ファイル100行」を超えたら、分割の合図だと思った方がいいです。


MVC/MVVMの「その先」へ:FluxとClean Architecture

設計の世界は奥が深いです。MVCやMVVMを理解したあなたが、次に遭遇するであろう概念を少しだけ紹介しておきます。

ReactとFlux/Redux

Reactは公式には「MVCのV(View)である」と言われたりしますが、実際はコンポーネント指向です。
アプリが大規模になると、MVVMの「双方向バインディング」だと、データの流れが追いづらくなることがあります(どこでデータが変わったかわからない!)。

そこで生まれたのがFlux(フラックス)というアーキテクチャ、そしてその実装であるReduxです。
特徴は「データフローが一方通行(単方向)」であること。

Action → Dispatcher → Store → View
という一方通行の流れを強制することで、データの変化を予測しやすくします。初心者は最初「コード量が増えて面倒くさい!」と思いますが、大規模開発ではこの「堅苦しさ」が救いになります。

Clean Architecture(クリーンアーキテクチャ)

MVCの「Model」をもっと細かく整理しよう、という考え方の到達点の一つです。
ビジネスロジック(ドメイン)を中心に置き、UIやデータベースなどの「詳細」を外側に配置する同心円状の図を見たことがあるかもしれません。

これは「フレームワークやDBが変わっても、ビジネスロジックは影響を受けないようにする」という強力な設計思想ですが、小規模なアプリでやるとオーバーエンジニアリング(やりすぎ)になります。

まずはMVC/MVVMをしっかり使いこなせるようになってから、必要に応じて学んでいけば大丈夫です。

階段を登るように、MVCから始めてClean Architectureへ進むイメージの線画イラスト

よくある質問(FAQ)

私がメンターをしていて、生徒さんからよく聞かれる質問をまとめました。

Q1: ReactはMVCですか?MVVMですか?

A: どちらかと言えばMVVMに近いですが、独自の「コンポーネント指向」です。
厳密な定義論争はあまり意味がありません。ただ、React hooks(useStateなど)を使って状態(State)管理し、それがUIに反映される仕組みは、MVVMの「ViewModel」の役割に近いと言えます。
大事なのは分類することより、「状態(State)とUI(View)をどう同期させるか」というReactのお作法を理解することです。

Q2: 独学で設計パターンを身につけるには?

A: 「わざと失敗してリファクタリングする」のが一番です。
最初から完璧なMVCで書こうとすると手が止まります。
おすすめは、

  1. まず動くものを作る(汚くてもいい)。
  2. 「機能を追加しにくいな」「コードが読みづらいな」と感じたら、MVCの原則に従ってコードを移動させてみる。
  3. 「お、スッキリした!」という快感を覚える。

この繰り返しが一番身につきます。他人の書いた綺麗なコード(OSSなど)を読むのも勉強になりますが、自分で痛みを経験しないと本当の意味での理解は難しいです。

Q3: フレームワークを使えば勝手に良い設計になりますか?

A: 残念ながら、なりません。
RailsやLaravelを使えば、強制的にMVCのフォルダ構成にはなります。しかし、前述した「Fat Controller」のように、枠組みだけMVCで、中身はスパゲッティということは往々にしてあります。
フレームワークはあくまで「枠」です。その枠の中にどうコードを配置するかは、あなたの設計スキルにかかっています。

Q4: 設計に正解はありますか?

A: ありません。「今のチームとプロジェクトにおける最適解」があるだけです。
2週間の寿命しかないキャンペーンサイトに、重厚なClean Architectureを導入するのは間違いです(開発スピードが落ちるから)。逆に、5年運用する基幹システムを、設計なしで作るのは自殺行為です。
「メンテナンス性」「開発速度」「学習コスト」のバランスを見て、設計を選ぶのがシニアエンジニアの役割です。


まとめ:設計パターンはエンジニア同士の「共通言語」

朝日を見ながら、綺麗なコードが書かれたPC画面を見て満足げなエンジニアの線画イラスト

長くなりましたが、最後に伝えたいことがあります。

MVCやMVVMといった設計パターンを学ぶ最大のメリット。それはコードが綺麗になること以上に、「チームメンバーと会話ができるようになること」です。

「ここのロジック、複雑だね」と言うより、
「ここ、Modelに切り出そうか」と言った方が、意図が一発で伝わります。
「画面の更新がうまくいかない」と言うより、
「データバインディングが効いてないみたい」と言えば、原因の切り分けが早くなります。

設計パターンは、エンジニア同士の共通言語(プロトコル)なんです。

もしあなたが今、「コードは書けるけど、なんか自信がない」と思っているなら、ぜひ一度、自分が使っているフレームワークの設計思想を深く掘り下げてみてください。
「なぜこのファイルはここにあるのか?」「なぜViewからDBを叩いてはいけないのか?」

その「なぜ」を一つずつ紐解いていった先に、スパゲッティコードから解放された、自由で楽しい開発の世界が待っています。

私もまだまだ勉強中です。一緒に、より良いコードを目指して走り続けましょう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いたエンジニア

金城 美咲のアバター 金城 美咲 フロントエンドエンジニア

フロントエンド領域に強く、UIの細部にまでこだわるデザイナー気質のエンジニア。React・Vueを扱い、モダン開発に柔軟に対応。人当たりがよく、相談しやすい雰囲気を持つ。休日はカフェ巡りやガジェット研究で新しい刺激を得ることが多い。

目次