حقن التبعية (Dependency Injection) في Rust للمبتدئين

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

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

ابدأ الدورة

مقدمة: لماذا يعتبر حقن التبعية (DI) مهمًا في Rust؟

\n\n

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

\n\n
struct UserService;\n\nimpl UserService {\n    fn get_user(&self, id: u32) -> String {\n        let db = Database::new(); // ❌ ارتباط صلب\n        db.query_user(id)\n    }\n}\n
\n\n

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

\n\n

في هذه المقالة، سنشرح كيفية حقن التبعيات في Rust من خلال أمثلة عملية: من النقل البسيط للهياكل إلى استخدام السمات والمؤشرات الذكية.

\n\n

1. الأسلوب الأساسي: نقل التبعيات عبر المُنشئ

\n\n

أبسط طريقة لحقن تبعية في Rust هي تمريرها كوسيطة للمُنشئ أو الدالة. لنلقِ نظرة على مثال مع مسجل.

\n\n

مثال سيء (بدون DI)

\n\n
struct ConsoleLogger;\n\nimpl ConsoleLogger {\n    fn log(&self, msg: &str) {\n        println!("[LOG]: {}", msg);\n    }\n}\n\nstruct OrderService {\n    logger: ConsoleLogger, // ❌ ارتباط صلب بنوع محدد\n}\n\nimpl OrderService {\n    fn new() -> Self {\n        Self {\n            logger: ConsoleLogger, // يتم إنشاؤه داخليًا\n        }\n    }\n\n    fn place_order(&self, item: &str) {\n        self.logger.log(&format!("طلب: {}", item));\n    }\n}\n
\n\n

مثال جيد (مع DI)

\n\n
struct ConsoleLogger;\n\nimpl ConsoleLogger {\n    fn log(&self, msg: &str) {\n        println!("[LOG]: {}", msg);\n    }\n}\n\nstruct OrderService {\n    logger: ConsoleLogger, // التبعية تُمرر من الخارج\n}\n\nimpl OrderService {\n    // المُنشئ يقبل التبعية\n    fn new(logger: ConsoleLogger) -> Self {\n        Self { logger }\n    }\n\n    fn place_order(&self, item: &str) {\n        self.logger.log(&format!("طلب: {}", item));\n    }\n}\n\nfn main() {\n    let logger = ConsoleLogger;\n    let service = OrderService::new(logger);\n    service.place_order("كتاب عن Rust");\n}\n
\n\n

ما الذي تغير؟ الآن OrderService لا ينشئ المسجل بنفسه. بل يستلمه جاهزًا. هذا بالفعل DI، ولكن هناك فارق بسيط: ما زلنا مرتبطين بالنوع المحدد ConsoleLogger. لجعل الكود مرنًا، نحتاج إلى استخدام السمات.

\n\n

2. استخدام السمات (Traits) لتجريد التبعيات

\n\n

السمات في Rust هي نظير الواجهات في اللغات الأخرى. تسمح بتحديد عقد يجب أن تتبعه التبعيات.

\n\n

تحديد سمة Logger

\n\n
trait Logger {\n    fn log(&self, msg: &str);\n}\n\nstruct ConsoleLogger;\nimpl Logger for ConsoleLogger {\n    fn log(&self, msg: &str) {\n        println!("[وحدة التحكم]: {}", msg);\n    }\n}\n\nstruct FileLogger {\n    filename: String,\n}\nimpl Logger for FileLogger {\n    fn log(&self, msg: &str) {\n        // محاكاة الكتابة إلى ملف\n        println!("[ملف {}]: {}", self.filename, msg);\n    }\n}\n
\n\n

حقن السمة عبر الأدوية (Generics)

\n\n
struct OrderService<L: Logger> {\n    logger: L, // نوع عام ينفذ سمة Logger\n}\n\nimpl<L: Logger> OrderService<L> {\n    fn new(logger: L) -> Self {\n        Self { logger }\n    }\n\n    fn place_order(&self, item: &str) {\n        self.logger.log(&format!("طلب: {}", item));\n    }\n}\n\nfn main() {\n    let console_logge                

المدونات

توصيات الكتب