Rust में Dependency Injection का परिचय
\n\nजब आप Rust में प्रोग्राम लिखते हैं, तो देर-सबेर आपको एक समस्या का सामना करना पड़ता है: कोड को लचीला, परीक्षण योग्य और शिथिल रूप से युग्मित कैसे बनाया जाए? इसका उत्तर है — Dependency Injection (DI), या निर्भरता इंजेक्शन। यह एक डिज़ाइन पैटर्न है जिसमें कोई ऑब्जेक्ट अपनी निर्भरताएँ बाहर से प्राप्त करता है, न कि स्वयं बनाता है।
\n\nइस लेख में, हम व्यावहारिक उदाहरणों के साथ Rust में Dependency Injection को लागू करने का तरीका समझेंगे। आप सीखेंगे कि लचीली आर्किटेक्चर बनाने के लिए trait-ऑब्जेक्ट्स और generics का उपयोग कैसे करें। यह सामग्री शुरुआती Rust डेवलपर्स के लिए है जो भाषा की मूल बातों से परिचित हैं।
\n\nDependency Injection की आवश्यकता क्यों है?
\n\nकल्पना करें कि आप सूचनाएँ भेजने के लिए एक एप्लिकेशन लिख रहे हैं। DI के बिना, NotificationService वर्ग एक विशिष्ट कार्यान्वयन (जैसे, EmailSender) से कसकर बंधा होगा। यह कोड को बनाता है:
- \n
- परीक्षण करना कठिन — वास्तविक सेवा को mock-ऑब्जेक्ट से बदलना असंभव है। \n
- विस्तार करना मुश्किल — भेजने का नया तरीका (SMS, Push) जोड़ने के लिए मौजूदा कोड में बदलाव की आवश्यकता होती है। \n
- कसकर युग्मित — एक भाग में परिवर्तन दूसरे को तोड़ देता है। \n
Dependency Injection कंस्ट्रक्टर, विधि या पैरामीटर के माध्यम से निर्भरताएँ पास करके इन समस्याओं का समाधान करता है। Rust, एक मजबूत टाइपिंग वाली सिस्टम भाषा होने के नाते, DI को लागू करने के लिए सुरुचिपूर्ण तरीके प्रदान करता है।
\n\nRust में DI के मुख्य दृष्टिकोण
\n\nRust में निर्भरता इंजेक्शन को लागू करने के दो मुख्य तरीके हैं: trait-ऑब्जेक्ट्स (डायनामिक डिस्पैच) के माध्यम से और generics (स्टैटिक डिस्पैच) के माध्यम से। आइए दोनों दृष्टिकोणों पर विचार करें।
\n\n1. trait-ऑब्जेक्ट्स के माध्यम से DI (डायनामिक डिस्पैच)
\n\nयह दृष्टिकोण रनटाइम पॉलीमॉर्फिज्म का उपयोग करता है। आप एक trait परिभाषित करते हैं, और फिर Box<dyn Trait> या एक संदर्भ &dyn Trait पास करते हैं।
उदाहरण: लॉगिंग सेवा
\n\n// लॉगिंग के लिए trait परिभाषित करें\ntrait Logger {\n fn log(&self, message: &str);\n}\n\n// कंसोल पर आउटपुट के लिए कार्यान्वयन\nstruct ConsoleLogger;\n\nimpl Logger for ConsoleLogger {\n fn log(&self, message: &str) {\n println!("[LOG]: {}", message);\n }\n}\n\n// फ़ाइल के लिए कार्यान्वयन\nstruct FileLogger {\n filename: String,\n}\n\nimpl Logger for FileLogger {\n fn log(&self, message: &str) {\n // फ़ाइल में सरलीकृत लेखन\n std::fs::write(&self.filename, message).unwrap();\n }\n}\n\n// DI का उपयोग करने वाली सेवा\nstruct UserService {\n logger: Box<dyn Logger>,\n}\n\nimpl UserService {\n // कंस्ट्रक्टर के माध्यम से निर्भरता इंजेक्शन\n fn new(logger: Box<dyn Logger>) -> Self {\n UserService { logger }\n }\n\n fn create_user(&self, name: &str) {\n self.logger.log(&format!("उपयोगकर्ता बनाया गया: {}", name));\n // उपयोगकर्ता बनाने का तर्क...\n }\n}\n\nfn main() {\n // ConsoleLogger का उपयोग करें\n let console_logger = Box::new(ConsoleLogger);\n let service = UserService::new(console_logger);\n service.create_user("Alice");\n\n // FileLogger का उपयोग करें\n let file_logger = Box::new(FileLogger { filename: "log.txt".into() });\n let service2 = UserService::new(file_logger);\n service2.create_user("Bob");\n}\n\nलाभ: लचीलापन, रनटाइम के दौरान कार्यान्वयन बदला जा सकता है। नुकसान: वर्चुअल कॉल के कारण थोड़ा ओवरहेड।
\n\n2. generics के माध्यम से DI (स्टैटिक डिस्पैच)
\n\nयहाँ निर्भरताएँ टाइप पैरामीटर के माध्यम से कंपाइल समय पर परिभाषित की जाती हैं। यह अधिक प्रदर्शनकारी दृष्टिकोण है, लेकिन कम लचीला है।
\n\nउदाहरण: generics के साथ वही लॉगिंग सेवा
\n\ntrait Logger {\n fn log(&self, message: &str);\n}\n\nstruct ConsoleLogge