مقدمة: لماذا يعتبر حقن التبعية (DI) مهمًا في Rust؟
\n\nعند كتابة برامج بلغة Rust، عاجلاً أم آجلاً ستواجه موقفًا حيث تحتاج وحدة نمطية (مثل خدمة أو متحكم) إلى استخدام إمكانيات وحدة نمطية أخرى (قاعدة بيانات، API، مسجل). أبسط طريقة هي إنشاء الكائن المطلوب مباشرة داخل الكود الذي يستخدمه:
\n\nstruct 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\n1. الأسلوب الأساسي: نقل التبعيات عبر المُنشئ
\n\nأبسط طريقة لحقن تبعية في Rust هي تمريرها كوسيطة للمُنشئ أو الدالة. لنلقِ نظرة على مثال مع مسجل.
\n\nمثال سيء (بدون DI)
\n\nstruct 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\nstruct 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. لجعل الكود مرنًا، نحتاج إلى استخدام السمات.
2. استخدام السمات (Traits) لتجريد التبعيات
\n\nالسمات في Rust هي نظير الواجهات في اللغات الأخرى. تسمح بتحديد عقد يجب أن تتبعه التبعيات.
\n\nتحديد سمة Logger
\n\ntrait 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\nstruct 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