حقن التبعيات ودورة حياة الـ Bean

شرح حقن التبعيات

18 دقيقة الدرس 1 من 13

شرح حقن التبعيات

حقن التبعيات (Dependency Injection — DI) من تلك الأفكار التي تبدو مجردة في المرة الأولى التي تصادفها، غير أن كل قاعدة كود Java احترافية تعتمد عليها. في جوهرها، الفكرة بسيطة: لا تنشئ الفئة الكائنات التي تعتمد عليها — بل يُوفّرها شيء خارجها. هذا التحوّل في المسؤولية هو كل الفكرة، وتداعياته على التصميم وقابلية الاختبار والصيانة عميقة جدًا.

المشكلة التي يحلّها حقن التبعيات

لنبدأ بنقطة انطلاق واقعية: خدمة OrderService ترسل رسائل تأكيد بالبريد الإلكتروني.

// بدون DI — الفئة تتحكم في تبعيتها الخاصة public class OrderService { private final EmailClient emailClient = new SmtpEmailClient("smtp.example.com", 587); public void placeOrder(Order order) { // ... منطق الأعمال ... emailClient.send(order.getCustomerEmail(), "تم تأكيد الطلب", buildBody(order)); } }

يبدو هذا بسيطًا، لكنه يُنشئ عدة مشاكل خفية:

  • تنفيذ مُضمَّن في الكود. OrderService مرتبطة بشكل دائم بـ SmtpEmailClient. التحوّل إلى SendGrid أو SES يستلزم تعديل هذه الفئة لا مجرد تغيير الإعدادات.
  • يستحيل اختبار الوحدة بشكل معزول. كل اختبار يستدعي placeOrder سيحاول فتح مقبس SMTP حقيقي. تصبح الاختبارات بطيئة وغير موثوقة وتعتمد على البنية التحتية.
  • إعدادات مخفية. مضيف SMTP ومنفذه مدفونان داخل OrderService. تغييرهما بين بيئة التطوير والإنتاج يتطلب تعديل كود منطق الأعمال.
  • اقتران وثيق (Tight Coupling). إن تغيّر مُنشئ SmtpEmailClient، وجب تغيير OrderService أيضًا، حتى لو لم يتغيّر العقد الأعلى مستوى (إرسال البريد الإلكتروني).

حقن التبعيات كحل

الحل مباشر: بدلًا من إنشاء SmtpEmailClient داخليًا، اقبل واجهة EmailClient من الخارج.

// تعريف العقد public interface EmailClient { void send(String to, String subject, String body); } // التنفيذ في الإنتاج public class SmtpEmailClient implements EmailClient { public SmtpEmailClient(String host, int port) { /* ... */ } @Override public void send(String to, String subject, String body) { /* منطق SMTP */ } } // OrderService لا تعرف بعد الآن كيف يُرسَل البريد public class OrderService { private final EmailClient emailClient; // تعتمد على الواجهة // التبعية تُحقَن عبر المُنشئ public OrderService(EmailClient emailClient) { this.emailClient = emailClient; } public void placeOrder(Order order) { // ... منطق الأعمال ... emailClient.send(order.getCustomerEmail(), "تم تأكيد الطلب", buildBody(order)); } }

الآن يمكن للاختبار حقن بديل خفيف الوزن دون أي اتصال بـ SMTP:

class OrderServiceTest { @Test void placeOrder_sendsConfirmationEmail() { // التهيئة — بديل بسيط في الذاكرة، لا اتصال SMTP حقيقي List<String> sentTo = new ArrayList<>(); EmailClient fake = (to, subject, body) -> sentTo.add(to); OrderService service = new OrderService(fake); Order order = new Order("customer@example.com"); // التنفيذ service.placeOrder(order); // التحقق assertEquals(1, sentTo.size()); assertEquals("customer@example.com", sentTo.get(0)); } }
الفكرة الجوهرية: حقن التبعيات ليس ميزة إطار عمل — بل هو مبدأ تصميم. الكود أعلاه لا يستخدم أي تعليق توضيحي لـ Spring ومع ذلك يستفيد كليًا من DI. تُؤتمت Spring وJakarta CDI وGuice عملية الربط، لكن المبدأ مستقل عن أي إطار عمل.

أنواع الحقن الثلاثة

ثمة ثلاث آليات قياسية لتسليم التبعية إلى فئة ما. لكلٍّ منها تركيب نحوي مختلف ومقايضات مختلفة — دروس لاحقة تتناول كلًا منها بالتفصيل، لكنك تحتاج الآن للتعرف عليها.

1. حقن المُنشئ (Constructor Injection)

تُمرَّر التبعية كمعامل للمُنشئ. هذا هو الأسلوب المفضّل في تطبيقات Spring الحديثة.

@Service public class OrderService { private final EmailClient emailClient; // لا يتغيّر بعد الإنشاء @Autowired // اختياري في Spring 6 عند وجود مُنشئ واحد فقط public OrderService(EmailClient emailClient) { this.emailClient = emailClient; } }

المزايا: يمكن جعل الحقل final (يضمن عدم كونه null بعد الإنشاء)؛ يستحيل إنشاء الفئة دون تبعياتها؛ وشجرة التبعيات صريحة في توقيع المُنشئ.

2. حقن المُعيِّن (Setter Injection)

تُوفَّر التبعية عبر دالة معيِّن بعد إنشاء الكائن.

@Service public class ReportService { private NotificationClient notificationClient; @Autowired public void setNotificationClient(NotificationClient client) { this.notificationClient = client; } }

مفيد حين تكون التبعية اختيارية فعلًا، أو حين تحتاج لدعم المراجع الدائرية (وإن كانت المراجع الدائرية تشير عادةً إلى مشكلة في التصميم). الجانب السلبي: لا يمكن جعل الحقل final، لذا يمكن نظريًا استبدال التبعية أو تركها null.

3. حقن الحقل (Field Injection)

يحقن إطار العمل مباشرةً في الحقل باستخدام الانعكاس (reflection)، متجاوزًا المُنشئات والمُعيِّنات كليًا.

@Service public class LegacyService { @Autowired private PaymentGateway gateway; // Spring يكتب مباشرة في هذا الحقل }
حقن الحقل مريح لكنه إشكالي. لا يمكن جعل الحقل final، التبعية غير مرئية في الواجهة البرمجية العامة، وإنشاء الفئة في اختبار وحدة دون سياق Spring يستلزم حيلًا بالانعكاس. فضّل حقن المُنشئ لجميع التبعيات الإلزامية. احتفظ بحقن الحقل للكود النمطي قصير العمر أو فئات الاختبار فقط (@MockBean في اختبارات Spring Boot حالة استخدام مشروعة).

لماذا يُحسِّن حقن التبعيات التصميم

يُطبِّق DI مبدأ انعكاس التبعيات (حرف D في SOLID): يجب أن تعتمد الوحدات عالية المستوى (OrderService) على التجريدات (EmailClient)، لا على التنفيذات الفعلية (SmtpEmailClient). لهذا تداعيات عملية مباشرة:

  • تبديل التنفيذات دون تغيير منطق الأعمال. وجِّه رسائل البريد عبر طابور لتحسين الأداء، أو أسكتها في بيئة التجهيز، بتوفير bean مختلف لـ EmailClientOrderService لا تُمَسّ.
  • اختبار كل فئة باستقلالية. يوفّر اختبار الوحدة تبعية وهمية أو محاكاة؛ لا حاوية ولا قاعدة بيانات ولا SMTP. تعمل الاختبارات في ميلي ثانية.
  • الإعدادات في جذر التركيب. المكان الذي تُربط فيه الكائنات معًا (فئة @Configuration في Spring أو سياق التطبيق) هو المكان الوحيد الذي يعرف الأنواع الفعلية والإعدادات الخارجية. تظل فئات منطق الأعمال جاهلة بتفاصيل البنية التحتية.
  • التطوير المتوازي. يمكن للفرق العمل على OrderService وSmtpEmailClient في آن واحد، متفقين فقط على عقد واجهة EmailClient.

حاوية Spring بوصفها المُحقِن

في تطبيق Java عادي، تربط التبعيات يدويًا في دالة main — يُسمّى هذا جذر التركيب (Composition Root):

public class Main { public static void main(String[] args) { EmailClient emailClient = new SmtpEmailClient("smtp.example.com", 587); OrderService orderService = new OrderService(emailClient); // استخدام orderService ... } }

في تطبيق Spring، يُعدّ ApplicationContext جذرَ التركيب. تُضيف للفئات تعليقات توضيحية كـ @Service و@Repository و@Component؛ يفحصها Spring وينشئها بالترتيب الصحيح ويحقن تبعياتها. نادرًا ما تكتب كود الربط يدويًا.

فكّر في Spring كمصنع ذكي. يقرأ تعليقاتك التوضيحية، ويكتشف ما تحتاجه كل فئة، ويبني كل شيء بالترتيب الصحيح، ويسلّمك سياق تطبيق مُجمَّعًا بالكامل. DI هو المبدأ؛ Spring هو الأتمتة.

الخلاصة

حقن التبعيات يعني تزويد الفئة بالكائنات التي تحتاجها بدلًا من السماح لها بإنشائها. الآليات الثلاث — حقن المُنشئ والمُعيِّن والحقل — تحقق هذا كلها، لكن حقن المُنشئ مفضّل بشدة لأنه ينتج فئات غير قابلة للتغيير وقابلة للاختبار وموثّقة ذاتيًا. يُطبِّق DI مبدأ انعكاس التبعيات، ويفصل منطق الأعمال عن البنية التحتية، وهو الأساس الذي يقوم عليه نظام Spring البيئي بأكمله. في الدرس القادم ستُطبّق حقن المُنشئ بعمق، بما في ذلك كيفية حلّ Spring لمعاملات المُنشئ وتلبيتها تلقائيًا.