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

الحقن عبر المُنشئ

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

الحقن عبر المُنشئ

من بين الأساليب الثلاثة التي يدعمها Spring لحقن التبعيات — الحقن عبر المُنشئ، والحقن عبر الضابط (setter)، والحقن عبر الحقل (field) — يُوصي فريق Spring باعتماد الحقن عبر المُنشئ خيارًا افتراضيًا. ينتج عن هذا الأسلوب كائناتٌ تُشبَع تبعياتها في اللحظة التي تُولد فيها، مما يجعل الكود أسهل اختبارًا وأوضح فهمًا وأصعب إساءةً في الاستخدام. يستعرض هذا الدرس السبب في ذلك وكيف يبدو هذا الأسلوب في الكود الإنتاجي الحقيقي.

لماذا يُفضَّل الحقن عبر المُنشئ؟

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

  • لا يمكن لأي فئة تبديل التبعية في أثناء عمل الكائن بعد إنشائه.
  • الكائن آمن تلقائيًا من حيث التزامن (thread-safe) فيما يخص حقوله المحقونة.
  • يُلزمك المُصرِّف (compiler) بتوفير كل تبعية مطلوبة — معامِل مفقود يعني خطأ في وقت التصريف لا NullPointerException في وقت التشغيل.

ثمة فائدة عملية ثانية: لأن التبعيات هي معاملات المُنشئ، يمكنك إنشاء كائن من الفئة في اختبار وحدة عادي باستخدام new ومرير كائنات اختبار بديلة (mocks) كمعاملات. لا سياق Spring، ولا استخدام للـ reflection، ولا حاجة لـ @SpringBootTest في اختبارات الوحدة البسيطة.

مثال واقعي

تخيّل خدمة تجارة إلكترونية ترسل رسائل تأكيد الطلبات. تعتمد على متعاونَين: OrderRepository لقراءة الطلبات من قاعدة البيانات، وNotificationService لإرسال البريد الإلكتروني. إليك طريقة ربطهما بالحقن عبر المُنشئ:

package com.example.shop.service; import com.example.shop.repository.OrderRepository; import com.example.shop.service.NotificationService; import org.springframework.stereotype.Service; @Service public class OrderConfirmationService { private final OrderRepository orderRepository; private final NotificationService notificationService; // Spring يكتشف المُنشئ الوحيد تلقائيًا — لا حاجة لـ @Autowired public OrderConfirmationService(OrderRepository orderRepository, NotificationService notificationService) { this.orderRepository = orderRepository; this.notificationService = notificationService; } public void confirmOrder(Long orderId) { var order = orderRepository.findByIdOrThrow(orderId); order.markConfirmed(); orderRepository.save(order); notificationService.sendConfirmation(order); } }

كلا الحقلَين final. لا يوجد مُنشئ افتراضي. إن لم يعثر Spring على بين (bean) من نوع OrderRepository أو NotificationService في السياق، فسيفشل عند بدء التشغيل برسالة خطأ واضحة — لا صمتًا يتحوّل إلى خطأ لاحق عند أول استدعاء لـ confirmOrder.

قاعدة المُنشئ الوحيد: منذ Spring 4.3، إذا كانت الفئة تمتلك مُنشئًا واحدًا فقط، يحقنه Spring تلقائيًا دون الحاجة إلى @Autowired. هذه هي الحالة الشائعة وتُبقي الكود خاليًا من التعليقات التوضيحية الزائدة. أضف @Autowired فقط حين تمتلك الفئة مُنشئات متعددة وتحتاج إلى إخبار Spring أيها يستخدم.

تعليقة @Autowired على المُنشئات

قبل Spring 4.3، كان كل مُنشئ مخصص للحقن يجب تزيينه بهذه التعليقة. لا تزال ترى ذلك في قواعد الكود القديمة، وتحتاج إليه حين تمتلك الفئة أكثر من مُنشئ:

@Service public class ReportService { private final OrderRepository orderRepository; private final AuditLogger auditLogger; @Autowired // ضروري هنا لأن مُنشئًا ثانيًا موجود أدناه public ReportService(OrderRepository orderRepository, AuditLogger auditLogger) { this.orderRepository = orderRepository; this.auditLogger = auditLogger; } // مُنشئ ذو نطاق package يستخدمه مساعد اختبار ReportService(OrderRepository orderRepository) { this.orderRepository = orderRepository; this.auditLogger = AuditLogger.noop(); } }

@Autowired على المُنشئ تقول لـ Spring: "استخدم هذا المُنشئ عند تجميع البين." قيمة الخاصية required في التعليقة هي true افتراضيًا، لذا فإن أي معامل غير مُشبَع يُلقي استثناءً عند بدء التشغيل. نادرًا ما يكون ضبط required = false صحيحًا في الحقن عبر المُنشئ — افضّل الحقن عبر الضابط (setter) للمتعاونين الاختياريين حقًا.

عدم القابلية للتغيير عمليًا — كلمة final

تعريف الحقول بـ final ليس اختيارًا أسلوبيًا فحسب؛ بل يُفعّل ضمان "happens-before" في JVM. أي خيط تنفيذ (thread) يرى الكائن المُنشأ بالكامل يرى أيضًا القيم المُسندة في المُنشئ. هذا مهم في Spring لأن البينز تُنشأ في خيط واحد (خيط بدء تشغيل السياق) ثم يستخدمها لاحقًا خيوط طلبات متعددة.

// صحيح — الحقول final تُنشر بأمان لجميع الخيوط private final OrderRepository orderRepository; // ✓ // هشّ — حقل غير final يمكن، نظريًا، رؤيته كـ null // من خيط يعمل قبل انتشار كتابة المُنشئ private OrderRepository orderRepository; // ✗ (عند استخدام الحقن)
اجعل كل حقل محقون final. إذا وجدت نفسك عاجزًا عن ذلك — ربما لأن الحقل يجب إعادة تعيينه لاحقًا — فاسأل نفسك إن كنت فعلًا تحتاج إلى الحقن عبر الضابط أو ما إذا كان يجب إعادة النظر في التصميم. معظم الخدمات الحقيقية تمتلك متعاونين ثابتين لا يتغيرون بعد الإنشاء.

الحقن عبر المُنشئ واختبار الوحدات

تظهر إحدى أكبر الفوائد في الاختبارات. قارن بين الأسلوبين:

// باستخدام الحقن عبر المُنشئ — بدون Spring، بدون reflection، Java خالصة class OrderConfirmationServiceTest { @Test void confirmsOrderAndSendsEmail() { var repo = new FakeOrderRepository(); var notification = new SpyNotificationService(); var service = new OrderConfirmationService(repo, notification); service.confirmOrder(42L); assertTrue(repo.findById(42L).isConfirmed()); assertTrue(notification.wasCalledWith(42L)); } }

تُنشئ الكائن مباشرةً بكائنات اختبار بديلة — لا @MockBean، ولا تأخير بدء تشغيل سياق التطبيق، واختبارات تنتهي في أجزاء من الثانية. يُلغي الحقن عبر الحقل هذا الخيار كليًا؛ @InjectMocks من Mockito يعمل بالـ reflection وهو هشّ ولا يزال يحتاج إلى اكتشاف كل حقل وحقنه فرديًا.

حين يغدو الحقن عبر المُنشئ مرهقًا

إذا نما المُنشئ إلى أكثر من أربعة أو خمسة معاملات فهذه رائحة كود (code smell)، وليست مبررًا للتحوّل إلى الحقن عبر الحقل. يعني ذلك عادةً أن الفئة تضطلع بمسؤوليات كثيرة جدًا. أعد الهيكلة باستخراج مجموعة متماسكة من التبعيات في خدمة جديدة، أو بتحديد التبعيات التي تنتمي إلى مستوى تجريد جديد.

لا تستخدم الحقن عبر الحقل لإخفاء مشكلة في التصميم. التحوّل إلى @Autowired على الحقول الخاصة فقط لتجنب مُنشئ طويل يجعل قائمة التبعيات غير مرئية — لكنه لا يُقصّرها. تستطيع أدوات مثل IntelliJ تحذيرك حين يمتلك بين Spring عددًا من الحقول المحقونة يتجاوز حدًا تُعيّنه؛ فعّل هذا الفحص.

@RequiredArgsConstructor من Lombok

تلجأ كثير من الفرق إلى Lombok للتخلص من الكود النمطي المتكرر. تُولّد التعليقة @RequiredArgsConstructor تلقائيًا مُنشئًا عامًا لكل حقل final (ولكل حقل غير null بلا قيمة افتراضية) وقت التصريف:

import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor // Lombok يولّد المُنشئ public class OrderConfirmationService { private final OrderRepository orderRepository; private final NotificationService notificationService; public void confirmOrder(Long orderId) { var order = orderRepository.findByIdOrThrow(orderId); order.markConfirmed(); orderRepository.save(order); notificationService.sendConfirmation(order); } }

المُنشئ المولَّد مطابق تمامًا لما ستكتبه يدويًا. يراه Spring ويستخدمه. الفئة قابلة للاختبار كاملًا عبر new OrderConfirmationService(repo, notification) في الاختبارات. هذا النمط هو ما ستصادفه في معظم مشاريع Spring الحديثة.

الخلاصة

الحقن عبر المُنشئ هو أسلوب الحقن المُفضَّل في Spring لأنه يُلزم بتوفير التبعيات المطلوبة عند بدء التشغيل، ويُتيح تعريف الحقول بـ final لضمان عدم قابلية التغيير وسلامة التزامن، وينتج فئات يسهل اختبارها كوحدات بمعزل عن سياق Spring. يُكتشف المُنشئ الوحيد ويُستخدم تلقائيًا، ولا تُحتاج @Autowired إلا عند وجود مُنشئات متعددة. إذا طال المُنشئ كثيرًا فعامِله كإشارة تصميم لا مبررًا للتحوّل إلى الحقن عبر الحقل. في الدرس القادم ستتعرف على الحقن عبر الضابط والحقل — الأسلوبان اللذان لهما حالات استخدام مشروعة لكنها أضيق.