إطار Spring وحاوية IoC

عكس التحكم (IoC)

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

عكس التحكم (IoC)

إذا كتبت كودًا بـ Java الخالصة لأي فترة من الزمن، فأنت قد كتبت بالتأكيد كودًا كهذا: تقرّر فئة ما ما تحتاجه، تُنشئه، ثم تستخدمه. هذا الترتيب طبيعي، لكنه يُشابك كودك بمجرد أن تحتاج إلى استبدال تنفيذ معين، أو إجراء اختبار معزول، أو إعادة استخدام مكوّن في سياق مختلف. عكس التحكم (IoC) هو مبدأ التصميم الذي يفكّ هذا التشابك بقلب مسؤولية إنشاء التبعيات.

المشكلة الكلاسيكية: التبعيات المُشفَّرة مباشرةً

لنأخذ خدمة معالجة طلبات بسيطة:

public class OrderService { private final PaymentGateway gateway; private final NotificationService notifier; public OrderService() { this.gateway = new StripeGateway(); // مُشفَّر مباشرةً this.notifier = new EmailNotifier(); // مُشفَّر مباشرةً } public void placeOrder(Order order) { gateway.charge(order.total()); notifier.send(order.customerEmail(), "Order confirmed"); } }

ما الذي يسوء هنا؟

  • لا يمكن اختباره في عزل. في كل مرة تُنشئ فيها OrderService في اختبار، تحصل على StripeGateway حقيقية — أي استدعاءات HTTP حقيقية وعمليات شحن حقيقية.
  • التغيير مؤلم. التبديل من Stripe إلى PayPal يستلزم تعديل OrderService، وهي لا ينبغي أن تهتم باختيار مزوّد الدفع.
  • اقتران خفي. الفئة تعرف الأنواع الملموسة التي تحتاجها، وتنتشر هذه المعرفة في قاعدة الكود ويجب تحديثها في كل فئة تُنشئ تبعياتها بنفسها.

قلب التحكم

يقول مبدأ IoC: لا تُنشئ تبعياتك — أعلن عنها ودع شيئًا آخر يوفّرها. أبسط أشكاله هو الحقن بالمُنشئ:

public class OrderService { private final PaymentGateway gateway; private final NotificationService notifier; // التبعيات تُعطى لنا، ولا نُنشئها نحن public OrderService(PaymentGateway gateway, NotificationService notifier) { this.gateway = gateway; this.notifier = notifier; } public void placeOrder(Order order) { gateway.charge(order.total()); notifier.send(order.customerEmail(), "Order confirmed"); } }

تعتمد OrderService الآن على واجهات لا على فئات ملموسة. لم تعد تعرف — أو تهتم — إن كان gateway محوّل Stripe أو PayPal أو كائنًا وهميًا كتبته في اختبار. المُستدعي (أو الحاوية) من يقرر.

الفكرة الجوهرية: IoC مبدأ وليس ميزة إطار عمل. يمكنك تطبيقه بـ Java الخالصة بأن تجعل دالة main توصّل الكائنات معًا. Spring يُؤتمت هذا التوصيل ويُوسّعه فقط.

ثلاث طرق لحقن التبعيات

يُنفَّذ IoC عبر حقن التبعيات (DI)، وله ثلاثة أساليب شائعة:

  1. الحقن بالمُنشئ — التبعيات نهائية (final) وتُضبط مرة واحدة وصالحة دائمًا. هذا الأسلوب الذي توصي به Spring وما ينبغي الوصول إليه بشكل افتراضي.
    // الأسلوب المُفضَّل public OrderService(PaymentGateway gw, NotificationService ns) { ... }
  2. الحقن بالمُعيِّن (Setter) — مفيد للتبعيات الاختيارية أو عند الحاجة إلى استبدال متعاون بعد الإنشاء. أكثر مرونة، لكن الكائن قد يوجد في حالة إعداد ناقصة.
    public void setGateway(PaymentGateway gw) { this.gateway = gw; }
  3. الحقن في الحقل — تحقن Spring مباشرةً في حقل خاص عبر الانعكاس (Reflection). مختصر في الكتابة، لكنه يُخفي التبعية في المُنشئ ويُعقّد الاختبار، ويُعدّ بوجه عام غير مُستحسَن في كود الإنتاج.
    @Autowired private PaymentGateway gateway; // تجنّب هذا في فئات الإنتاج
فضّل الحقن بالمُنشئ. يجعل جميع التبعيات المطلوبة صريحةً في واجهة برمجة الفئة، ويسمح بأن تكون الحقول final، ويتيح لك تهيئة الفئة في اختبار JUnit عادي دون سياق Spring — فقط مرّر كائنات وهمية إلى المُنشئ.

التوصيل اليدوي مقابل توصيل الحاوية

لترى الصورة كاملة، وصّل الكائنات يدويًا أولًا:

// بدون Spring — "حاوية IoC بدائية" في main() public class Application { public static void main(String[] args) { // 1. بناء المكوّنات منخفضة المستوى PaymentGateway gateway = new StripeGateway(System.getenv("STRIPE_KEY")); NotificationService ns = new EmailNotifier("smtp.example.com"); // 2. الحقن في المكوّنات رفيعة المستوى OrderService orderService = new OrderService(gateway, ns); ReportService reports = new ReportService(orderService); // 3. استخدام الرسم البياني للكائنات الموصَّل بالكامل reports.runDailyReport(); } }

هذا يعمل. لكن مع نموّ الرسم البياني إلى عشرات الكائنات — كل منها بتبعياته الخاصة — يصبح كود التوصيل مئات الأسطر. ستُدير أيضًا أعمار الكائنات (كائن واحد مشترك أم نسخة جديدة في كل مرة) وتتعامل مع التبعيات الدائرية بنفسك. هذه تمامًا هي المشكلة التي يحلّها ApplicationContext في Spring.

ما تُضيفه الحاوية

تقرأ حاوية IoC في Spring إعداداتك (التوضيحات، إعداد Java، أو XML)، وتبني كل كائن، وتحقن كل تبعية، وتُدير دورة حياة كل كائن. من تلك اللحظة تطلب من الحاوية كائنًا موصَّلًا بالكامل بدلًا من استدعاء new:

// مع Spring — الحاوية تملك رسم الكائنات البياني ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); OrderService orderService = ctx.getBean(OrderService.class); orderService.placeOrder(order); // gateway وnotifier مُحقَنَتان بالفعل

تُجري الحاوية التوصيل ذاته الذي فعلته في main()، لكن تلقائيًا واستنادًا إلى تصريحاتك. كما تُطبّق النطاق (كائن OrderService واحد مشترك عبر التطبيق، أو نسخة جديدة لكل طلب HTTP)، وتستدعي استدعاءات دورة الحياة (@PostConstruct / @PreDestroy) لتتمكّن مكوّناتك من التهيئة والتنظيف بشكل نظيف.

لماذا يهمّ هذا في الواقع

  • قابلية الاختبار. لأن OrderService تقبل تبعياتها عبر المُنشئ، يمكن لاختبار الوحدة تمرير MockPaymentGateway — بلا سياق Spring، ولا شبكة، ولا قاعدة بيانات، تعمل الاختبارات في ميلي ثوانٍ.
  • قابلية الاستبدال. التبديل من EmailNotifier إلى SmsNotifier تغيير في سطر واحد من الإعداد لا بحث واستبدال في كل فئة تُنشئ منبّهًا.
  • مبدأ المسؤولية الواحدة. تتحرّر فئات منطق الأعمال من التعقيد العرضي لإنشاء الكائنات. تؤدّي شيئًا واحدًا: وظيفتها.
  • التركيب بدلًا من الوراثة. يُركَّب السلوك بدمج المتعاونين المحقَنين بدلًا من تسلسلات هرمية عميقة للفئات — أسهل بكثير في الاستيعاب والتوسعة.
IoC لا يعني عدم استخدام new أبدًا. كائنات القيمة — سجل Money، مغلّف OrderId، كائن نقل بيانات (DTO) — لا تمتلك متعاونين وإنشاؤها بـ new مقبول تمامًا. احتفظ بـ IoC للخدمات والمكوّنات البنية التحتية التي لها تبعيات وتحتاج للاستبدال أو الاختبار في عزل.

IoC في العالم الحقيقي: مبدأ هوليوود

يُوصَف IoC أحيانًا بـ مبدأ هوليوود: "لا تتصل بنا، سنتصل بك." في الكود التقليدي، تستدعي فئتك مكتبة ما. مع IoC، يستدعي الإطار كودك — يُشغّل دوالك التجارية في الوقت المناسب بعد أن يُجمّع رسم الكائنات البياني. أنت توفّر القطع؛ الإطار ينسّقها. هذا القلب هو مصدر قوة Spring وهو سبب تسمية المبدأ بهذا الاسم.

الخلاصة

يعكس IoC مسؤولية إنشاء الكائنات وتوصيلها: بدلًا من أن تصل الفئة إلى تبعياتها بنفسها، تُزوَّد بها من الخارج. الحقن بالمُنشئ هو الآلية المُفضَّلة. يُثبت التوصيل اليدوي أن المفهوم يعمل؛ أما ApplicationContext في Spring فيُؤتمته على نطاق واسع. النتيجة كود قابل فعلًا للاختبار المعزول، وسهل إعادة الإعداد، ومتوافق مع مبدأ المسؤولية الواحدة. يدرس الدرس التالي الحاوية نفسها — كيف تقرأ تصريحاتك وكيف تُدير دورة حياة كل كائن تتحكّم فيه.