ข้าม​ไป​ยัง​เนื้อหา

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 บน​เว็บ​มี​ดังนี้:

  1. ผู้​ใช้​ส่ง request (คลิก link, submit ฟอร์ม) เข้า​มา​ที่ app
  2. router จับ​คู่ request กับ Controller action ที่​เหมาะสม
  3. Controller อ่าน input, เรียก Model (ผ่าน repository/service) เพื่อ​อ่าน​หรือ​อัปเดต​สถานะ — Controller ไม่​ทำ business logic เอง แค่​ประสาน​งาน
  4. Controller เลือก View ที่​จะ render และ​ส่ง Model (หรือ ViewModel) ให้
  5. View อ่าน​ข้อมูล​จาก Model แล้ว​สร้าง​ผลลัพธ์ (HTML) ส่ง​กลับ​ให้​ผู้​ใช้
  6. ถ้า 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 เว็บ​ยุค​นี้​นำ​มา​ใช้

ตัวอย่าง 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, เลือก View
public 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