حقن التبعيات في Rust: دليل شامل للمطورين

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

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

ابدأ الدورة
```html

مقدمة إلى Dependency Injection في Rust



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



على عكس اللغات ذات الكتابة الديناميكية أو جمع القمامة، تتطلب Rust نهجاً خاصاً لحقن التبعيات. لا توجد هنا انعكاسية (Reflection) أو حاويات وقت التشغيل (Runtime Containers) بالمعنى التقليدي. بدلاً من ذلك، يستخدم المطورون آليات ثابتة: كائنات trait، الأنواع العامة (Generics)، والتركيب عبر الهياكل (Structs).



في هذه المقالة، سنستعرض ثلاثة أساليب رئيسية لتطبيق Dependency Injection في Rust، بدءاً من أبسط حقن يدوي وصولاً إلى استخدام المكتبات. سيتم توضيح كل نهج بأمثلة عملية من الكود.



1. الحقن اليدوي للتبعيات (Manual DI)



الطريقة الأبسط والأكثر وضوحاً هي تمرير التبعيات عبر مُنشئ الهيكل (Struct Constructor). لا يتطلب هذا النهج مكتبات إضافية ويتم التحكم فيه بالكامل بواسطة مترجم Rust.



مثال: مسجل (Logger) وخدمة (Service)



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

// تنفيذ المسجل للإنتاجstruct ConsoleLogger;

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

// تنفيذ للاختباراتstruct MockLogger { pub messages: Vec<String>,}

impl Logger for MockLogger { fn log(&self, message: &str) { self.messages.push(message.to_string()); }}

// خدمة تعتمد على Loggerstruct UserService<T: Logger> { logger: T,}

impl<T: Logger> UserService<T> { fn new(logger: T) -> Self { Self { logger } }

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

fn main() { let logger = ConsoleLogger; let service = UserService::new(logger); service.create_user("أليكسي");}


المزايا:

  • أمان تام للأنواع في مرحلة الترجمة
  • أقصى أداء (zero-cost abstraction)
  • سهولة الفهم والتصحيح


العيوب:

  • عند زيادة عدد التبعيات، يصبح الكود ضخماً
  • صعوبة تغيير التطبيقات في وقت التشغيل (runtime)
  • يتطلب تمرير جميع التبعيات بشكل صريح عبر سلسلة الاستدعاءات


2. الحقن عبر كائنات trait (Dynamic Dispatch)



إذا كنت بحاجة إلى إمكانية تغيير التطبيقات في وقت التشغيل أو إخفاء النوع المحدد، استخدم كائنات trait (Box<dyn Trait>). هذا هو النهج الكلاسيكي لـ DI المعروف من لغات أخرى.



مثال: مستودع مجرد (Abstract Repository)



use std::sync::Mutex;

trait UserRepository { fn find_by_id(&self, id: u32) -> Option<String>; fn save(&self, user: String);}

struct InMemoryRepository { users: Mutex<Vec<String>>,}

impl UserRepository for InMemoryRepository { fn find_by_id(&self, id: u32) -> Option<String> { let users = self.users.lock().unwrap(); users.get(id as usize).cloned() }

fn save(&self, user: String) { let mut users = self.users.lock().unwrap(); users.push(user); }}

struct UserController { repository: Box<dyn UserRepository>,}

impl UserController { fn new(repository: Box<dyn UserRepository>) -> Self { Self { repository } }

fn get_user(&self, id: u32) -> Option<String> ```

المدونات

توصيات الكتب