البرمجة

حقن التبعية في دوت نت

حقن التبعية (Dependency Injection) في .NET: المفاهيم، الأنواع، التطبيقات، والمزايا

تُعد تقنية حقن التبعية (Dependency Injection – DI) أحد المبادئ الأساسية في تصميم البرمجيات الحديثة والتي تُسهم بشكل كبير في تطوير تطبيقات مرنة، قابلة للتوسعة، وسهلة الاختبار. في بيئة .NET، يُعد دعم DI جزءاً مدمجاً من الإطار منذ إصدار .NET Core، مما أتاح للمطورين إمكانية تطبيق مفاهيم تصميم قوية دون الحاجة إلى مكتبات خارجية معقدة.

يهدف هذا المقال إلى شرح مفهوم حقن التبعية بالتفصيل، من حيث الأسس النظرية والتطبيقية، مع التركيز على كيفية تنفيذه في إطار .NET، موضحًا الأنواع المختلفة له، ومزاياه، بالإضافة إلى أمثلة تطبيقية توضيحية شاملة.


ما هو مفهوم التبعية (Dependency)؟

في سياق البرمجة الكائنية التوجه، التبعية هي العلاقة التي تربط بين كائنين، حيث يحتاج كائن معين (Consumer) إلى كائن آخر (Service أو Dependency) لأداء مهامه. على سبيل المثال، إذا كان لدينا كائن يمثل خدمة إرسال البريد الإلكتروني، وكائن آخر يمثل وحدة تسجيل المستخدمين، فإن الأخير يعتمد على خدمة إرسال البريد الإلكتروني لإرسال رسائل التأكيد.

في غياب بنية حقن التبعية، يتم إنشاء الكائنات التي يعتمد عليها مباشرة داخل الكائنات الأخرى، مما يُنتج ترابطاً عالياً (Tight Coupling) يصعّب من اختبار النظام وتطويره.


ما هو حقن التبعية (Dependency Injection)؟

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

في .NET، يتم تنفيذ حقن التبعية عبر حاوية خدمات (Service Container) تُستخدم لإدارة دورة حياة التبعيات وتوفيرها عند الحاجة.


أنواع حقن التبعية في .NET

في .NET، يمكن تنفيذ DI بعدة طرق، لكل منها حالات استخدام مناسبة:

1. Constructor Injection – الحقن عبر المُنشئ

يُعد أكثر الأساليب شيوعاً في .NET. يتم تمرير التبعيات المطلوبة عبر مُعاملات المُنشئ (Constructor Parameters).

csharp
public interface ILoggerService { void Log(string message); } public class ConsoleLogger : ILoggerService { public void Log(string message) => Console.WriteLine(message); } public class UserService { private readonly ILoggerService _logger; public UserService(ILoggerService logger) { _logger = logger; } public void CreateUser() { _logger.Log("User created."); } }

2. Property Injection – الحقن عبر الخصائص

يتم تعيين التبعيات إلى خاصية قابلة للكتابة. يُستخدم في الحالات التي لا تكون التبعية ضرورية لإنشاء الكائن.

csharp
public class ReportService { public ILoggerService Logger { get; set; } public void GenerateReport() { Logger?.Log("Report generated."); } }

3. Method Injection – الحقن عبر المعاملات

يتم تمرير التبعيات مباشرة إلى الأسلوب (Method) الذي يحتاجها.

csharp
public class NotificationService { public void SendNotification(string message, ILoggerService logger) { logger.Log(message); } }

الحاوية المدمجة في .NET Core

بدأت Microsoft بدعم رسمي لحقن التبعية مع .NET Core عبر خدمة الحاوية (IServiceCollection) والتي تُسجل فيها التبعيات، ويتم تمريرها تلقائيًا عبر البنية التحتية عند تشغيل التطبيق.

تسجيل الخدمات

في ملف Program.cs أو Startup.cs:

csharp
var builder = WebApplication.CreateBuilder(args); // تسجيل التبعيات builder.Services.AddScoped(); builder.Services.AddTransient(); var app = builder.Build();

استخدام الخدمات

csharp
app.MapGet("/create-user", (UserService userService) => { userService.CreateUser(); });

دورة حياة التبعيات (Service Lifetimes)

عند تسجيل التبعيات في الحاوية، يتم تحديد دورة الحياة التي تُحدد كيفية إنشاء الكائنات ومتى يتم التخلص منها:

نوع الخدمة الوصف
Singleton يتم إنشاء كائن واحد فقط ويتم مشاركته طوال عمر التطبيق
Scoped يتم إنشاء كائن جديد لكل طلب HTTP (في تطبيقات الويب)
Transient يتم إنشاء كائن جديد في كل مرة يتم فيها طلب التبعية

مثال على تسجيل دورة الحياة:

csharp
services.AddSingleton(); // كائن واحد للتطبيق كله services.AddScoped(); // كائن جديد لكل طلب services.AddTransient(); // كائن جديد في كل مرة

مزايا استخدام حقن التبعية

1. فصل الاهتمامات (Separation of Concerns)

تمكن هذه التقنية من تفكيك المهام داخل النظام، بحيث تتعامل كل وحدة مع مسؤولية محددة، ما يرفع من جودة التصميم ويقلل التعقيد.

2. تعزيز قابلية الاختبار (Testability)

عند استخدام DI، يمكن بسهولة استبدال التبعيات الأصلية بمحاكاة (Mocks أو Stubs)، مما يُسهّل اختبار الوحدات بشكل دقيق.

3. تقليل الترابط (Loose Coupling)

لا يعتمد الكائن على كائنات محددة بل على واجهات (Interfaces)، ما يجعله أقل ارتباطًا، وأسهل في التبديل أو التوسعة.

4. إعادة الاستخدام وسهولة الصيانة

يمكن إعادة استخدام الخدمات في أكثر من مكان بدون تكرار منطق الإنشاء، مما يجعل الكود أكثر تنظيمًا وأسهل في الصيانة.


أمثلة تطبيقية متقدمة

سيناريو استخدام الخدمات المتداخلة

csharp
public class AuditService : IAuditService { private readonly ILoggerService _logger; public AuditService(ILoggerService logger) { _logger = logger; } public void Audit(string action) { _logger.Log($"Audit: {action}"); } } public class PaymentService { private readonly IAuditService _audit; public PaymentService(IAuditService audit) { _audit = audit; } public void ProcessPayment() { _audit.Audit("Payment processed."); } }

تسجيل متعدد لنفس الواجهة

csharp
services.AddTransient(); services.AddTransient();

في هذه الحالة، يمكن طلب IEnumerable لحقن جميع الإصدارات المسجلة.


المقارنة مع مكتبات خارجية

في إصدارات .NET Framework السابقة، كان استخدام DI يتم عبر مكتبات خارجية مثل:

  • Autofac

  • Ninject

  • Unity

  • StructureMap

لكن مع اعتماد حاوية مدمجة في .NET Core، أصبح بالإمكان بناء تطبيقات تعتمد على DI بدون أي أدوات خارجية، ما يعزز الأداء والبساطة.


تحديات استخدام حقن التبعية

رغم المزايا العديدة، إلا أن هناك بعض التحديات:

  • الإفراط في التجريد قد يؤدي إلى تصميم معقد يصعب تتبعه.

  • أخطاء في دورة الحياة عند تسجيل الخدمات قد تؤدي إلى سلوك غير متوقع.

  • ضعف الأداء إذا تم حقن العديد من التبعيات في كائن واحد أو عند استخدام Transient بكثرة دون داعٍ.


أفضل الممارسات (Best Practices)

  • استخدم Constructor Injection كلما أمكن لأنه الأكثر وضوحًا وموثوقية.

  • قم بتسجيل الخدمات بدقة حسب دورة الحياة المناسبة لتقليل استهلاك الموارد.

  • اعتمد على الواجهات (Interfaces) بدلاً من الأصناف لتسهيل الاختبار والتبديل.

  • تجنب الحقن الزائد في الكائن الواحد، قسم المنطق إلى خدمات صغيرة.

  • لا تستخدم الحاوية داخل الخدمات نفسها (Avoid Service Locator anti-pattern).

  • استخدم التحقق من الصحة (Validation) للتبعيات داخل المُنشئ لمنع NullReferenceException.


جدول توضيحي لأنواع الحقن ومتى تستخدم

نوع الحقن الاستخدام المناسب المزايا العيوب
Constructor Injection عندما تكون التبعية ضرورية لإنشاء الكائن واضح، قابل للاختبار، ثابت لا يمكن تغييره بعد الإنشاء
Property Injection عندما تكون التبعية اختيارية أو قابلة للتغيير مرن، سهل التكوين قد تكون الخصائص غير مهيأة (null)
Method Injection عندما تكون التبعية مطلوبة لمهمة محددة فقط مناسب للعمليات المؤقتة لا يُناسب الحالات العامة، يصعب إدارته في مشاريع ضخمة

الخلاصة

حقن التبعية في .NET يمثل حجر الزاوية في تصميم تطبيقات حديثة تتسم بالقوة، والمرونة، وقابلية التطوير المستمر. بفضل الحاوية المدمجة في .NET Core وما تلاها، أصبح تنفيذ هذا النمط أكثر بساطة وفاعلية دون الحاجة إلى أدوات خارجية. إن الالتزام بتطبيقه وفقاً لأفضل الممارسات لا يُساهم فقط في تسهيل الصيانة والتوسعة، بل يعزز من جودة الكود على المدى الطويل، ويُمكن الفرق البرمجية من بناء نظم قابلة للتطور والتوسع بشكل مستمر.


المراجع:

  1. Microsoft Docs – Dependency Injection in .NET

  2. Martin Fowler – Inversion of Control Containers and the Dependency Injection pattern