Model-View-Presenter
Presenter ถือ reference ไปยัง View ผ่าน interface — ทดสอบได้เต็มที่
ปัญหาที่ MVP แก้คือปัญหาเดียวกับที่ MVC พยายามแก้ นั่นคือ logic การนำเสนอ (presentation logic) — กฎว่า UI ควรตอบสนองอย่างไรต่อ input ของผู้ใช้, ควรแสดง/ซ่อนอะไรเมื่อไหร่, ควร format ข้อมูลอย่างไรก่อนแสดงผล — มักถูกฝังปนอยู่ใน code-behind ของ UI widget โดยตรง ทำให้ทดสอบได้ยากหรือทดสอบไม่ได้เลย เพราะต้องพึ่ง UI framework จริง (ต้องเปิดหน้าต่าง, ต้อง render, ต้องมี event loop) การเขียน unit test ให้ logic เหล่านี้จึงกลายเป็นภาระหนักหรือถูกข้ามไปเลย
MVP ดัดแปลงมาจาก MVC โดยความต่างเชิงโครงสร้างที่สำคัญที่สุดคือ Presenter ถือ reference ไปยัง View ผ่าน interface (ไม่ใช่ผ่าน concrete UI class) แนวคิดนี้มีต้นกำเนิดที่ Taligent ในช่วงต้นทศวรรษ 1990 (โครงการร่วมของ Apple, IBM และ Hewlett-Packard สำหรับสภาพแวดล้อม CommonPoint บน C++) ถูกเผยแพร่ต่อโดย Mike Potel และภายหลังถูกขัดเกลาอย่างละเอียดโดยทีม Dolphin Smalltalk ในเอกสาร “Twisting the Triad” ก่อนที่ Martin Fowler จะวิเคราะห์และแยกย่อยมันออกเป็นสองรูปแบบย่อยใน “GUI Architectures” — Passive View และ Supervising Controller
หัวใจของแนวคิดคือ: ดึง presentation logic ทั้งหมด ออกจาก view แล้วย้ายไปไว้ที่ presenter ที่เป็น plain object ธรรมดา ไม่ผูกกับ UI framework ใด ๆ เมื่อ view เหลือแค่ property/event ที่ประกาศผ่าน interface (IView) presenter ก็สามารถถูกทดสอบด้วย mock/fake ของ view นั้นได้อย่างสมบูรณ์ โดยไม่ต้องเปิดหน้าต่างจริงเลยสักครั้งเดียว
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class IOrderView {
<<interface>>
+CustomerName string
+OrderTotal string
+ShowError string
+LoadClicked event
+SaveClicked event
}
class OrderView {
-presenter OrderPresenter
+CustomerName string
+OrderTotal string
+ShowError string
}
class OrderPresenter {
-view IOrderView
-repository IOrderRepository
+OnLoad()
+OnSave()
}
class IOrderRepository {
<<interface>>
+GetById(id) Order
+Save(order)
}
class Order {
+CustomerName string
+Total decimal
}
OrderView ..|> IOrderView
OrderPresenter --> IOrderView
OrderPresenter --> IOrderRepository
OrderPresenter --> Order
สังเกตทิศทางของลูกศร: OrderPresenter พึ่งพา IOrderView (interface) ไม่ใช่ OrderView (concrete class) ส่วน OrderView เป็นฝ่าย implement interface นั้นและถือ reference กลับไปยัง presenter เพื่อส่งต่อ event ของผู้ใช้ ความสัมพันธ์นี้ทำให้ presenter ไม่รู้จัก UI framework เลยแม้แต่น้อย — มันเห็นแค่ property string กับ event ธรรมดา
Fowler แยก MVP ออกเป็นสองแบบตามระดับความ “โง่” ของ view ที่ยอมให้:
- Passive View — view ถูกทำให้ passive ที่สุด ไม่มีแม้แต่การ binding กับ model โดยตรง ทุกอย่างต้องผ่าน presenter รวมถึงการเติมข้อมูลลง widget presenter จึงต้องเรียก setter ของ view ทุก property ที่เปลี่ยน (verbose กว่า แต่ทดสอบได้ 100%)
- Supervising Controller — view ยังได้รับอนุญาตให้ผูก (bind) กับ model โดยตรงสำหรับ logic ง่าย ๆ (เช่น data binding ธรรมดา) ส่วน presenter จะเข้ามาจัดการเฉพาะ logic ที่ซับซ้อนหรือมีเงื่อนไข — คล้ายเป็นจุดกึ่งกลางระหว่าง MVC กับ Passive View
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ผู้เล่นหลักสามฝ่ายมีบทบาทดังนี้:
- View — เป็น passive interface ล้วน ๆ เปิดเผยเฉพาะ property (สำหรับตั้งค่าที่แสดงบนจอ) และ event (สำหรับแจ้ง user gesture เช่นคลิกปุ่ม) ไม่มี logic การตัดสินใจใด ๆ อยู่ในนี้เลย
- Presenter — รับ event จาก view, เรียก model/repository เพื่อดึงหรือบันทึกข้อมูล, แปลง (format) ข้อมูลให้พร้อมแสดงผล แล้ว “สั่ง” view ผ่าน interface ให้ set ค่า presenter จึงเป็นที่รวมของ presentation logic ทั้งหมด
- Model — domain object หรือ service ที่ไม่รู้จัก UI เลย (repository, entity, business service ทั่วไป)
ลำดับเหตุการณ์ทั่วไปเมื่อผู้ใช้กดปุ่ม “โหลดข้อมูล”:
sequenceDiagram
participant U as ผู้ใช้
participant V as OrderView
participant P as OrderPresenter
participant R as IOrderRepository
U->>V: คลิกปุ่ม Load
V->>P: raise LoadClicked event
P->>R: GetById(id)
R-->>P: Order
P->>V: set CustomerName, OrderTotal
V-->>U: จอแสดงข้อมูลใหม่
จุดสำคัญคือ view เพียง “ส่งสัญญาณ” event ออกไป แต่ไม่รู้เลยว่า presenter จะทำอะไรต่อ — และ presenter เพียง “สั่ง” view ผ่าน interface โดยไม่รู้เลยว่าเบื้องหลัง view เป็น WinForms, WPF, เว็บ หรือ mock ใน test นี่คือกลไกที่ทำให้ทั้งสองฝั่งแยกจากกันอย่างสมบูรณ์ (decoupled) ตาม Dependency Inversion Principle
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”using System.Globalization;
// Model — domain object ที่ไม่รู้จัก UI เลยpublic class Order{ public int Id { get; set; } public string CustomerName { get; set; } = ""; public decimal Total { get; set; }}
public interface IOrderRepository{ Order? GetById(int id); void Save(Order order);}
// View interface — สัญญาที่ presenter พึ่งพา (mock ได้ใน test)public interface IOrderView{ string CustomerName { get; set; } string OrderTotal { get; set; } string ErrorMessage { set; }
event Action<int> LoadRequested; event Action SaveRequested;}
// Presenter — ที่รวมของ presentation logic ทั้งหมด ทดสอบได้โดยไม่ต้องมี UI จริงpublic class OrderPresenter{ private readonly IOrderView _view; private readonly IOrderRepository _repository; private Order? _current;
public OrderPresenter(IOrderView view, IOrderRepository repository) { _view = view; _repository = repository;
// ผูก event ของ view เข้ากับ handler — presenter เป็นฝ่าย "ควบคุม" _view.LoadRequested += OnLoadRequested; _view.SaveRequested += OnSaveRequested; }
private void OnLoadRequested(int orderId) { var order = _repository.GetById(orderId); if (order is null) { _view.ErrorMessage = $"ไม่พบคำสั่งซื้อหมายเลข {orderId}"; return; }
_current = order; // presenter จัดการ formatting ทั้งหมด view แค่โชว์ string ที่ได้รับ _view.CustomerName = order.CustomerName; _view.OrderTotal = order.Total.ToString("C", new CultureInfo("th-TH")); }
private void OnSaveRequested() { if (_current is null) { _view.ErrorMessage = "กรุณาโหลดคำสั่งซื้อก่อนบันทึก"; return; }
_current.CustomerName = _view.CustomerName; _repository.Save(_current); }}ทดสอบ presenter โดยไม่ต้องมี UI จริงเลยสักครั้งเดียว ด้วยการเขียน fake/stub ของ IOrderView:
using System.Collections.Generic;
public class FakeOrderView : IOrderView{ public string CustomerName { get; set; } = ""; public string OrderTotal { get; set; } = ""; public string ErrorMessage { set { LastError = value; } } public string? LastError { get; private set; }
public event Action<int>? LoadRequested; public event Action? SaveRequested;
public void SimulateLoad(int id) => LoadRequested?.Invoke(id);}
public class InMemoryOrderRepository : IOrderRepository{ private readonly Dictionary<int, Order> _orders = new();
public Order? GetById(int id) => _orders.TryGetValue(id, out var order) ? order : null;
public void Save(Order order) => _orders[order.Id] = order;}
[Fact]public void Load_ที่มีคำสั่งซื้อจริง_ต้องแสดงชื่อลูกค้าและยอดรวม(){ var repo = new InMemoryOrderRepository(); repo.Save(new Order { Id = 1, CustomerName = "สมชาย", Total = 250 });
var view = new FakeOrderView(); var presenter = new OrderPresenter(view, repo);
view.SimulateLoad(1);
Assert.Equal("สมชาย", view.CustomerName); Assert.Equal("฿250.00", view.OrderTotal);}เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ต้องการทดสอบ presentation logic ด้วย unit test โดยไม่พึ่ง UI framework หรือ test runner แบบ UI automation
- ทำงานกับ UI framework แบบ event-driven ที่ไม่มี data binding ที่ทรงพลัง (WinForms แบบเก่า, บาง framework บนมือถือ/embedded)
- ต้องการรองรับหลาย view implementation (เช่น desktop กับ web) ที่ใช้ presenter ตัวเดียวกัน ผ่านการ implement interface เดียวกัน
- ทีมต้องการแยกความรับผิดชอบชัดเจนระหว่างคนที่ออกแบบ UI (implement view) กับคนที่เขียน business/presentation logic (เขียน presenter)
- มี legacy codebase ที่ logic ปนอยู่ใน code-behind และต้องการค่อย ๆ แยกออกมาให้ทดสอบได้ (มักเริ่มจาก Supervising Controller แล้วค่อยขยับไป Passive View)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- UI framework มี two-way data binding ที่ดีอยู่แล้ว (เช่น WPF, Angular, Vue) — MVVM มักเหมาะกว่าเพราะใช้ binding แทนการ set property ทีละตัวด้วยมือ
- application เล็กมาก มี screen เดียวหรือ2 logic น้อย — ต้นทุนของการสร้าง interface และ presenter แยกต่างหากอาจไม่คุ้ม (YAGNI)
- ทีมทำงานกับ web framework สมัยใหม่ที่มี pattern ของตัวเองอยู่แล้ว (เช่น component-based UI) การฝืนใช้ MVP อาจขัดกับ convention ของ framework
- ต้องการ synchronize state ระหว่างหลาย view พร้อมกันแบบ real-time ซับซ้อน — MVVM หรือ state-management library มักออกแบบมาสำหรับเคสนี้โดยเฉพาะ
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | Presentation logic ทดสอบได้เต็มที่ด้วย unit test ธรรมดา ไม่ต้องพึ่ง UI |
| ข้อดี | View กับ presenter แยกจากกันชัดเจนตาม Interface Segregation — สลับ view implementation ได้โดยไม่แตะ presenter |
| ข้อดี | ลด logic ที่ซ้ำซ้อนหรือกระจัดกระจายในหลาย code-behind เพราะรวมศูนย์ไว้ที่ presenter |
| ข้อเสีย | ต้อง set ทุก property ผ่าน interface ด้วยมือ (โดยเฉพาะ Passive View) ทำให้ boilerplate เยอะขึ้น |
| ข้อเสีย | ถ้า UI framework มี binding ที่ดีอยู่แล้ว MVP จะดูซ้ำซ้อนเมื่อเทียบกับ MVVM |
| ข้อเสีย | Presenter อาจบวมกลายเป็น God Class ถ้าไม่ระวังแยก responsibility ให้ดี |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- MVC
- MVVM
- Interface Segregation Principle
- Dependency Inversion Principle
- Repository
- Test-Driven Development