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 เป็นผู้ควบคุมลำดับการเรียกทั้งหมด ไม่ใช่ตัว pluginpublic 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 บาง ๆ เป็นจุดเชื่อมต่อเท่านั้น
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Dependency Inversion Principle
- Inversion of Control
- Open/Closed Principle
- Observer
- Domain Events
- Dependency Injection