We are still cooking the magic in the way!
الحقن عبر المُنشئ
الحقن عبر المُنشئ
من بين الأساليب الثلاثة التي يدعمها Spring لحقن التبعيات — الحقن عبر المُنشئ، والحقن عبر الضابط (setter)، والحقن عبر الحقل (field) — يُوصي فريق Spring باعتماد الحقن عبر المُنشئ خيارًا افتراضيًا. ينتج عن هذا الأسلوب كائناتٌ تُشبَع تبعياتها في اللحظة التي تُولد فيها، مما يجعل الكود أسهل اختبارًا وأوضح فهمًا وأصعب إساءةً في الاستخدام. يستعرض هذا الدرس السبب في ذلك وكيف يبدو هذا الأسلوب في الكود الإنتاجي الحقيقي.
لماذا يُفضَّل الحقن عبر المُنشئ؟
الميزة الجوهرية هي عدم القابلية للتغيير (Immutability). حين تصل التبعية عبر المُنشئ يمكنك تعريف الحقل بالكلمة المفتاحية final. لا يمكن إعادة تعيين حقل final بعد انتهاء تنفيذ المُنشئ — وهو ضمان يكفله نموذج الذاكرة في JVM نفسه. يعني ذلك:
- لا يمكن لأي فئة تبديل التبعية في أثناء عمل الكائن بعد إنشائه.
- الكائن آمن تلقائيًا من حيث التزامن (thread-safe) فيما يخص حقوله المحقونة.
- يُلزمك المُصرِّف (compiler) بتوفير كل تبعية مطلوبة — معامِل مفقود يعني خطأ في وقت التصريف لا
NullPointerExceptionفي وقت التشغيل.
ثمة فائدة عملية ثانية: لأن التبعيات هي معاملات المُنشئ، يمكنك إنشاء كائن من الفئة في اختبار وحدة عادي باستخدام new ومرير كائنات اختبار بديلة (mocks) كمعاملات. لا سياق Spring، ولا استخدام للـ reflection، ولا حاجة لـ @SpringBootTest في اختبارات الوحدة البسيطة.
مثال واقعي
تخيّل خدمة تجارة إلكترونية ترسل رسائل تأكيد الطلبات. تعتمد على متعاونَين: OrderRepository لقراءة الطلبات من قاعدة البيانات، وNotificationService لإرسال البريد الإلكتروني. إليك طريقة ربطهما بالحقن عبر المُنشئ:
كلا الحقلَين final. لا يوجد مُنشئ افتراضي. إن لم يعثر Spring على بين (bean) من نوع OrderRepository أو NotificationService في السياق، فسيفشل عند بدء التشغيل برسالة خطأ واضحة — لا صمتًا يتحوّل إلى خطأ لاحق عند أول استدعاء لـ confirmOrder.
@Autowired. هذه هي الحالة الشائعة وتُبقي الكود خاليًا من التعليقات التوضيحية الزائدة. أضف @Autowired فقط حين تمتلك الفئة مُنشئات متعددة وتحتاج إلى إخبار Spring أيها يستخدم.
تعليقة @Autowired على المُنشئات
قبل Spring 4.3، كان كل مُنشئ مخصص للحقن يجب تزيينه بهذه التعليقة. لا تزال ترى ذلك في قواعد الكود القديمة، وتحتاج إليه حين تمتلك الفئة أكثر من مُنشئ:
@Autowired على المُنشئ تقول لـ Spring: "استخدم هذا المُنشئ عند تجميع البين." قيمة الخاصية required في التعليقة هي true افتراضيًا، لذا فإن أي معامل غير مُشبَع يُلقي استثناءً عند بدء التشغيل. نادرًا ما يكون ضبط required = false صحيحًا في الحقن عبر المُنشئ — افضّل الحقن عبر الضابط (setter) للمتعاونين الاختياريين حقًا.
عدم القابلية للتغيير عمليًا — كلمة final
تعريف الحقول بـ final ليس اختيارًا أسلوبيًا فحسب؛ بل يُفعّل ضمان "happens-before" في JVM. أي خيط تنفيذ (thread) يرى الكائن المُنشأ بالكامل يرى أيضًا القيم المُسندة في المُنشئ. هذا مهم في Spring لأن البينز تُنشأ في خيط واحد (خيط بدء تشغيل السياق) ثم يستخدمها لاحقًا خيوط طلبات متعددة.
final. إذا وجدت نفسك عاجزًا عن ذلك — ربما لأن الحقل يجب إعادة تعيينه لاحقًا — فاسأل نفسك إن كنت فعلًا تحتاج إلى الحقن عبر الضابط أو ما إذا كان يجب إعادة النظر في التصميم. معظم الخدمات الحقيقية تمتلك متعاونين ثابتين لا يتغيرون بعد الإنشاء.
الحقن عبر المُنشئ واختبار الوحدات
تظهر إحدى أكبر الفوائد في الاختبارات. قارن بين الأسلوبين:
تُنشئ الكائن مباشرةً بكائنات اختبار بديلة — لا @MockBean، ولا تأخير بدء تشغيل سياق التطبيق، واختبارات تنتهي في أجزاء من الثانية. يُلغي الحقن عبر الحقل هذا الخيار كليًا؛ @InjectMocks من Mockito يعمل بالـ reflection وهو هشّ ولا يزال يحتاج إلى اكتشاف كل حقل وحقنه فرديًا.
حين يغدو الحقن عبر المُنشئ مرهقًا
إذا نما المُنشئ إلى أكثر من أربعة أو خمسة معاملات فهذه رائحة كود (code smell)، وليست مبررًا للتحوّل إلى الحقن عبر الحقل. يعني ذلك عادةً أن الفئة تضطلع بمسؤوليات كثيرة جدًا. أعد الهيكلة باستخراج مجموعة متماسكة من التبعيات في خدمة جديدة، أو بتحديد التبعيات التي تنتمي إلى مستوى تجريد جديد.
@Autowired على الحقول الخاصة فقط لتجنب مُنشئ طويل يجعل قائمة التبعيات غير مرئية — لكنه لا يُقصّرها. تستطيع أدوات مثل IntelliJ تحذيرك حين يمتلك بين Spring عددًا من الحقول المحقونة يتجاوز حدًا تُعيّنه؛ فعّل هذا الفحص.
@RequiredArgsConstructor من Lombok
تلجأ كثير من الفرق إلى Lombok للتخلص من الكود النمطي المتكرر. تُولّد التعليقة @RequiredArgsConstructor تلقائيًا مُنشئًا عامًا لكل حقل final (ولكل حقل غير null بلا قيمة افتراضية) وقت التصريف:
المُنشئ المولَّد مطابق تمامًا لما ستكتبه يدويًا. يراه Spring ويستخدمه. الفئة قابلة للاختبار كاملًا عبر new OrderConfirmationService(repo, notification) في الاختبارات. هذا النمط هو ما ستصادفه في معظم مشاريع Spring الحديثة.
الخلاصة
الحقن عبر المُنشئ هو أسلوب الحقن المُفضَّل في Spring لأنه يُلزم بتوفير التبعيات المطلوبة عند بدء التشغيل، ويُتيح تعريف الحقول بـ final لضمان عدم قابلية التغيير وسلامة التزامن، وينتج فئات يسهل اختبارها كوحدات بمعزل عن سياق Spring. يُكتشف المُنشئ الوحيد ويُستخدم تلقائيًا، ولا تُحتاج @Autowired إلا عند وجود مُنشئات متعددة. إذا طال المُنشئ كثيرًا فعامِله كإشارة تصميم لا مبررًا للتحوّل إلى الحقن عبر الحقل. في الدرس القادم ستتعرف على الحقن عبر الضابط والحقل — الأسلوبان اللذان لهما حالات استخدام مشروعة لكنها أضيق.