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

حقن الضبط وحقن الحقل

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

حقن الضبط وحقن الحقل

يدعم Spring ثلاثة أساليب لحقن التبعيات: حقن المُنشئ، وحقن الضبط (Setter)، وحقن الحقل (Field). أرسى الدرس السابق حقنَ المُنشئ باعتباره الخيار المُوصى به والافتراضي. يُغطّي هذا الدرس الأسلوبين الآخرين — ليس لأنّك تستخدمهما بحرية، بل لأنّك ستصادفهما في قواعد الكود الفعلية وتحتاج إلى فهم ما يجعل حقن المُنشئ الخيار الأكثر أمانًا. إنّ إدراك المقايضات هو ما يُتيح لك اتخاذ قرارات مدروسة بدلًا من اتباع أعراف بلا فهم.

حقن الضبط (Setter Injection)

في حقن الضبط، تُعلّق على دالة ضبط عامة (أو على مستوى الحزمة) باستخدام @Autowired. يُنشئ Spring الـ bean أولًا ثم يستدعي كل دالة ضبط مُعلَّقة لحقن التبعية.

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class ReportService { private NotificationSender notificationSender; private AuditLogger auditLogger; // يستدعيها Spring بعد الإنشاء @Autowired public void setNotificationSender(NotificationSender notificationSender) { this.notificationSender = notificationSender; } // تبعية اختيارية — تُحقن فقط إن وُجد الـ bean @Autowired(required = false) public void setAuditLogger(AuditLogger auditLogger) { this.auditLogger = auditLogger; } public void generate(Report report) { // قد يكون auditLogger فارغًا إن لم يُوجد أي bean if (auditLogger != null) { auditLogger.log("Generating report: " + report.getId()); } notificationSender.send(report.getRecipient(), report.getSummary()); } }

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

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

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

يُعلّق حقن الحقل مباشرةً على الحقل باستخدام @Autowired. يستخدم Spring الانعكاس (reflection) لتعيين القيمة بعد الإنشاء، متجاوزًا أي مُعدِّل وصول.

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService { @Autowired private PaymentGateway paymentGateway; @Autowired private InventoryRepository inventoryRepository; public OrderConfirmation placeOrder(Order order) { inventoryRepository.reserve(order.getItems()); return paymentGateway.charge(order.getTotal(), order.getPaymentMethod()); } }

حقن الحقل هو الأسلوب الأكثر إيجازًا وكان شائعًا في مشاريع Spring المبكرة لأنه يتطلب أقل قدر من الكود المتكرر. أصبح IntelliJ IDEA وتوثيق Spring الرسمي يُحذّران منه الآن: "Field injection is not recommended."

لماذا يُفضَّل حقن المُنشئ — الحجة الكاملة

التفضيل لحقن المُنشئ ليس مسألة ذوق؛ بل يرتبط مباشرةً بمشاكل هندسية ملموسة:

  1. الثبات وحقول final. يمكن الإعلان عن حقل محقون عبر المُنشئ بوصفه final. هذا يضمن تعيينه ويمنع استبداله في وقت التشغيل — مما يقضي على فئة كاملة من الأخطاء التي ينتج عنها مُتعاون مُعاد تعيينه عن طريق الخطأ أو لم يُعيَّن قط.
  2. ضمان الخلو من Null عند بدء التشغيل. إن كانت الـ bean المطلوبة مفقودة، يرمي Spring استثناء NoSuchBeanDefinitionException أثناء تهيئة سياق التطبيق، قبل تقديم أي طلب. أما مع حقن الحقل أو الضبط فتظهر التبعية المفقودة فقط كـ NullPointerException داخل طلب جارٍ، وغالبًا في بيئة الإنتاج.
  3. قابلية الاختبار بدون سياق Spring. مع حقن المُنشئ تستطيع إنشاء الفئة واختبارها بجافا عادية:
    // لا سياق Spring، لا سحر Mockito — Java خالصة OrderService service = new OrderService( new FakePaymentGateway(), new InMemoryInventoryRepository() );
    مع حقن الحقل، الحقول خاصة وغير قابلة للوصول بدون Spring أو مشغّل اختبار قائم على الانعكاس. يجب استخدام @ExtendWith(SpringExtension.class) أو ReflectionTestUtils.setField()، وكلاهما يُضيف تعقيدًا ويُبطئ حزمة الاختبارات.
  4. اكتشاف التبعيات الدائرية. سيرمي Spring استثناء BeanCurrentlyInCreationException عند بدء التشغيل إن وُجدت تبعية مُنشئ دائرية (A تحتاج B، وB تحتاج A). هذا أمر إيجابي — يُجبرك على إعادة التصميم. أما حقن الحقل أو الضبط فيسمح بصمت للرسوم البيانية الدائرية بالوجود وقد يُخفي مشاكل معمارية لأشهر.
  5. انتهاك مبدأ المسؤولية الواحدة يصبح واضحًا. حين تحتاج فئة إلى ثماني تبعيات محقونة، ثمانية معاملات مُنشئ مؤلمة للقراءة والكتابة. هذا الألم إشارة لتقسيم الفئة. ثمانية حقول @Autowired بالمقابل يسهل تجاهلها وتتراكم في صمت.
حقن الحقل وحاوية Spring مترابطان لا ينفصلان. فئة تعتمد على @Autowired في حقول خاصة لا يمكن استخدامها خارج Spring على الإطلاق — لا في اختبارات الوحدة، ولا في سكريبت دُفعي، ولا في مكتبة تستهلكها مشاريع أخرى. لقد اقترنت الفئة بشكل دائم بحاوية IoC. أما حقن المُنشئ فيُبقي الفئة كائن Java عادي؛ وSpring مجرد طريقة واحدة لتوصيله.

مزج الأساليب في قاعدة كود حقيقية

قاعدة عملية تُطبّقها الفِرق الاحترافية:

  • التبعيات الإلزامية: دائمًا حقن المُنشئ، الحقول مُعلَنة بـ final.
  • التبعيات الاختيارية (يمكن أن تغيب والفئة لا تزال تعمل): حقن الضبط مع @Autowired(required = false).
  • حقن الحقل: مقتصر على فئات الاختبار قصيرة العمر (مثل اختبارات التكامل عبر @SpringBootTest حيث تريد فقط سحب bean من السياق دون كتابة مُنشئ). لا تستخدمه أبدًا في beans الإنتاج.
@Service public class ExportService { // إلزامية — final، محقونة عبر المُنشئ private final ReportRepository reportRepository; private final StorageClient storageClient; // اختيارية — محقونة عبر الضبط، قد تكون null private MetricsCollector metricsCollector; public ExportService(ReportRepository reportRepository, StorageClient storageClient) { this.reportRepository = reportRepository; this.storageClient = storageClient; } @Autowired(required = false) public void setMetricsCollector(MetricsCollector metricsCollector) { this.metricsCollector = metricsCollector; } public void export(long reportId) { Report r = reportRepository.findById(reportId).orElseThrow(); String url = storageClient.upload(r.toBytes()); if (metricsCollector != null) { metricsCollector.record("export.success"); } } }
اختصار Lombok: عَلِّق الفئة بـ @RequiredArgsConstructor (من Project Lombok) وسيُولَّد مُنشئ لجميع الحقول final وقت الترجمة بدون أي كود متكرر. منذ Spring 4.3، إن كان هناك مُنشئ واحد فقط، فإن @Autowired عليه اختياري — يستنتجه Spring تلقائيًا.

الخلاصة

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