Dependency Injection
ส่ง dependency เข้ามาจากภายนอก แทนที่จะให้ class สร้างขึ้นเอง
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”Dependency Injection (DI) เป็นเทคนิคที่เอื้อให้ระบบเชิงวัตถุเป็นแบบ loosely coupled มันเกี่ยวข้องอย่างใกล้ชิดกับ Dependency Inversion Principle — DIP บอกว่า module ระดับสูงไม่ควรขึ้นกับรายละเอียดของ module ระดับต่ำโดยตรง ส่วน DI คือ เทคนิคเชิงปฏิบัติ ที่ทำให้หลักการนั้นเป็นจริงใน code
ในระบบง่าย ๆ การอ้างอิงถึง object ที่ทำงานร่วมกัน (collaborating objects) มักถูกสร้างขึ้นตรง ๆ ภายใน class ที่ต้องใช้มัน เช่น new SmtpEmailSender() อยู่กลาง method ผลคือ tightly coupled ระหว่าง class เหล่านี้ ทำให้ทดสอบ refactor และดูแลรักษายากขึ้น เพราะจะสลับ implementation หรือใส่ mock แทนไม่ได้เลยถ้าไม่แก้ code class ที่ใช้งาน
Dependency Injection คือเทคนิคที่ object ซึ่งทำงานร่วมกันถูก ส่งเข้ามา ยัง class ที่ต้องใช้ และตัว class เองเขียน code โดยอ้างกับ interface หรือ base class แทนที่จะอ้างกับ class implementation ที่เจาะจง ตาม Wikipedia เทคนิคนี้ประกอบด้วยสี่บทบาท: service (class ที่มีความสามารถที่ต้องการใช้), client (class ที่ใช้ service นั้น), interface (สัญญาที่ client รู้จัก โดยไม่ต้องรู้ว่า service ตัวจริง implement อย่างไร) และ injector (หรือเรียกว่า assembler, container, provider, factory — ตัวที่ประกอบและส่ง service เข้าไปให้ client)
Martin Fowler อธิบายไว้ในบทความชื่อดังปี 2004 ว่า DI เป็นกรณีเฉพาะของหลักการที่กว้างกว่าคือ Inversion of Control — แทนที่ code ของเราจะเป็นฝ่ายควบคุมว่าจะสร้างและค้นหา dependency อย่างไรเอง (control flow แบบเดิม) เรากลับ “พลิกกลับ” ให้ framework หรือ container เป็นฝ่ายสร้างและส่งมันเข้ามาให้แทน Fowler ยังเปรียบเทียบ DI กับอีกแนวทางหนึ่งคือ Service Locator ซึ่งให้ client เรียกหา service เองจากตัวค้นหากลาง และสรุปว่าทางเลือกไหนสำคัญน้อยกว่าหลักการที่แท้จริง คือการ แยกการตั้งค่า (configuration) ออกจากการใช้งาน (use) ของ service
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”มีหลายวิธีในการฉีด dependency เข้าไปใน class ผ่านหนึ่งในช่องทางเหล่านี้: constructor, property หรือ method
Constructor injection — วิธีที่พบบ่อยที่สุด
หัวข้อที่มีชื่อว่า “Constructor injection — วิธีที่พบบ่อยที่สุด”Constructor injection คือการส่ง instance ของ dependency เข้ามาทาง constructor ของ class ซึ่ง constructor จะเก็บ dependency นั้นไว้ใน private field เพื่อนำไปใช้ภายใน class ตามต้องการ นี่ยังเป็นตัวอย่างของ Strategy design pattern อีกด้วย class ที่ยึด Explicit Dependencies Principle จะได้ประโยชน์จาก constructor injection ได้อย่างง่ายดาย เพราะแค่ดู signature ของ constructor ก็รู้ทันทีว่า class ต้องพึ่งพาอะไรบ้าง โดยไม่ต้องไปอ่าน implementation ทั้ง file
ข้อดีสำคัญคือ constructor injection รับประกันว่า object จะถูกสร้างได้ก็ต่อเมื่อ dependency ที่จำเป็นทั้งหมดถูกจัดหาให้ครบ จึงขจัดความเป็นไปได้ที่ object จะอยู่ในสถานะไม่ถูกต้อง (ถูกสร้างขึ้นมาแล้วแต่ยังไม่ได้ตั้งค่า dependency)
Property (setter) injection — เมื่อ constructor รับ parameter เพิ่มไม่ได้
หัวข้อที่มีชื่อว่า “Property (setter) injection — เมื่อ constructor รับ parameter เพิ่มไม่ได้”Property injection คล้ายกัน แต่แทนที่จะให้ instance ผ่าน constructor กลับกำหนด dependency ผ่าน property ของ class เทคนิคนี้มีประโยชน์ในสถานการณ์ที่การสร้าง class ไม่สามารถรับ parameter ได้ เช่นในระบบ ASP.NET web forms หรือ framework บางตัวที่ควบคุมการสร้าง object เอง ข้อเสียคือ code ฝั่งผู้เรียกต้องกำหนดค่า dependency property เองเพื่อให้ class ทำงานได้ถูกต้อง หากลืมตั้งค่า จะได้ null และพังตอน runtime แทนที่จะพังตอน compile หรือตอนสร้าง object
Method injection — สำหรับ dependency แบบใช้ครั้งเดียว
หัวข้อที่มีชื่อว่า “Method injection — สำหรับ dependency แบบใช้ครั้งเดียว”Method injection คือการส่ง object ที่ทำงานร่วมกันเข้ามาเป็น parameter ของ method มันมีประโยชน์ที่สุดกับ public method ที่มี dependency ซึ่งไม่ได้ถูกใช้ที่อื่นใน class จนการใช้ constructor หรือ property injection จะเกินความจำเป็น ในทางปฏิบัติ class ที่ยึด Single Responsibility Principle มักไม่ค่อยมี method ที่มีชุด dependency ต่างจาก method อื่น ๆ ใน class มากนัก เพราะนั่นมักเป็นสัญญาณของ cohesion ที่ต่ำ และ/หรือมีความรับผิดชอบหลายอย่าง
Wikipedia ยังกล่าวถึงรูปแบบที่สี่คือ interface injection ซึ่ง client ต้อง implement interface พิเศษที่มี method สำหรับรับ dependency เข้ามา แนวทาง Avalon framework ใช้วิธีนี้ แต่ในโลก .NET สมัยใหม่แทบไม่พบการใช้งานรูปแบบนี้แล้ว เพราะ constructor injection ครอบคลุมกรณีส่วนใหญ่ได้ดีกว่า
IoC container ประกอบ graph ของ dependency ให้อัตโนมัติ
หัวข้อที่มีชื่อว่า “IoC container ประกอบ graph ของ dependency ให้อัตโนมัติ”Dependency injection เกี่ยวข้องกับ Inversion of Control container ซึ่งช่วยจัดการแบบรวมศูนย์และอัตโนมัติว่า instance ใดควรถูกจัดหาให้เมื่อต้องสร้าง object ใน .NET สมัยใหม่ container ในตัวคือ IServiceProvider — เราลงทะเบียน service ไว้ที่จุดเริ่มต้นของ app (composition root) ผ่าน IServiceCollection แล้ว container จะไล่วิเคราะห์ constructor ของแต่ละ class เพื่อประกอบ graph dependency ทั้งหมดให้เอง
แผนภาพด้านล่างแสดงว่า composition root (จุดประกอบ dependency แห่งเดียวของ app) เป็นฝ่ายสร้างทั้ง implementation จริงและ class ที่ใช้งาน แล้วฉีดผ่าน interface กลาง ทำให้ใน code ทดสอบสามารถสลับไปประกอบด้วย fake implementation แทนได้โดยไม่ต้องแก้ OrderService เลย
flowchart LR
Root[Composition Root]
Order[OrderService]
Iface[IEmailSender]
Sender[SmtpEmailSender]
Test[Unit Test]
Fake[FakeEmailSender]
Root -->|new SmtpEmailSender| Sender
Root -->|inject via constructor| Order
Sender -->|implements| Iface
Order -->|depends on| Iface
Test -->|new FakeEmailSender| Fake
Test -->|inject via constructor| Order
Fake -->|implements| Iface
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”Constructor injection: ประกาศ dependency ที่จำเป็นไว้ตรง ๆ ผ่าน interface
public interface IEmailSender{ void Send(string to, string subject, string body);}
public class OrderService{ private readonly IEmailSender _email;
// dependency ถูกส่งเข้ามา ไม่ใช่สร้างเองด้วย new public OrderService(IEmailSender email) { _email = email ?? throw new ArgumentNullException(nameof(email)); }
public void CompleteOrder(string customerEmail) { // ... บันทึกออเดอร์ ... _email.Send(customerEmail, "ขอบคุณที่สั่งซื้อ", "ออเดอร์ของคุณเสร็จสมบูรณ์แล้ว"); }}ลงทะเบียน service กับ container ที่ composition root แล้วปล่อยให้ container ประกอบ graph ให้เอง
using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// จุดเดียวที่ผูก interface เข้ากับ implementation จริงbuilder.Services.AddScoped<IEmailSender, SmtpEmailSender>();builder.Services.AddScoped<OrderService>();
using var host = builder.Build();
// container สร้าง OrderService พร้อมฉีด SmtpEmailSender ให้อัตโนมัติvar orderService = host.Services.GetRequiredService<OrderService>();ในระบบ ASP.NET Core และ .NET generic host แต่ละ service ที่ลงทะเบียนจะมี lifetime กำกับอยู่ ซึ่งกำหนดว่า container จะสร้าง instance ใหม่บ่อยแค่ไหน: Singleton (สร้างครั้งเดียวใช้ตลอดอายุ app), Scoped (สร้างใหม่ต่อ request หรือต่อ scope) และ Transient (สร้างใหม่ทุกครั้งที่ถูก resolve) การเลือก lifetime ผิดเป็น bug ที่พบบ่อย เช่น การฉีด scoped service เข้าไปใน singleton จะทำให้อายุของมันถูกยืดออกไปโดยไม่ตั้งใจ (เรียกว่า captive dependency)
ในการทดสอบหน่วย (unit test) เราสามารถสร้าง OrderService เองโดยส่ง fake หรือ mock ของ IEmailSender เข้าไปตรง ๆ โดยไม่ต้องพึ่ง container เลย นี่คือประโยชน์หลักที่ DI มอบให้กับความสามารถในการทดสอบ
ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- Loose coupling — class รู้จักกันผ่าน interface เท่านั้น ไม่ผูกกับ implementation ที่เจาะจง ทำให้สลับ implementation ได้โดยไม่แตะ code ที่ใช้งาน
- ทดสอบได้ง่ายขึ้น — ส่ง mock หรือ stub เข้าไปแทน dependency จริงได้โดยตรงใน test ไม่ต้องพึ่งฐานข้อมูลหรือบริการภายนอกจริง
- แยกความรับผิดชอบชัดเจน — การประกอบ (configuration) ของ graph dependency แยกออกจากการใช้งาน (use) ตามที่ Fowler เน้นย้ำ ทำให้จุดตั้งค่าทั้งหมดรวมอยู่ที่ composition root แห่งเดียว
- รองรับการเปลี่ยนแปลงในอนาคต — เพิ่ม implementation ใหม่ (เช่นเปลี่ยนจาก SMTP เป็นคิวข้อความ) โดยไม่กระทบ class ที่เรียกใช้ สอดคล้องกับ Open/Closed Principle
ข้อควรระวัง
- Inversion of control ทำให้ debug ยากขึ้น — Fowler เตือนไว้ตรง ๆ ว่าการที่ control flow ถูกพลิกกลับให้ container เป็นผู้ประกอบ ทำให้ตามรอย call stack ยากขึ้นเมื่อเกิดปัญหา เพราะไม่เห็นจุด
newตรง ๆ ใน code - Constructor ที่ parameter ยาวเกินไปคือกลิ่น code ไม่ใช่ปัญหาของ DI เอง แต่มักเป็นสัญญาณว่า class นั้นละเมิด Single Responsibility Principle ควรแตก class มากกว่าจะยัด dependency เพิ่มเข้าไปเรื่อย ๆ
- อย่าสับสนกับ Service Locator — การให้ class เรียกหา dependency เองจาก container กลาง (Service Locator) ยังคงซ่อน dependency ไว้ใน code implementation ทำให้ signature ของ class ไม่บอกความจริงว่ามันต้องพึ่งอะไรบ้าง ต่างจาก DI ที่ dependency ปรากฏชัดใน parameter
- Lifetime mismatch — ในระบบที่มี container จัดการ scope เช่น ASP.NET Core การฉีด scoped service เข้า singleton โดยไม่ตั้งใจจะทำให้เกิด captive dependency ซึ่งอาจนำไปสู่ bug ที่ตามรอยยาก .NET จึงมีการตรวจสอบ scope validation ในโหมด development เพื่อจับปัญหานี้ตั้งแต่เนิ่น ๆ
- มี “magic” มากเกินไป — การพึ่ง reflection และ auto-wiring หนักเกินไปอาจทำให้ IDE navigate code ไม่ได้ตรงไปตรงมา และเพิ่มต้นทุนการเรียนรู้ให้ทีมใหม่
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Dependency Inversion Principle
- Explicit Dependencies Principle
- Inversion of Control
- Single Responsibility Principle
- Service Locator (pattern ที่มักถูกเทียบกับ DI)
- Strategy Design Pattern