Inversion of Control
ให้ framework คุมการไหลของการควบคุม ไม่ใช่ code เรา
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Inversion of Control (IoC) คือหลักการออกแบบที่ว่า การไหลของการควบคุม (flow of control) ในโปรแกรมถูกส่งมอบให้กับ “แหล่งภายนอก” — โดยทั่วไปคือ framework หรือ container — แทนที่จะให้ code ของ application เป็นผู้ควบคุมลำดับการทำงานเองทั้งหมด นี่คือแก่นของสิ่งที่เรียกว่า Hollywood Principle — “อย่าโทรหาเรา เราจะโทรหาคุณเอง”
Martin Fowler อธิบายความแตกต่างนี้ผ่านมุมมอง library เทียบกับ framework: เมื่อเราใช้ library (library) code ของเราเป็นผู้เรียก function ของ library ควบคุมจังหวะและลำดับเอง แต่เมื่อเราใช้ framework code ของเรากลับเป็นฝ่าย “ถูกเรียก” — เราเขียนส่วนขยาย (extension point) แล้วปล่อยให้ framework เป็นผู้ตัดสินใจว่าจะเรียกเมื่อไรและอย่างไร ทิศทางการควบคุมจึง “กลับทิศ” (inverted) เมื่อเทียบกับการเขียนโปรแกรมเชิงกระบวนการแบบดั้งเดิม
IoC ไม่ใช่เทคนิคเดียว แต่เป็นหลักการกว้าง ๆ ที่ปรากฏได้หลายรูปแบบ เช่น event loop ที่รอเรียก event handler ของเรา, GUI framework ที่เรียก callback เมื่อผู้ใช้คลิก, Template Method pattern ที่ตัว algorithm หลักคุมลำดับแล้วเรียก hook method ของ subclass, หรือที่พบบ่อยที่สุดในโลก enterprise คือ IoC container (หรือที่รู้จักกันว่า Dependency Injection container) ซึ่งเป็น factory เฉพาะทางที่ทำหน้าที่ประกอบ (wire) และฉีด (inject) dependency ให้กับ object ต่าง ๆ แทนที่ object เหล่านั้นจะสร้าง dependency ของตัวเองแบบ hard-code
ข้อควรระวังคือ IoC container/dependency injection เป็นเพียง “วิธีการหนึ่ง” ที่นำ IoC ไปปฏิบัติ ไม่ใช่ตัวหลักการเอง และ IoC เองก็ไม่เหมือนกับ Dependency Inversion เสียทีเดียว — DIP พูดถึง “รูปทรง” ของ dependency คือให้ module ระดับสูงและระดับต่ำต่างพึ่งพา abstraction ร่วมกัน ในขณะที่ IoC พูดถึง “ทิศทาง” ของการควบคุมว่าใครเป็นผู้เรียกใคร ทั้งสองหลักการมักทำงานร่วมกันใน code จริง แต่แยกประเด็นกันได้ชัดเจน
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”- ลดการผูกติดกับ implementation ที่เจาะจง — code ที่พึ่งพา interface แล้วปล่อยให้แหล่งภายนอกเป็นผู้จัดหา implementation จะไม่ผูกติดกับรายละเอียดของ implementation นั้น ทำให้สลับ implementation ได้โดยไม่ต้องแก้ code ที่ใช้งาน
- ทดสอบได้ง่ายขึ้น — เมื่อ dependency ถูกฉีดเข้ามาจากภายนอกแทนที่จะถูกสร้างขึ้นภายใน class เราสามารถแทนที่ด้วย test double (mock/stub/fake) ได้โดยตรงในการทดสอบหน่วย
- ขยายได้โดยไม่ต้องแก้ code เดิม — framework ที่ออกแบบตาม IoC เปิดจุดขยาย (extension point) ให้ผู้ใช้ plugin พฤติกรรมใหม่เข้าไปได้ โดยไม่ต้องไปแก้ source code ของ framework เอง สอดคล้องกับ Open/Closed Principle
- แยกความรับผิดชอบเรื่อง “การประกอบระบบ” ออกจาก “ตรรกะทางธุรกิจ” — class ที่ใช้ dependency ไม่ต้องรู้ว่า dependency ถูกสร้างขึ้นอย่างไรหรือมาจากไหน ทำให้ logic ภายใน class สะอาดและมุ่งเน้นเฉพาะหน้าที่ของตัวเอง
- เอื้อต่อการนำ framework กลับมาใช้ซ้ำ — framework ที่คุมการไหลของการควบคุมสามารถถูกนำไปใช้กับ application ที่แตกต่างกันได้มากมาย เพราะพฤติกรรมเฉพาะเจาะจงถูกฉีดเข้ามาทีหลังผ่านจุดขยาย ไม่ได้ hard-code ไว้ใน framework
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ก่อน — TaskProcessor คุมการสร้าง dependency เอง
หัวข้อที่มีชื่อว่า “ก่อน — TaskProcessor คุมการสร้าง dependency เอง”ใน version นี้ TaskProcessor เป็นผู้สร้าง instance ของ TaskService เอง และเรียกใช้งานโดยตรง การไหลของการควบคุมและการตัดสินใจเรื่อง implementation ทั้งหมดอยู่ในมือของ TaskProcessor ทำให้ทดสอบยากเพราะไม่มีทางแทนที่ TaskService ด้วย test double ได้เลย และหากต้องการเปลี่ยน implementation ก็ต้องแก้ code ของ TaskProcessor โดยตรง:
public class TaskService{ public string PerformTask() { // ตรรกะของงานอยู่ตรงนี้ return "Task completed"; }}
public class TaskProcessor{ // TaskProcessor เป็นผู้สร้าง dependency เอง — ควบคุมทุกอย่างเองทั้งหมด private readonly TaskService taskService = new TaskService();
public string ProcessTask() { return taskService.PerformTask(); }}หลัง — ควบคุมถูกกลับทิศผ่าน constructor injection
หัวข้อที่มีชื่อว่า “หลัง — ควบคุมถูกกลับทิศผ่าน constructor injection”ใน version นี้ ITaskService เป็น interface ส่วน TaskService เป็น implementation จริง TaskProcessor ไม่สร้างและไม่รู้จัก implementation ที่เจาะจงอีกต่อไป — มันรับ ITaskService ผ่าน constructor และปล่อยให้ code ภายนอก (หรือ IoC container) เป็นผู้ตัดสินใจว่าจะฉีด implementation ตัวไหนเข้ามา การควบคุมเรื่อง “ใช้ implementation อะไร” จึงถูกกลับทิศออกจาก TaskProcessor ไปอยู่ที่ผู้ประกอบระบบ:
// interface ของ service — สิ่งเดียวที่ TaskProcessor รู้จักpublic interface ITaskService{ string PerformTask();}
// implementation จริงของ servicepublic class TaskService : ITaskService{ public string PerformTask() { // ตรรกะของงานอยู่ตรงนี้ return "Task completed"; }}
// class ที่พึ่งพา ITaskService เท่านั้น ไม่รู้จัก TaskService โดยตรงpublic class TaskProcessor{ private readonly ITaskService taskService;
// ฉีด dependency ผ่าน constructor — การควบคุมถูกกลับทิศออกไปจาก class นี้ public TaskProcessor(ITaskService taskService) { this.taskService = taskService; }
public string ProcessTask() { return taskService.PerformTask(); }}
class Program{ static void Main() { // ผู้ประกอบระบบ (composition root) เป็นผู้ตัดสินใจว่าจะใช้ implementation ไหน // ในงานจริงมักใช้ IoC container เช่น Microsoft.Extensions.DependencyInjection, Autofac ITaskService taskService = new TaskService(); TaskProcessor taskProcessor = new TaskProcessor(taskService);
string result = taskProcessor.ProcessTask(); Console.WriteLine(result); }}แผนภาพต่อไปนี้เปรียบเทียบทิศทางการควบคุมแบบดั้งเดิม (ซ้าย: code เรียก library ตรง ๆ) กับ IoC (ขวา: framework เรียกกลับเข้า code ของเราผ่านจุดขยาย และ container เป็นผู้จัดหา dependency ให้):
flowchart LR
Caller -->|calls directly| UtilityLibrary
HostFramework -->|invokes| ApplicationHook
ApplicationHook -->|registers with| HostFramework
ApplicationHook -->|resolves via| IoCContainer
IoCContainer -->|provides| ConcreteService
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- class สร้าง instance ของ dependency ด้วย
newตรง ๆ ภายในตัวเอง แทนที่จะรับผ่าน constructor หรือช่องทางอื่นจากภายนอก - ต้องแก้ไข source code ของ class หรือ framework ทุกครั้งที่ต้องการเปลี่ยนพฤติกรรมหรือเพิ่ม implementation ใหม่ ทั้งที่ควรทำได้ผ่านจุดขยายเท่านั้น
- ทดสอบหน่วย (unit test) ทำไม่ได้จริงเพราะไม่มีทางแทนที่ dependency ด้วย test double — ต้องพึ่งพา infrastructure จริง เช่น ฐานข้อมูลหรือบริการภายนอก ทุกครั้งที่รัน test
- code ระดับสูง (high-level) เขียนขั้นตอนควบคุมทุกจังหวะของ code ระดับต่ำ (low-level) เองทั้งหมด ไม่มีการมอบอำนาจการตัดสินใจให้ส่วนใดเลย
- method
Mainหรือ entry point กลายเป็นจุดที่ต้องรู้จักและประสานงานกับทุก class ในระบบโดยตรง แทนที่จะมอบหมายให้ container หรือ framework เป็นผู้ประกอบ (compose) ให้
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Hollywood Principle
- Dependency Inversion
- Dependency Injection
- Explicit Dependencies
- Service Locator
- Open/Closed Principle