في مجال هندسة البرمجيات الحديثة، أصبح حقن التبعيات (DI) المعيار الفعلي لإدارة إنشاء الكائنات والاقتران. بينما يعد تنفيذ DI في نظام أحادي بسيط أمرًا مباشرًا، فإن إدارة دورات الحياة عبر نظام موزع من الخدمات المصغرة يقدم طبقة من التعقيد التي غالبًا ما تُربك حتى المهندسين ذوي الخبرة. إن حاوية التحكم بالعكس (IoC) ليست مجرد مصنع للكائنات؛ بل هي مدير لدورات الحياة يحدد متى يتم تخصيص الموارد ومتى يتم تحريرها، وهو أمر بالغ الأهمية.
يمكن أن يؤدي سوء إدارة هذه الدورات إلى تسريبات في الذاكرة، أو مشاكل في البيانات القديمة، أو استنفاد مجموعات الاتصال. تستكشف هذه المقالة التنفيذ العملي لدورات حياة حقن التبعيات، مع التركيز على الفروق الدقيقة في إدارة الحالة عبر حدود الخدمات.
فهم دورات الحياة الأساسية
تدعم معظم حاويات IoC، مثل Autofac، أو Spring Framework، أو المزود المدمج في .NET Core، ثلاث نطاقات رئيسية لدورة الحياة. يعد فهم الفرق الدلالي بين هذه النطاقات الخطوة الأولى نحو هندسة معمارية قوية.
- عابر (Transient): يتم إنشاء نسخة جديدة في كل مرة يتم فيها طلب الخدمة. هذا مثالي للخدمات عديمة الحالة أو الكائنات خفيفة الوزن.
- نطاق (Scoped): يتم إنشاء نسخة واحدة لكل نطاق. في تطبيقات الويب، يرتبط النطاق عادةً بطلب HTTP واحد. هذا مثالي لأنماط وحدة العمل، مثل
DbContextفي Entity Framework. - مفرد (Singleton): يتم إنشاء نسخة واحدة لكل حاوية. يُستخدم هذا للتكوين ذي الحالة أو الخدمات التي تحافظ على حالة عالمية.
فخ دورة الحياة النطاقية في الخدمات المصغرة
يحدث الخطأ المعماري الأكثر شيوعًا عندما يحاول المطورون مشاركة الخدمات ذات النطاق عبر حدود غير متزامنة غير مرتبطة بنطاق الطلب الأصلي. في بنية الخدمات المصغرة، قد يؤدي طلب وارد واحد إلى سلسلة من المكالمات الداخلية عبر طوابير الرسائل أو عملاء HTTP.
إذا تم حقن خدمة ذات نطاق (مثل سياق قاعدة البيانات) في عامل خلفية يعمل خارج نطاق الطلب، فإما أنه سيفشل في الحل أو سيؤدي إلى مشاكل خطيرة في التزامن.
خذ في الاعتبار هذا النمط المشكل في C#:
// ❌ خطير: حقن خدمة ذات نطاق في خدمة مفردة
public class BackgroundWorker
{
private readonly IDataService _dataService; // IDataService مسجلة كـ Scoped
public BackgroundWorker(IDataService dataService)
{
_dataService = dataService; // قد ترمي الحاوية خطأً هنا
// أو تحتفظ بمرجع إلى نسخة تم التخلص منها
}
}
لإصلاح هذا، يجب علينا إدارة النطاق بشكل صريح داخل المهمة الخلفية. لا ينبغي للمستهلك حقن الخدمة ذات النطاق مباشرة في عامل العمل المفرد. بدلاً من ذلك، يجب أن يقبل العامل IServiceScopeFactory، مما يسمح له بإنشاء نطاق جديد لكل وحدة عمل.
الحل العملي: إدارة النطاق الصريحة
من خلال حقن مصنع النطاق، نضمن إنشاء سياق قاعدة البيانات أو التبعيات ذات النطاق الأخرى والتخلص منها بشكل صحيح لكل رسالة فردية يعالجها العامل.
// ✅ آمن: استخدام IServiceScopeFactory لإدارة التبعيات ذات النطاق
public class SafeBackgroundWorker
{
private readonly IServiceScopeFactory _scopeFactory;
public SafeBackgroundWorker(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task ProcessMessageAsync(string message)
{
// إنشاء نطاق جديد لهذه العملية المحددة
using (var scope = _scopeFactory.CreateScope())
{
// حل الخدمة ذات النطاق ضمن النطاق الجديد
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
await dataService.UpdateAsync(message);
} // يتم تنظيف النطاق وجميع خدماته المتخلص منها هنا
}
}
الخاتمة
يتطلب إدارة دورات حياة IoC في الخدمات المصغرة نهجًا منضبطًا لحل التبعيات. بينما تعتبر الخدمات العابرة آمنة والمفردات قوية، فإن الخدمات ذات النطاق تتطلب إدارة حدود صريحة. من خلال فهم عمر تبعياتك وتجنب المشاركة الضمنية للنطاق عبر الحدود غير المتزامنة، يمكنك بناء أنظمة ليست فقط معيارية ولكن أيضًا مقاومة لتسريبات الذاكرة وأخطاء التزامن. تذكر: في الأنظمة الموزعة، وضوح دورة الحياة لا يقل أهمية عن وضوح الكود.