MVC 架構:讓你的程式碼不再像義大利麵
如果你曾經接觸過軟體開發,可能或多或少聽過「MVC 架構」這個詞。也許是在面試時被問到、也許是在技術文件中看到、也許是同事隨口提起。但 MVC 到底是什麼?為什麼這麼多框架都在用它?
更重要的是:為什麼你的程式碼需要它?
如果你曾經打開三個月前自己寫的程式碼,卻完全看不懂在幹嘛;如果你改個畫面結果把整個邏輯搞爛;如果你的 HTML、資料庫查詢、商業邏輯全部擠在同一個檔案裡—那這篇文章就是為你寫的。
什麼是 MVC?三個臭皮匠的故事
想像你在經營一家餐廳。你有個廚師(Model)負責做菜和管理食材、一個服務生(View)負責端菜和跟客人互動、還有個經理(Controller)負責接單和協調兩邊。如果廚師直接跑出來跟客人收錢,服務生跑進廚房炒菜,那場面肯定一團混亂對吧?
這就是 MVC(Model-View-Controller)架構模式的核心概念:讓每個人做好自己的事,互不干擾。
MVC 是 1979 年由 Trygve Reenskaug 在 Xerox PARC 研究中心開發 Smalltalk-79 時發明的(來源)。當時他面對複雜的使用者介面,就想:「能不能把這坨程式碼分成幾塊,讓人看得懂?」於是 MVC 就誕生了。
三劍客各司其職
Model(資料管家)
負責處理資料和商業邏輯,就像餐廳廚師管理食材庫存和烹飪技術。
Model 的責任:
- 與資料庫互動(讀取、儲存、更新、刪除)
- 執行商業規則(例如:訂單金額必須大於 0)
- 資料驗證(例如:Email 格式是否正確)
- 不管 UI 長什麼樣
View(門面擔當)
負責呈現畫面給使用者看,就像服務生端上精美的餐點。
View 的責任:
- 顯示資料(HTML、JSON、PDF 等)
- 處理畫面排版和樣式
- 只管顯示,不管資料從哪來
- 不處理任何商業邏輯
Controller(交通警察)
接收使用者請求,決定要叫 Model 做什麼,再把結果交給 View 顯示。
Controller 的責任:
- 接收 HTTP 請求(GET、POST、PUT、DELETE)
- 決定要呼叫哪個 Model 方法
- 選擇用哪個 View 來顯示結果
- 處理路由和導向
這種關注點分離(Separation of Concerns)的設計,讓你可以改 UI 而不動資料庫邏輯,或是調整商業規則而不影響畫面—工程師的夢想成真!
為什麼需要 MVC?沒有它會怎樣?
沒有 MVC 的慘況
想像你寫了一個使用者註冊功能,把所有程式碼塞在一個檔案裡:
<?php
// 一坨義大利麵程式碼
if ($_POST['register']) {
$email = $_POST['email'];
// 驗證邏輯混在一起
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo "<p style='color:red'>Email 格式錯誤!</p>"; // HTML 混在 PHP 裡
exit;
}
// 資料庫邏輯混在一起
$conn = mysqli_connect("localhost", "user", "pass", "db");
$sql = "INSERT INTO users (email) VALUES ('$email')"; // SQL Injection 風險!
mysqli_query($conn, $sql);
// 又是 HTML 混在一起
echo "<html><body><h1>註冊成功!</h1></body></html>";
}
?>
問題爆炸:
- UI 設計師改不了畫面 - HTML 和 PHP 混在一起
- 安全性漏洞 - SQL Injection 隨便打
- 無法重複使用 - 要在其他地方註冊使用者?複製貼上整段 code
- 無法測試 - 怎麼測試這一坨?
- 三個月後看不懂 - 自己寫的都看不懂了
有 MVC 的優雅解法
同樣的功能,用 MVC 分層:
# Model (app/models/user.rb)
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end
# Controller (app/controllers/users_controller.rb)
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to success_path
else
render :new
end
end
end
# View (app/views/users/new.html.erb)
<%= form_with model: @user do |f| %>
<%= f.email_field :email %>
<%= f.submit "註冊" %>
<% end %>
好處立刻顯現:
- ✅ 設計師可以獨立修改 View
- ✅ 驗證邏輯寫在 Model,可重複使用
- ✅ Controller 超級乾淨,只負責協調
- ✅ 容易測試每個部分
- ✅ 三年後回來看還是清楚
MVC 的實際運作流程
假設使用者想看「所有已發布的文章」:
1. 使用者訪問 /posts
↓
2. Controller 接收請求
↓
3. Controller 問 Model:"給我所有已發布的文章"
↓
4. Model 去資料庫查詢
↓
5. Model 回傳資料給 Controller
↓
6. Controller 把資料交給 View
↓
7. View 產生 HTML
↓
8. 使用者看到漂亮的文章列表
整個流程中:
- Model 不需要知道畫面長怎樣
- View 不需要知道資料從哪來
- Controller 只負責當中間人
常見的 MVC 框架
MVC 不是特定框架專屬的,它是一種設計模式,被廣泛應用在各種語言和框架中:
語言 | 框架 | 特色 |
---|---|---|
Ruby | Ruby on Rails | Convention over Configuration,開發速度快 |
Python | Django | “Batteries included”,內建超多功能 |
PHP | Laravel | 優雅的語法,豐富的生態系 |
C# | ASP.NET MVC | 微軟官方支援,與 .NET 整合 |
Elixir | Phoenix | 高並發,即時通訊強項 |
Java | Spring MVC | 企業級應用標配 |
我自己常用的是 Ruby on Rails 和 Phoenix,它們都遵循 MVC 架構,但各有特色:
- Rails 適合快速開發 MVP,社群資源豐富
- Phoenix 適合需要高並發的即時應用(聊天室、線上遊戲等)
MVC 的變體:不只一種分層方式
MVC 的核心概念「分層管理」太好用了,後來衍生出很多變體:
MVVM (Model-View-ViewModel)
用在 Vue.js、Angular 等前端框架。ViewModel 負責處理 View 的狀態和邏輯。
MVP (Model-View-Presenter)
用在 Android 開發。Presenter 取代 Controller,更強調測試性。
Flux/Redux
React 生態系的單向資料流。雖然架構不同,但核心精神一樣:把邏輯分層管理。
它們的共同點?都在解決同一個問題:如何讓程式碼不會變成一團糾纏的義大利麵。
何時該用 MVC?
✅ 適合的場景
- 多人協作:前端設計師改 View,後端工程師改 Model,不會互相踩到
- 長期維護:三年後回來改程式碼,還能快速找到該改哪裡
- 複雜業務邏輯:當你的 if-else 開始超過三層,該考慮重構了
- 需要多種輸出格式:同樣的資料要輸出成 HTML、JSON、PDF?換 View 就好
❌ 不適合的場景
- 超簡單的網頁:如果只是一個靜態頁面,用 MVC 反而太重
- 極致效能需求:分層會帶來一些額外開銷(但通常可以忽略)
- 原型快速測試:先把功能做出來,之後再重構成 MVC 也可以
MVC 的常見誤區
誤區 1:Model 只是資料庫的對應
❌ 錯誤理解: Model = 資料庫的表格 ✅ 正確理解: Model = 商業邏輯 + 資料存取
例如「計算訂單總金額」的邏輯應該寫在 Model,不是 Controller。
誤區 2:Controller 可以寫商業邏輯
❌ 錯誤: 在 Controller 寫一堆 if-else 判斷 ✅ 正確: Controller 保持輕薄,複雜邏輯移到 Model
Controller 應該像交通警察,只負責指揮,不負責開車。
誤區 3:View 完全不能有邏輯
❌ 極端: View 連 for 迴圈都不能寫 ✅ 合理: View 可以有顯示邏輯(迴圈、條件顯示),但不能有商業邏輯
顯示邏輯:「如果是 VIP 會員,顯示金色徽章」- OK 商業邏輯:「計算 VIP 折扣價格」- 不 OK,應該在 Model
實戰技巧:讓 MVC 更好用
技巧 1:Fat Model, Skinny Controller
把邏輯寫在 Model,Controller 只做協調。
# ❌ 不好:邏輯塞在 Controller
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.total = @order.items.sum(&:price) * 0.9 # 折扣計算
@order.save
end
end
# ✅ 好:邏輯放在 Model
class Order < ApplicationRecord
before_save :calculate_total
def calculate_total
self.total = items.sum(&:price) * discount_rate
end
end
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.save
end
end
技巧 2:使用 Partial View 避免重複
如果多個頁面都要顯示「使用者卡片」,抽成 Partial:
<!-- _user_card.html.erb -->
<div class="user-card">
<h3><%= user.name %></h3>
<p><%= user.email %></p>
</div>
<!-- 在其他 View 中重複使用 -->
<%= render 'user_card', user: @user %>
技巧 3:用 Service Object 處理跨 Model 邏輯
如果邏輯涉及多個 Model,別硬塞在其中一個 Model,用 Service Object:
# app/services/order_checkout_service.rb
class OrderCheckoutService
def initialize(order, payment_method)
@order = order
@payment_method = payment_method
end
def call
ActiveRecord::Transaction do
@order.process!
@payment_method.charge!(@order.total)
NotificationMailer.order_confirmed(@order).deliver_later
end
end
end
# Controller 超級乾淨
class OrdersController < ApplicationController
def checkout
service = OrderCheckoutService.new(@order, params[:payment_method])
service.call
end
end
結語
下次當你的程式碼變成義大利麵時,想想 MVC 這三個好朋友:
- 讓 Model 管資料和商業邏輯
- 讓 View 管畫面顯示
- 讓 Controller 當交通警察
你的未來自己(還有接手的同事)會感謝你的!
MVC 從 1979 年誕生以來,已經 40 多年了。雖然出現了很多新的架構模式,但 MVC 的核心精神—關注點分離—依然是軟體設計的基石。
記住:好的架構不是讓程式碼變複雜,而是讓複雜的問題變簡單。