Model-View-Controller
แยก app เป็นสามส่วน: Model, View, Controller
เมื่อสร้าง app ที่มี user interface code มักพันกันสามเรื่องไปพร้อมกัน: การเก็บและประมวลผลข้อมูล (business logic), การแสดงผลข้อมูลนั้นให้ผู้ใช้เห็น (presentation) และการรับ input จากผู้ใช้แล้วตัดสินใจว่าจะทำอะไรต่อ (control flow) ถ้าเขียนทั้งสามอย่างปนกันใน file เดียว — เช่น code-behind ของฟอร์มที่ทั้ง query ฐานข้อมูล คำนวณ validation และจัดการ layout ในตัวเดียวกัน — ผลลัพธ์คือ code ที่ทดสอบยาก เปลี่ยน UI ไม่ได้โดยไม่กระทบ logic และ reuse logic ข้าม UI คนละแบบไม่ได้เลย
Model-View-Controller (MVC) แก้ปัญหานี้ด้วยการแยกความรับผิดชอบออกเป็นสามส่วนที่ประสานงานกัน:
- Model — สถานะและกฎทางธุรกิจของ app ไม่รู้จัก UI เลย
- View — รับผิดชอบการแสดงผล (rendering) ของข้อมูลจาก Model เท่านั้น ไม่มี logic การตัดสินใจ
- Controller — รับ input จากผู้ใช้ แปลเป็นคำสั่งที่ทำกับ Model แล้วเลือกว่าจะให้ View ไหนแสดงผลลัพธ์
แนวคิดหลักเบื้องหลังคือ separation of concerns: แต่ละชั้นเปลี่ยนแปลงได้อิสระจากกัน — เปลี่ยนหน้าตา UI โดยไม่แตะ business logic, เปลี่ยนกฎธุรกิจโดยไม่ต้องแก้ทุกหน้าจอ, และทดสอบ Model ได้โดยไม่ต้อง render UI จริง
MVC ถือกำเนิดจาก Trygve Reenskaug ที่ Xerox PARC ในปี 1979 สำหรับ Smalltalk-80 และภายหลังแพร่หลายอย่างมากบนเว็บผ่าน framework อย่าง Ruby on Rails, Django และ ASP.NET MVC จนกลายเป็นหนึ่งใน pattern ที่ถูกอ้างถึงมากที่สุด — และก็ถูกเข้าใจผิดมากที่สุดเช่นกัน (Martin Fowler เรียกมันว่า “the most quoted (and most misquoted) patterns around”) เพราะแต่ละ framework ตีความ “MVC” ต่างกันไปตามบริบทของตัวเอง
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Controller {
+HandleRequest()
+SelectView()
}
class Model {
-state
+UpdateState()
+GetState()
}
class View {
+Render()
}
class User
User --> Controller : input
Controller --> Model : updates and reads
Controller --> View : selects and passes model
View --> Model : reads
View --> User : render output
สามบทบาทในแผนภาพนี้ไม่ผูกติดกับ class เดียว — ใน ASP.NET Core MVC, Controller มักเป็น class ที่สืบทอดจาก Controller, Model อาจเป็นทั้ง domain entity, DTO หรือ ViewModel ที่ประกอบมาเพื่อการแสดงผลโดยเฉพาะ ส่วน View คือ template (เช่น Razor .cshtml) ที่รับ model แล้ว render เป็น HTML
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานทั่วไปของ MVC บนเว็บมีดังนี้:
- ผู้ใช้ส่ง request (คลิก link, submit ฟอร์ม) เข้ามาที่ app
- router จับคู่ request กับ Controller action ที่เหมาะสม
- Controller อ่าน input, เรียก Model (ผ่าน repository/service) เพื่ออ่านหรืออัปเดตสถานะ — Controller ไม่ทำ business logic เอง แค่ประสานงาน
- Controller เลือก View ที่จะ render และส่ง Model (หรือ ViewModel) ให้
- View อ่านข้อมูลจาก Model แล้วสร้างผลลัพธ์ (HTML) ส่งกลับให้ผู้ใช้
- ถ้า Model เปลี่ยนแปลงจากที่อื่น (เช่นในระบบ desktop แบบดั้งเดิม) View มักสมัคร observer กับ Model เพื่ออัปเดตอัตโนมัติ — นี่คือส่วนที่ Fowler เน้นว่า MVC แท้ ๆ พึ่งพา observer pattern ไม่ใช่แค่การเรียก function ทางเดียว
sequenceDiagram
participant U as User
participant C as Controller
participant M as Model
participant V as View
U->>C: HTTP request
C->>M: query or command
M-->>C: state
C->>V: select view and pass model
V->>M: read data to render
V-->>U: HTML response
ข้อสังเกตสำคัญ: ในเว็บ app สมัยใหม่แบบ request-response (เช่น ASP.NET Core MVC) วงจร observer แบบดั้งเดิมมักถูกตัดออก เพราะ View render ครั้งเดียวแล้วจบ request — ไม่มีการอัปเดตแบบ real-time เหมือน MVC บน desktop ดั้งเดิม สิ่งที่เหลืออยู่คือหลักการแยก concern สามส่วน ซึ่งเป็นคุณค่าหลักที่ app เว็บยุคนี้นำมาใช้
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่าง ASP.NET Core MVC ที่แสดงระบบดูรายละเอียดคำสั่งซื้อ (order) และแก้ไขที่อยู่จัดส่ง — Controller ประสานงานระหว่าง Model กับ View โดยไม่มี presentation logic หรือ business rule ปนอยู่ในตัวมันเอง
// Model — domain entity ไม่รู้จัก UI เลยpublic class Order{ public int Id { get; private set; } public string CustomerName { get; private set; } public Address ShippingAddress { get; private set; } public decimal Total { get; private set; } public bool IsShipped { get; private set; }
public void UpdateShippingAddress(Address newAddress) { // กฎธุรกิจ: ห้ามเปลี่ยนที่อยู่หลังจากจัดส่งไปแล้ว if (IsShipped) throw new InvalidOperationException("คำสั่งซื้อนี้จัดส่งไปแล้ว ไม่สามารถแก้ไขที่อยู่ได้");
ShippingAddress = newAddress; }}
// ViewModel — รูปทรงข้อมูลเฉพาะสำหรับหน้าจอนี้ ไม่ใช่ domain entity ตรง ๆpublic class OrderDetailsViewModel{ public int Id { get; set; } public string CustomerName { get; set; } public string ShippingAddressLine { get; set; } public decimal Total { get; set; }}
// Controller — รับ input, เรียก Model, เลือก Viewpublic class OrdersController : Controller{ private readonly IOrderRepository _repo;
public OrdersController(IOrderRepository repo) => _repo = repo;
// GET /orders/5 [HttpGet("orders/{id}")] public IActionResult Details(int id) { var order = _repo.GetById(id); // อ่านจาก Model if (order is null) return NotFound();
var viewModel = new OrderDetailsViewModel // ประกอบ ViewModel สำหรับ View { Id = order.Id, CustomerName = order.CustomerName, ShippingAddressLine = order.ShippingAddress.ToString(), Total = order.Total, };
return View(viewModel); // เลือก View + ส่งข้อมูลให้ }
// POST /orders/5/address [HttpPost("orders/{id}/address")] public IActionResult UpdateAddress(int id, AddressInput input) { var order = _repo.GetById(id); if (order is null) return NotFound();
try { order.UpdateShippingAddress(input.ToAddress()); // business logic อยู่ใน Model _repo.Save(order); } catch (InvalidOperationException ex) { ModelState.AddModelError(string.Empty, ex.Message); var viewModel = new OrderDetailsViewModel // View ต้องการ ViewModel ชนิดเดียวกับ Details เสมอ { Id = order.Id, CustomerName = order.CustomerName, ShippingAddressLine = order.ShippingAddress.ToString(), Total = order.Total, }; return View("Details", viewModel); // กลับไป View เดิมพร้อม error }
return RedirectToAction(nameof(Details), new { id }); }}@* View — Details.cshtml: มีหน้าที่ render อย่างเดียว ไม่มี logic การตัดสินใจ *@@model OrderDetailsViewModel
<h1>คำสั่งซื้อ #@Model.Id</h1><p>ลูกค้า: @Model.CustomerName</p><p>ที่อยู่จัดส่ง: @Model.ShippingAddressLine</p><p>ยอดรวม: @Model.Total.ToString("C")</p>เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- app ที่มีหลายหน้าจอ (view) แสดงข้อมูลชุดเดียวกันในรูปแบบต่างกัน — แยก Model ออกมาให้ reuse ได้
- ทีมที่อยากทดสอบ business logic ด้วย unit test โดยไม่ต้อง spin up UI framework
- เว็บ framework ฝั่ง server ที่ต้อง map URL → action → response (ASP.NET Core MVC, Rails, Django, Spring MVC)
- ทีมที่ต้องการให้ frontend/backend หรือ presentation/domain แยกทีมกันพัฒนาได้คู่ขนาน
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- UI แบบ data-binding เข้มข้น (desktop/mobile ที่ต้อง sync สถานะสองทางบ่อย ๆ) — MVVM มักเหมาะกว่าเพราะมี binding layer ในตัว
- app เล็กมาก หรือ endpoint ที่ทำงานเดียว (เช่น API endpoint คืน JSON ล้วน) — MVC เต็มรูปแบบอาจเกิน needs ลอง minimal API หรือ REPR แทน
- เมื่อทีมยังไม่เข้าใจ separation of concerns ดีพอ เพราะ MVC ที่ทำผิด (เช่น ใส่ query database ใน View, ใส่ business rule ใน Controller) จะแย่กว่าไม่มี pattern เลย — กลายเป็น anti-pattern ที่ชื่อเหมือน MVC แต่พฤติกรรมไม่ใช่
- ระบบที่ต้องการ testability ของ presentation logic เองสูงมาก (ไม่ใช่แค่ domain) — MVP แยก presenter ออกมาให้ mock View ได้ง่ายกว่า
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | Separation of concerns ชัดเจน — เปลี่ยน UI ไม่กระทบ domain logic |
| ข้อดี | Model ทดสอบได้อิสระจาก UI framework, เขียน unit test ได้เร็วและเสถียร |
| ข้อดี | Controller บาง ทำให้ business logic ไม่กระจายอยู่หลายที่ ลด duplicate |
| ข้อดี | รองรับหลาย View ต่อ Model เดียว (เช่น เว็บ + API + mobile) |
| ข้อเสีย | ถ้าไม่มีวินัย Controller มักบวมกลายเป็น “fat controller” ที่แอบมี business logic |
| ข้อเสีย | คำว่า “MVC” ถูกตีความต่างกันในแต่ละ framework ทำให้สื่อสารคลาดเคลื่อนได้ง่าย |
| ข้อเสีย | เพิ่มจำนวน file/ชั้นสำหรับ app เล็ก ๆ ที่ business logic แทบไม่มี |
| ข้อเสีย | การ sync สถานะสองทาง (two-way binding) ทำได้ไม่เป็นธรรมชาติเท่า MVVM |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Separation of Concerns
- Single Responsibility Principle
- MVP (Model-View-Presenter)
- MVVM (Model-View-ViewModel)
- REPR (Request-EndPoint-Response)
- Repository Pattern