ป้ายกำกับ: Controller

MVP ( Model-View-Presenter )MVP ( Model-View-Presenter )

เพื่อให้เห็นภาพการทำงานของ 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” ครับ

  1. การสื่อสารกับ Model ( ใครคุยกับใคร ? )
    • Controller (MVC): ในโครงสร้าง MVC แบบดั้งเดิม View สามารถมองเห็นและดึงข้อมูลจาก Model ได้โดยตรง Controller มีหน้าที่แค่รับคำสั่งจากผู้ใช้ ( เช่น กดปุ่ม ) แล้วไปสั่ง Model ให้อัปเดตตัวเอง เมื่อ Model เปลี่ยนแปลง มันจะแจ้งเตือน View ให้มาดึงข้อมูลใหม่ไปแสดงผล
    • Presenter (MVP): ตัดการเชื่อมต่อนี้ทิ้งอย่างเด็ดขาด! View และ Model จะไม่มีวันรู้จักกัน Presenter จะเป็น “คนกลาง ( Middleman )” เต็มตัว มันจะดึงข้อมูลจาก Model มาปรุงแต่ง แล้วค่อย “ป้อน ( Inject )” ข้อมูลนั้นใส่ลงไปใน View
  2. ความฉลาดของหน้าจอ ( View )
    • MVC (Active View): View มีความฉลาดอยู่บ้าง มันรู้ว่าต้องไปดึงข้อมูลอะไรจาก Model มาแสดงผล
    • MVP (Passive View): View “โง่” มาก ( เราเรียกแบบนี้จริง ๆ ครับ คือ Passive View ) มันไม่มีตรรกะอะไรเลย ไม่รู้ว่าข้อมูลมาจากไหน มันมีหน้าที่แค่รับข้อมูลที่ Presenter โยนมาให้ แล้วแปะลงบนหน้าจอ และคอยส่งเสียงบอก Presenter เวลาผู้ใช้คลิกหรือพิมพ์อะไรบางอย่าง
  3. ความสัมพันธ์ ( Relationship )
    • Controller: มักจะมีความสัมพันธ์แบบ 1 ต่อ หลาย ( 1-to-Many ) คือ 1 Controller อาจจะควบคุมการทำงานของหลาย ๆ View ได้ ( เช่น พนักงานรับออเดอร์ 1 คน ดูแลลูกค้าหลายโต๊ะ )
    • Presenter: มักจะมีความสัมพันธ์แบบ 1 ต่อ 1 ( 1-to-1 ) คือ 1 Presenter จะจับคู่ดูแล 1 View อย่างใกล้ชิด ( เช่น บอดี้การ์ดส่วนตัว ) ทำให้โค้ดผูกพันกันมากกว่าในแง่ของการจับคู่
  4. ความง่ายในการทดสอบ ( 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 : หลาย Views1 Presenter : 1 View
การเขียน Unit Testทำได้ แต่อาจจะยากกว่าทำได้ง่ายมาก

สรุปง่าย ๆ คือ Presenter คือ Controller ที่มีความเป็นเผด็จการสูงกว่า ควบคุมทุกอย่างเบ็ดเสร็จ และไม่ยอมให้ลูกน้อง ( View กับ Model ) แอบคุยกันเองข้ามหัวมันครับ


อ่านเพิ่มเติม