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

Hollywood Principle

“อย่า​โทร​หา​เรา เดี๋ยว​เรา​โทร​หา​คุณ​เอง”

Hollywood Principle กล่าว​ว่า “Don’t Call Us, We’ll Call You” — ชื่อ​หลักการ​นี้​ล้อ​กับ​คำ​ปฏิเสธ​คลาสสิก​ที่​ผู้​กำกับ​ใน​วงการ​ฮอลลีวูดมัก​พูด​กับ​นัก​แสดง​ที่​มา​ออดิชัน คือ​ไม่​ปฏิเสธ​ตรง ๆ แต่​บอกว่า “ถ้า​เรา​สนใจ เรา​จะ​โทร​กลับ​หา​คุณ​เอง” สื่อ​ความหมาย​ว่า​ฝ่าย​ที่ “มี​อำนาจ​ตัดสิน​ใจ” เป็น​ผู้​ริเริ่ม​การ​ติดต่อ ไม่ใช่​อีก​ฝ่าย

ใน​ทาง​ซอฟต์แวร์ หลักการ​นี้​พลิก​ทิศทาง​ของ control flow ตาม​แบบ​ดั้งเดิม จาก​ที่ code application ของ​เรา​เป็น​ผู้​เรียก dependency ต่าง ๆ เอง​ที​ละ​ขั้น (เหมือน​เรียก​ใช้ library) กลาย​เป็น​ว่า code ของ​เรา​ถูก​เขียน​ขึ้น​เพื่อ “รอ” ให้ framework หรือ host เป็น​ผู้​เรียก​กลับ​มา​เมื่อ​ถึง​จังหวะ​ที่​เหมาะสม นี่​คือ​แก่น​ของ​สิ่ง​ที่ Martin Fowler อธิบาย​ไว้​ใน​บทความ Inversion of Control ว่า​เป็น​เส้น​แบ่ง​สำคัญ​ระหว่าง library กับ framework: เมื่อ​ใช้ library code ของ​เรา​เป็น​ฝ่าย active คอย​เรียก method ของ library แล้ว​รอ​ผลลัพธ์​กลับ​มา แต่​เมื่อ​ใช้ framework ตัว framework ต่างหาก​ที่​เป็น “main program” code ของ​เรา​กลาย​เป็น​ชิ้น​ส่วน​ที่​ถูก​เสียบ​เข้าไป (ผ่าน​การ​สืบทอด class หรือ implement interface) แล้ว​รอ​ให้ framework เรียก​เมื่อ​ถึง​เวลา

ตัวอย่าง​ที่​ชัด​ที่สุด​คือ​หน้า ASP.NET Web Form ทั่วไป ที่​มี​ตัว​จัดการ event ใน​หน้า codebehind เพื่อ​ตอบ​สนอง​ต่อ event อย่าง Page_Load และ Button_Click — นัก​พัฒนา​ไม่​ได้​เป็น​เจ้าของ main loop ของ​เว็บ server และ​ตัดสิน​ใจ​เรียก method เหล่า​นี้​เอง แต่ ASP.NET runtime ต่างหาก​ที่​ควบคุม​วงจร​ชีวิต​ทั้งหมด และ “โทร​กลับ” มายัง code ของ​เรา​ตาม​จังหวะ​ที่​กำหนด​ไว้

หลักการ​นี้​เกี่ยวข้อง​อย่าง​ใกล้​ชิด​กับ​ทั้ง Inversion of Control และ Dependency Inversion Principle แต่​เน้น​คนละ​มุม: Dependency Inversion Principle พูด​ถึง​ทิศทาง​ของ dependency ที่ compile-time (module ระดับ​สูง​และ​ต่ำ​ต่าง​พึ่งพา abstraction ร่วม​กัน) ส่วน Hollywood Principle พูด​ถึง​ทิศทาง​ของ การ​ควบคุม​ที่ runtime — ใคร​เป็น​ผู้​ตัดสิน​ใจ​ว่า​จะ​เรียก method ไหน​เมื่อไร ตาม​ที่ Wikipedia บันทึก​ไว้ คำ​ว่า “Hollywood Principle” ปรากฏ​ครั้ง​แรก​ใน​เอกสาร​ของ Richard Sweet เกี่ยว​กับ​ภาษา Mesa ปี 1983 ส่วน​คำ​ว่า “Inversion of Control” ถูก​ใช้​อย่าง​เป็น​ทางการ​ใน​บทความ​ของ Johnson และ Foote ปี 1988 ก่อน​ที่ Robert C. Martin และ Martin Fowler จะ​ช่วย​กัน​ทำให้​แนวคิด​นี้​เป็น​ที่​รู้จัก​ใน​วง​กว้าง​ช่วง​ปี 2004

รูปแบบ​ที่​พบ​บ่อย​ของ Hollywood Principle ใน​ทาง​ปฏิบัติ ได้แก่:

  • Event/callback — code ของ​เรา subscribe event แล้ว​รอ framework เรียก handler กลับ (เช่น Swing/WinForms button click)
  • Template Method — class แม่​ควบคุม​ลำดับ​ขั้นตอน​ทั้งหมด ส่วน class ลูก override เฉพาะ​บาง​ขั้น แล้ว​รอ​ให้ class แม่​เรียก
  • Plugin/extension model — โปรแกรม​หลัก​เปิดเผย “hook” ให้ module plugin ลง​ทะเบียน​ตัวเอง แล้ว​โปรแกรม​หลัก​เป็น​ผู้​ตัดสิน​ใจ​ว่า​จะ​เรียก plugin ไหน​เมื่อไร
  • Dependency Injection container — เรา​ประกาศ dependency ที่​ต้องการ แล้ว container เป็น​ผู้​สร้าง​และ “เรียก” constructor/property ให้ โดยที่​เรา​ไม่​ได้ new มัน​เอง
  • ลด coupling ระหว่าง code ระดับ​สูง​กับ code ระดับ​ต่ำ เพราะ plugin หรือ module ย่อย​ไม่​จำเป็น​ต้อง​รู้จัก​ลำดับ​การ​ทำงาน​ทั้ง​ระบบ แค่ implement hook ที่​ตกลง​กัน​ไว้​ก็​พอ ทำให้ framework เปลี่ยน​วิธี​จัด flow ภายใน​ได้​โดย​ไม่​กระทบ code plugin
  • เปิด​ทาง​ให้​ขยาย​ระบบ​ได้​โดย​ไม่​แก้ code เดิม สอดคล้อง​กับ Open/Closed Principle — เพิ่ม plugin ใหม่​ได้​แค่ implement interface ที่ framework กำหนด โดย​ไม่​ต้อง​ไป​แก้ core ของ framework เลย
  • ป้องกัน​ไม่​ให้ code plugin ยึดครอง​การ​ควบคุม​ทั้ง application model plugin ที่​ดี​ต้อง​เปิดเผย event หรือ hook ให้ module ภายนอก​ใช้ เพื่อ​ให้​โปรแกรม​หลัก​ยัง​คง​เป็น​ผู้​เรียก​เข้าไป​ใน plugin เมื่อ​จำเป็น ไม่ใช่​ให้ plugin แย่งชิง main loop ไป​เอง
  • ทดสอบ​ง่าย​ขึ้น เพราะ​เมื่อ code ของ​เรา​เป็น​แค่ implementation ของ interface/hook ที่​รอ​ถูก​เรียก เรา​สามารถ​เรียก​มัน​ตรง ๆ ใน​หน่วย​ทดสอบ​ได้​โดย​ไม่​ต้อง​พึ่ง framework จริง​ทั้ง​ชุด
  • แยก​ความ​รับผิดชอบ​ชัดเจน — “เมื่อไร​ควร​เรียก” เป็น​หน้าที่​ของ framework/host ส่วน “เรียก​แล้ว​ต้อง​ทำ​อะไร” เป็น​หน้าที่​ของ code ที่​เรา​เขียน ทำให้​แต่ละ​ฝั่ง​เปลี่ยนแปลง​ได้​อย่าง​อิสระ​ต่อ​กัน (low coupling, high cohesion)

ตัวอย่าง​ด้าน​ล่าง​จำลอง​ระบบ​สร้าง​รายงาน โดย​ฝั่ง “ก่อน” code หลัก​เป็น​ผู้​ควบคุม​ทุก​ขั้นตอน​เอง เรียก dependency ที​ละ​ตัว​ตาม​ลำดับ​ตายตัว​เหมือน​เรียก​ใช้ library — เพิ่ม​ขั้นตอน​ใหม่​ไม่​ได้​โดย​ไม่​แก้ ReportGenerator:

// ก่อน - code ของเราเป็นผู้ควบคุมทุกจังหวะ เรียก dependency เองทีละขั้น
public class ReportGenerator
{
public void Run()
{
var data = new DataLoader().Load();
var validator = new DataValidator();
if (!validator.Validate(data))
{
return;
}
var report = new PdfRenderer().Render(data);
new EmailSender().Send(report);
// อยากเพิ่มขั้นตอนใหม่ เช่น audit log หรือ archive
// ต้องมาแก้ method นี้ทุกครั้ง
}
}

ฝั่ง “หลัง” ใช้ Hollywood Principle: เรา​ออกแบบ IReportStep เป็น hook แล้ว​ให้​แต่ละ​ขั้นตอน “ลง​ทะเบียน” ตัวเอง​กับ pipeline โดยที่​ตัว plugin ไม่รู้​เลย​ว่า​ใคร​จะ​เรียก​มัน​เมื่อไร มี​แค่ ReportPipeline เท่านั้น​ที่​ควบคุม​ลำดับ​และ​ตัดสิน​ใจ​ว่า​จะ​เรียก​ขั้นตอน​ไหน​ต่อ:

// หลัง - เรา "ลงทะเบียน" ตัวเองกับ host แล้วรอให้ host เรียกกลับ (don't call us, we'll call you)
public interface IReportStep
{
void Execute(ReportContext context);
}
public class ValidationStep : IReportStep
{
public void Execute(ReportContext context)
{
if (!context.Data.IsValid)
{
context.Cancel();
}
}
}
public class PdfRenderStep : IReportStep
{
public void Execute(ReportContext context)
{
context.Report = context.Renderer.Render(context.Data);
}
}
public class EmailNotificationStep : IReportStep
{
// ขั้นตอนนี้ไม่รู้เลยว่าจะถูกเรียกตอนไหน มันแค่รอ pipeline เรียกกลับมา
public void Execute(ReportContext context)
{
context.EmailSender.Send(context.Report);
}
}
// Framework/host เป็นผู้ควบคุมลำดับการเรียกทั้งหมด ไม่ใช่ตัว plugin
public class ReportPipeline
{
private readonly List<IReportStep> _steps = new();
public void Register(IReportStep step) => _steps.Add(step);
public void Run(ReportContext context)
{
foreach (var step in _steps)
{
step.Execute(context);
if (context.IsCancelled)
{
return;
}
}
}
}

ตอน​นี้​เพิ่ม​ขั้นตอน​ใหม่ เช่น AuditLogStep ได้​แค่ Register เข้า pipeline โดย​ไม่​ต้อง​แตะ ReportPipeline เลย นี่​คือ​การ​พลิก​ทิศทางการ​ควบคุม: จาก “code ของ​เรา​เรียก​ทุก​อย่าง​เอง” กลาย​เป็น “เรา​แค่​บอก host ว่า​เรา​มี​อะไร​ให้​ใช้ แล้ว​รอ​ให้ host เรียก​กลับ”

แผนภาพ​ด้าน​ล่าง​เปรียบเทียบ​ทิศทางการ​ควบคุม​ทั้ง​สอง​แบบ:

flowchart LR
    subgraph Conventional
        AppCode[Application code] --> Library[Library function]
    end
    subgraph Hollywood
        PluginCode[Plugin code] --> Register[Register a hook]
        Register --> Framework[Framework]
        Framework --> Invoke[Framework invokes the hook]
        Invoke --> PluginCode
    end
  • code plugin หรือ module ย่อย​เรียก​กลับ​เข้าไป​ควบคุม main loop เอง เช่น เรียก Application.Run() หรือ​วน loop ตรวจ​สถานะ​ของ host ซ้ำ ๆ (busy polling) แทนที่​จะ​รอ event หรือ callback จาก host
  • ต้อง​แก้ code core ของ​ระบบ​ทุก​ครั้ง​ที่​เพิ่ม plugin ใหม่ เช่น ต้อง​เติม if/else หรือ switch ใน orchestrator เพื่อ​รองรับ implementation ใหม่ แทนที่​จะ​แค่ Register เข้าไป — สัญญาณ​นี้​มัก​ไป​พร้อม​กับ​การ​ละเมิด Open/Closed Principle ด้วย
  • module ระดับ​ต่ำ new หรือ​เรียก service ระดับ​สูง​โดยตรง ทำให้​เกิด​การ​พึ่งพา​แบบ​วงกลม (circular dependency) ระหว่าง​ชั้น​ที่​ควร​จะ​แยก​ทิศทาง​กัน​ชัดเจน
  • code client ต้อง poll สถานะ​ซ้ำ ๆ แทนที่​จะ​ได้​รับ​การ​แจ้ง​เตือน​ผ่าน event หรือ callback เมื่อ​มี​อะไร​เปลี่ยนแปลง เป็น​สัญญาณ​ว่า​ไม่มี hook ให้ “ลง​ทะเบียน” ความ​สนใจ​ไว้​ล่วงหน้า
  • plugin ต้อง​รู้จัก​รายละเอียด​ภายใน​ของ host มาก​เกิน​ไป เพื่อ​จะ​เรียก​กลับ​เข้าไป​ให้​ถูก​จังหวะ ทั้ง​ที่​ควร​มี​แค่ interface หรือ event บาง ๆ เป็น​จุด​เชื่อม​ต่อ​เท่านั้น