ในโลกของการพัฒนาซอฟต์แวร์ขนาดใหญ่และระบบที่ซับซ้อน การจัดการการพึ่งพา (Dependency Management) ถือเป็นหัวใจสำคัญที่กำหนดความยืดหยุ่นและความสามารถในการบำรุงรักษาของโค้ดเบสทั้งหมด หากเราปล่อยให้คลาสต่างๆ สร้างอินสแตนซ์ของตัวเองโดยตรง ระบบจะกลายเป็นโครงสร้างแบบ Monolithic ที่แก้ไขยาก การทำความเข้าใจว่าทรัพยากรใดควรถูกสร้างขึ้นเมื่อไหร่ และควรมีอายุการใช้งานนานแค่ไหน จึงเป็นทักษะที่นักพัฒนาอาวุโสทุกคนต้องเชี่ยวชาญ
หัวใจของการจัดการ Dependency Injection (DI) คือการกำหนด “วงจรชีวิต” (Lifecycle) ของ Object ที่ถูก Inject เข้ามา การทำความเข้าใจว่า Service นั้นๆ ควรมีอายุสั้นแค่ไหน หรือควรคงอยู่ตลอดช่วงเวลาใดของแอปพลิเคชัน จะช่วยป้องกันปัญหาเรื่อง State Leakage, Memory Leaks และ Race Conditions ได้อย่างมีประสิทธิภาพ
🎯 3 วงจรชีวิตหลักของ Dependency Injection
ความแตกต่างระหว่าง Transient, Scoped, และ Singleton ไม่ใช่แค่คำศัพท์ แต่คือปรัชญาการออกแบบระบบ (Design Philosophy) ที่ส่งผลต่อพฤติกรรมของแอปพลิเคชัน โดยแต่ละรูปแบบจะควบคุมขอบเขตการสร้างและทำลาย Object อย่างชัดเจน
1. Transient (Ephemeral)
หลักการ: ทุกครั้งที่มีการร้องขอ (Request) Service นี้จะถูกสร้างอินสแตนซ์ใหม่เสมอ ไม่ว่าจะถูกเรียกใช้กี่ครั้งก็ตาม
เหมาะสำหรับ: Utility classes หรือ Service ที่ไม่มีสถานะ (Stateless) เช่น ตัวแปลงข้อมูล (Mapper) หรือตัวคำนวณที่ไม่ต้องจดจำข้อมูลระหว่างการเรียกใช้
2. Scoped (Per-Scope)
หลักการ: Service จะถูกสร้างอินสแตนซ์เพียงครั้งเดียวต่อขอบเขต (Scope) การทำงานหนึ่งๆ เช่น ในบริบทของ HTTP Request หนึ่งๆ หรือ Transaction หนึ่งๆ
เหมาะสำหรับ: การจัดการฐานข้อมูล (Database Context) หรือ Repository ที่ต้องการรักษา Session ข้อมูลตลอดช่วงการทำงานของ Request นั้นๆ เพื่อให้มั่นใจว่าการเปลี่ยนแปลงข้อมูลจะถูกจัดการอย่างเป็นชุด (Atomic Transaction)
3. Singleton (Application Lifetime)
หลักการ: Service จะถูกสร้างอินสแตนซ์เพียงครั้งเดียวตลอดอายุการใช้งานของแอปพลิเคชัน ไม่ว่าจะมีการร้องขอจากส่วนใดก็ตาม
ข้อควรระวัง: ควรใช้กับ Service ที่มีสถานะ (Stateful) และเป็นข้อมูลที่แชร์กันได้อย่างปลอดภัยเท่านั้น เช่น Configuration Manager หรือ Logger ที่ต้องการเข้าถึงการตั้งค่าหลักของระบบ
💡 การประยุกต์ใช้ในบริบทของ PostgreSQL
แม้ว่า DI จะเป็นแนวคิดของโค้ดแอปพลิเคชัน แต่การจัดการ State ของข้อมูลที่มาจาก PostgreSQL ก็ต้องคำนึงถึง Lifecycle เช่น การใช้ Scoped Service ในการจัดการ Database Context จะช่วยให้มั่นใจได้ว่าทุกการ Query ภายใน Request เดียวกันจะใช้ Connection เดียวกันและอยู่ใน Transaction เดียวกัน
สมมติว่าเรามี Service ที่ต้องจัดการรายการสินค้า (Product) ซึ่งต้องมีการเชื่อมต่อฐานข้อมูล (PostgreSQL) ในระหว่างการทำงานของ Request:
SQL
-- ตัวอย่างการใช้ Transaction Scope ใน PostgreSQL (เทียบเท่า Scoped Service) </code><pre><code class="language-sql">BEGIN; </code><pre><code class="language-sql">-- Query 1: ดึงข้อมูลสินค้า (Product ID 1) </code><pre><code class="language-sql">SELECT * FROM products WHERE id = 1; </code><pre><code class="language-sql">-- Query 2: อัปเดตสถานะสินค้า </code><pre><code class="language-sql">UPDATE products SET status = 'processed' WHERE id = 1; </code><pre><code class="language-sql">COMMIT;
การใช้ `BEGIN` และ `COMMIT` ในที่นี้คือการจำลอง ‘Scope’ การทำงาน ทำให้มั่นใจได้ว่าทุก Query จะถูกรวมอยู่ในขอบเขตการทำธุรกรรมเดียว (Transaction Scope) ซึ่งเป็นหลักการที่ Scoped Service ควรดูแล
✅ บทสรุป: การเลือก Lifecycle ที่ถูกต้อง
การเลือกวงจรชีวิตที่ถูกต้องคือการตัดสินใจเชิงสถาปัตยกรรมที่สำคัญที่สุด หากคุณใช้ Singleton กับข้อมูลที่ควรเป็น per-request อาจทำให้เกิดปัญหาข้อมูลทับซ้อน (Data Overwrite) ได้ง่าย
- Transient: สำหรับ Utility, Stateless.
- Scoped: สำหรับ Database Context, Session, Transaction.
- Singleton: สำหรับ Configuration, Global Cache ที่อ่านอย่างเดียว.
การทำความเข้าใจหลักการเหล่านี้จะช่วยให้โค้ดเบสของคุณมีความเสถียร ปลอดภัย และง่ายต่อการขยายตัวในระยะยาว