เพื่อให้เห็นภาพการทำงานของ MVP ( Model-View-Presenter ) ชัดเจนที่สุด เขียนตัวอย่างด้วย JavaScript โดยทำระบบ “เครื่องนับเลข ( Counter )” แบบง่าย ๆ ครับ
หัวใจสำคัญของ MVP คือ View และ Model จะไม่รู้จักกันเลย ทุกอย่างต้องวิ่งผ่าน Presenter ทั้งหมด
Model ( จัดการข้อมูล )
มีหน้าที่แค่เก็บข้อมูลและจัดการตรรกะ ( การบวกเลข ) ไม่สนว่าหน้าจอเป็นยังไง
class CounterModel {
constructor() {
this.count = 0;
}
// ตรรกะทางธุรกิจ: การเพิ่มค่า
increment() {
this.count++;
}
// ส่งคืนข้อมูลปัจจุบัน
getCount() {
return this.count;
}
}
View ( จัดการหน้าจอ )
มีหน้าที่จัดการ HTML ( DOM ) รับการคลิกจากผู้ใช้ และมีฟังก์ชันเตรียมไว้ให้ Presenter มาสั่งงาน
class CounterView {
constructor() {
// สมมติว่าใน HTML มี <h1 id="counter-display">0</h1> และ <button id="add-btn">Add</button>
this.displayElement = document.getElementById('counter-display');
this.buttonElement = document.getElementById('add-btn');
}
// เปิดช่องทางให้ Presenter เอาฟังก์ชันมาผูกกับปุ่ม
bindIncrementEvent(handler) {
this.buttonElement.addEventListener('click', () => {
handler(); // เมื่อถูกคลิก จะเรียกฟังก์ชันที่ Presenter ส่งมาให้
});
}
// เปิดช่องทางให้ Presenter สั่งอัปเดตหน้าจอ
render(count) {
this.displayElement.innerText = `จำนวนปัจจุบัน: ${count}`;
}
}
Presenter ( ผู้ประสานงาน )
เป็นตัวกลางที่จับ Model และ View มาคุยกัน ควบคุม Flow การทำงานทั้งหมด แทน Controller
class CounterPresenter {
constructor(model, view) {
this.model = model;
this.view = view;
// 1. นำฟังก์ชัน handleIncrement ไปผูกกับปุ่มใน View
this.view.bindIncrementEvent(() => this.handleIncrement());
// 2. สั่งแสดงผลครั้งแรกตอนเริ่มต้น
this.updateView();
}
// ทำงานเมื่อ View แจ้งว่ามีการคลิก
handleIncrement() {
this.model.increment(); // สั่ง Model ให้อัปเดตข้อมูล
this.updateView(); // สั่งหน้าจอให้อัปเดต
}
// ดึงข้อมูลจาก Model แล้วเอาไปสั่ง View ให้เรนเดอร์
updateView() {
const currentCount = this.model.getCount();
this.view.render(currentCount);
}
}
การเรียกใช้งาน ( Initialization )
ในหน้าหลักของเรา เราจะสร้างทั้ง 3 ส่วนขึ้นมาประกอบร่างกัน
// จำลองการเริ่มทำงานของระบบ
document.addEventListener('DOMContentLoaded', () => {
const model = new CounterModel();
const view = new CounterView();
// Presenter จะเป็นตัวจับ Model และ View เข้าด้วยกัน
const presenter = new CounterPresenter(model, view);
});
จุดสังเกตที่ทำให้ต่างจาก MVC ปกติ
จะเห็นว่าในฝั่ง CounterView ไม่มีการเรียกใช้ this.model.getCount() เลย หน้าจอทำงานโง่ ๆ ( Passive View ) แค่รอให้ผู้ใช้กดปุ่มแล้วตะโกนบอก Presenter จากนั้นก็รอ Presenter โยนข้อมูลที่เสร็จแล้วกลับมาให้ผ่านฟังก์ชัน render() อย่างเดียวครับ ทำให้เราสามารถเอา View ตัวอื่นมาเสียบแทนได้ง่ายมาก
Presenter VS Controller
ความแตกต่างระหว่าง Controller ( ใน MVC ) กับ Presenter ( ใน MVP ) เป็นเส้นบาง ๆ ที่ทำให้นักพัฒนาหลายคนสับสน จุดที่ทำให้สองตัวนี้แตกต่างกันอย่างชัดเจนที่สุดคือ “วิธีการที่พวกมันจัดการกับ View และ Model” ครับ
- การสื่อสารกับ Model ( ใครคุยกับใคร ? )
- Controller (MVC): ในโครงสร้าง MVC แบบดั้งเดิม View สามารถมองเห็นและดึงข้อมูลจาก Model ได้โดยตรง Controller มีหน้าที่แค่รับคำสั่งจากผู้ใช้ ( เช่น กดปุ่ม ) แล้วไปสั่ง Model ให้อัปเดตตัวเอง เมื่อ Model เปลี่ยนแปลง มันจะแจ้งเตือน View ให้มาดึงข้อมูลใหม่ไปแสดงผล
- Presenter (MVP): ตัดการเชื่อมต่อนี้ทิ้งอย่างเด็ดขาด! View และ Model จะไม่มีวันรู้จักกัน Presenter จะเป็น “คนกลาง ( Middleman )” เต็มตัว มันจะดึงข้อมูลจาก Model มาปรุงแต่ง แล้วค่อย “ป้อน ( Inject )” ข้อมูลนั้นใส่ลงไปใน View
- ความฉลาดของหน้าจอ ( View )
- MVC (Active View): View มีความฉลาดอยู่บ้าง มันรู้ว่าต้องไปดึงข้อมูลอะไรจาก Model มาแสดงผล
- MVP (Passive View): View “โง่” มาก ( เราเรียกแบบนี้จริง ๆ ครับ คือ Passive View ) มันไม่มีตรรกะอะไรเลย ไม่รู้ว่าข้อมูลมาจากไหน มันมีหน้าที่แค่รับข้อมูลที่ Presenter โยนมาให้ แล้วแปะลงบนหน้าจอ และคอยส่งเสียงบอก Presenter เวลาผู้ใช้คลิกหรือพิมพ์อะไรบางอย่าง
- ความสัมพันธ์ ( Relationship )
- Controller: มักจะมีความสัมพันธ์แบบ 1 ต่อ หลาย ( 1-to-Many ) คือ 1 Controller อาจจะควบคุมการทำงานของหลาย ๆ View ได้ ( เช่น พนักงานรับออเดอร์ 1 คน ดูแลลูกค้าหลายโต๊ะ )
- Presenter: มักจะมีความสัมพันธ์แบบ 1 ต่อ 1 ( 1-to-1 ) คือ 1 Presenter จะจับคู่ดูแล 1 View อย่างใกล้ชิด ( เช่น บอดี้การ์ดส่วนตัว ) ทำให้โค้ดผูกพันกันมากกว่าในแง่ของการจับคู่
- ความง่ายในการทดสอบ ( Unit Testing )
- MVC: ทดสอบ ( Test ) ฝั่ง Controller ยากกว่าเล็กน้อย เพราะบางครั้งมันผูกติดกับระบบการทำงานของ View หรือ Framework มากเกินไป
- MVP: ทดสอบง่ายกว่ามาก! เพราะ Presenter ไม่ได้ไปแตะต้องหน้าจอ ( HTML/DOM ) โดยตรงเลย มันแค่คุยผ่าน Interface ( ฟังก์ชันที่ View เตรียมไว้ ) เราจึงสามารถจำลอง ( Mock ) View ปลอม ๆ ขึ้นมาเพื่อทดสอบตรรกะใน Presenter ได้สบาย ๆ
สรุปเปรียบเทียบแบบรวบยอด
| คุณสมบัติ | Controller ( MVC ) | Presenter ( MVP ) |
| View รู้จัก Model ไหม? | ✅ รู้จัก ( ดึงข้อมูลเองได้ ) | ❌ ไม่รู้จักเด็ดขาด |
| ตรรกะของ View | มีบ้าง ( Active ) | ไม่มีเลย ( Passive ) รอรับคำสั่งอย่างเดียว |
| คนอัปเดตหน้าจอ | View อัปเดตตัวเองเมื่อ Model เปลี่ยน | Presenter เป็นคนสั่งอัปเดตหน้าจอ |
| ความสัมพันธ์ (โดยทั่วไป) | 1 Controller : หลาย Views | 1 Presenter : 1 View |
| การเขียน Unit Test | ทำได้ แต่อาจจะยากกว่า | ทำได้ง่ายมาก |
สรุปง่าย ๆ คือ Presenter คือ Controller ที่มีความเป็นเผด็จการสูงกว่า ควบคุมทุกอย่างเบ็ดเสร็จ และไม่ยอมให้ลูกน้อง ( View กับ Model ) แอบคุยกันเองข้ามหัวมันครับ
อ่านเพิ่มเติม