حقن التبعية في Rust: للمبتدئين

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

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



عند كتابة برامج بلغة Rust، عاجلاً أم آجلاً ستواجه مشكلة: كيف تجعل الكود مرناً، قابلاً للاختبار، وذا اقتران ضعيف؟ الإجابة هي حقن التبعية (Dependency Injection - DI). وهو نمط تصميم يحصل فيه الكائن على تبعياته من الخارج، بدلاً من إنشائها بنفسه.



في هذه المقالة، سنشرح كيفية تنفيذ حقن التبعية في Rust من خلال أمثلة عملية. ستتعلم كيفية استخدام كائنات trait و الأدوية العامة (generics) لإنشاء بنية مرنة. المحتوى موجه لمطوري Rust المبتدئين الذين لديهم معرفة بأساسيات اللغة.



لماذا نحتاج حقن التبعية؟



تخيل أنك تكتب تطبيقاً لإرسال الإشعارات. بدون DI، سيكون الكائن NotificationService مرتبطاً بشكل صارم بتطبيق معين (مثل EmailSender). هذا يجعل الكود:

  • صعب الاختبار — لا يمكن استبدال الخدمة الحقيقية بكائن وهمي (mock).
  • صعب التوسيع — إضافة طريقة إرسال جديدة (SMS، Push) تتطلب تغيير الكود الموجود.
  • شديد الاقتران — التغييرات في جزء واحد تؤدي إلى كسر جزء آخر.


حقن التبعية يحل هذه المشكلات عن طريق تمرير التبعيات عبر المُنشئ، أو الدالة، أو المعامل. Rust، كونها لغة نظام ذات كتابة قوية، تقدم طرقاً أنيقة لتنفيذ DI.



الطرق الرئيسية لـ DI في Rust



في Rust، هناك طريقتان رئيسيتان لتنفيذ حقن التبعية: عبر كائنات trait (التوزيع الديناميكي) وعبر الأدوية العامة (generics) (التوزيع الثابت). سننظر في كلا الطريقتين.



1. DI عبر كائنات trait (التوزيع الديناميكي)



تستخدم هذه الطريقة تعدد الأشكال في وقت التشغيل. تقوم بتعريف trait، ثم تمرر Box<dyn Trait> أو مرجع &dyn Trait.



مثال: خدمة تسجيل (logging)



// تعريف trait للتسجيلtrait Logger {    fn log(&self, message: &str);}

// تطبيق للطباعة على وحدة التحكمstruct ConsoleLogger;

impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("[LOG]: {}", message); }}

// تطبيق للملفstruct FileLogger { filename: String,}

impl Logger for FileLogger { fn log(&self, message: &str) { // كتابة مبسطة إلى ملف std::fs::write(&self.filename, message).unwrap(); }}

// خدمة تستخدم DIstruct UserService { logger: Box<dyn Logger>,}

impl UserService { // حقن التبعية عبر المُنشئ fn new(logger: Box<dyn Logger>) -> Self { UserService { logger } }

fn create_user(&self, name: &str) { self.logger.log(&format!("تم إنشاء مستخدم: {}", name)); // منطق إنشاء المستخدم... }}

fn main() { // استخدام ConsoleLogger let console_logger = Box::new(ConsoleLogger); let service = UserService::new(console_logger); service.create_user("Alice");

// استخدام FileLogger let file_logger = Box::new(FileLogger { filename: "log.txt".into() }); let service2 = UserService::new(file_logger); service2.create_user("Bob");}


المزايا: مرونة، يمكن تغيير التطبيق أثناء وقت التشغيل. العيوب: حمل إضافي بسيط بسبب الاستدعاءات الافتراضية.



2. DI عبر الأدوية العامة (التوزيع الثابت)



هنا، يتم تحديد التبعيات في وقت الترجمة عبر معاملات النوع. هذه الطريقة أكثر أداءً ولكنها أقل مرونة.



مثال: نفس خدمة التسجيل باستخدام الأدوية العامة



trait Logger {    fn log(&self, message: &str);}

struct ConsoleLogge

المدونات

توصيات الكتب