इस पोस्ट में, हम यह पता लगाएंगे कि हमारे कोड के उपयोगकर्ताओं द्वारा कॉन्फ़िगरेबल रूबी मॉड्यूल कैसे बनाए जाएं — एक पैटर्न जो जेम लेखकों को उनकी लाइब्रेरीज़ में अधिक लचीलापन जोड़ने की अनुमति देता है।
अधिकांश रूबी डेवलपर्स व्यवहार साझा करने के लिए मॉड्यूल का उपयोग करने से परिचित हैं। आखिरकार, डॉक्यूमेंटेशन के अनुसार यह उनके मुख्य उपयोग मामलों में से एक है:
रूबी में मॉड्यूल दो उद्देश्यों की पूर्ति करते हैं, नेमस्पेसिंग और मिक्सिन कार्यक्षमता।
रेल्स ने ActiveSupport::Concern के रूप में कुछ सिंटैक्टिक शुगर जोड़ा, लेकिन सामान्य सिद्धांत वही रहता है।
समस्या
मिक्सिन कार्यक्षमता प्रदान करने के लिए मॉड्यूल का उपयोग करना आमतौर पर सीधा होता है। हमें बस कुछ मेथड्स को बंडल करना होता है और अपने मॉड्यूल को कहीं और शामिल करना होता है:
module HelloWorld def hello "Hello, world!" end end class Test include HelloWorld end Test.new.hello #=> "Hello, world!"
यह एक बहुत ही स्टैटिक तंत्र है, हालांकि रूबी के inherited और extended हुक मेथड्स शामिल करने वाली क्लास के आधार पर कुछ भिन्न व्यवहार की अनुमति देते हैं:
module HelloWorld def self.included(base) define_method :hello do "Hello, world from #{base}!" end end end class Test include HelloWorld end Test.new.hello #=> "Hello, world from Test!"
यह कुछ अधिक डायनामिक है लेकिन फिर भी हमारे कोड उपयोगकर्ताओं को, उदाहरण के लिए, मॉड्यूल समावेश के समय hello मेथड का नाम बदलने की अनुमति नहीं देता।
समाधान: कॉन्फ़िगरेबल रूबी मॉड्यूल
पिछले कुछ वर्षों में, एक नया पैटर्न उभरा है जो इस समस्या को हल करता है, जिसे लोग कभी-कभी "मॉड्यूल बिल्डर पैटर्न" कहते हैं। यह तकनीक रूबी की दो प्राथमिक विशेषताओं पर निर्भर करती है:
मॉड्यूल किसी भी अन्य ऑब्जेक्ट की तरह हैं — उन्हें ऑन-द-फ्लाई बनाया जा सकता है, वेरिएबल्स को असाइन किया जा सकता है, डायनामिकली संशोधित किया जा सकता है, साथ ही मेथड्स को पास किया जा सकता है या उनसे लौटाया जा सकता है।
def make_module # एक मॉड्यूल को ऑन-द-फ्लाई बनाएं और इसे वेरिएबल को असाइन करें mod = Module.new # मॉड्यूल को संशोधित करें mod.module_eval do def hello "Hello, AppSignal world!" end end # इसे स्पष्ट रूप से लौटाएं mod endincludeयाextendकॉल्स का आर्गुमेंट एक मॉड्यूल होना जरूरी नहीं है, यह एक एक्सप्रेशन भी हो सकता है जो एक लौटाता है, उदाहरण के लिए, एक मेथड कॉल।class Test # make_module द्वारा लौटाए गए मॉड्यूल को शामिल करें include make_module end Test.new.hello #=> "Hello, AppSignal world!"
मॉड्यूल बिल्डर एक्शन में
अब हम इस ज्ञान का उपयोग एक सरल मॉड्यूल Wrapper बनाने के लिए करेंगे, जो निम्न व्यवहार को इम्प्लीमेंट करता है:
Wrapperशामिल करने वाली क्लास केवल एक विशिष्ट प्रकार की ऑब्जेक्ट्स को रैप कर सकती है। कंस्ट्रक्टर आर्गुमेंट टाइप को वेरिफाई करेगा और यदि टाइप अपेक्षित से मेल नहीं खाता तो त्रुटि उठाएगा।- रैप्ड ऑब्जेक्ट
original_नामक एक इंस्टेंस मेथड के माध्यम से उपलब्ध होगा, उदाहरण के लिएoriginal_integerयाoriginal_string। - यह हमारे कोड के उपभोक्ताओं को इस एक्सेसर मेथड के लिए एक वैकल्पिक नाम निर्दिष्ट करने की अनुमति देगा, उदाहरण के लिए,
the_string।
आइए देखें कि हम अपने कोड को कैसे व्यवहार करना चाहते हैं:
# 1 class IntWrapper # 2 include Wrapper.for(Integer) end # 3 i = IntWrapper.new(42) i.original_integer #=> 42 # 4 i = IntWrapper.new("42") #=> TypeError (not a Integer) # 5 class StringWrapper include Wrapper.for(String, accessor_name: :the_string) end s = StringWrapper.new("Hello, World!") # 6 s.the_string #=> "Hello, World!"
चरण 1 में, हम IntWrapper नामक एक नई क्लास परिभाषित करते हैं।
चरण 2 में, हम सुनिश्चित करते हैं कि यह क्लास केवल नाम से एक मॉड्यूल शामिल नहीं करती बल्कि इसके बजाय, Wrapper.for(Integer) कॉल के परिणाम में मिक्स इन करती है।
चरण 3 में, हम अपनी नई क्लास का एक ऑब्जेक्ट इंस्टेंटिएट करते हैं और इसे i को असाइन करते हैं। जैसा कि निर्दिष्ट है, इस ऑब्जेक्ट में original_integer नामक एक मेथड है, जो हमारी आवश्यकताओं में से एक को पूरा करता है।
चरण 4 में, यदि हम गलत प्रकार का आर्गुमेंट पास करने का प्रयास करते हैं, जैसे एक स्ट्रिंग, तो एक सहायक TypeError उठाया जाएगा। अंत में, आइए सत्यापित करें कि उपयोगकर्ता कस्टम एक्सेसर नाम निर्दिष्ट करने में सक्षम हैं।
इसके लिए, हम चरण 5 में StringWrapper नामक एक नई क्लास परिभाषित करते हैं और कीवर्ड आर्गुमेंट accessor_name के रूप में the_string पास करते हैं, जिसे हम चरण 6 में एक्शन में देखते हैं।
हालांकि यह एक कुछ हद तक गढ़ा हुआ उदाहरण है, इसमें मॉड्यूल बिल्डर पैटर्न और इसके उपयोग को दिखाने के लिए पर्याप्त भिन्न व्यवहार है।
पहला प्रयास
आवश्यकताओं और उपयोग उदाहरण के आधार पर, हम अब इम्प्लीमेंटेशन शुरू कर सकते हैं। हम पहले से ही जानते हैं कि हमें Wrapper नामक एक मॉड्यूल की आवश्यकता है जिसमें for नामक एक मॉड्यूल-लेवल मेथड हो, जो एक क्लास को वैकल्पिक कीवर्ड आर्गुमेंट के रूप में लेता है:
module Wrapper def self.for(klass, accessor_name: nil) end end
चूंकि इस मेथड का रिटर्न वैल्यू include का आर्गुमेंट बन जाता है, यह एक मॉड्यूल होना चाहिए। इस प्रकार, हम Module.new के साथ एक नया अनाम मॉड्यूल बना सकते हैं।
Module.new do end
हमारी आवश्यकताओं के अनुसार, इसे एक कंस्ट्रक्टर परिभाषित करने की आवश्यकता है जो पास की गई ऑब्जेक्ट के प्रकार को वेरिफाई करता है, साथ ही एक उचित नाम वाला एक्सेसर मेथड। आइए कंस्ट्रक्टर से शुरू करते हैं:
define_method :initialize do |object| raise TypeError, "not a #{klass}" unless object.is_a?(klass) @object = object end
यह कोड define_method का उपयोग करके रिसीवर में डायनामिकली एक इंस्टेंस मेथड जोड़ता है। चूंकि ब्लॉक क्लोजर के रूप में कार्य करता है, यह बाहरी स्कोप से klass ऑब्जेक्ट का उपयोग आवश्यक टाइप चेक करने के लिए कर सकता है।
उचित नाम वाला एक्सेसर मेथड जोड़ना इतना कठिन नहीं है:
# 1 method_name = accessor_name || begin klass_name = klass.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase "original_#{klass_name}" end # 2 define_method(method_name) { @object }
सबसे पहले, हमें यह देखने की आवश्यकता है कि क्या हमारे कोड के कॉलर ने accessor_name पास किया है। यदि हां, तो हम इसे केवल method_name को असाइन करते हैं और फिर हो चुके हैं। अन्यथा, हम क्लास लेते हैं और इसे अंडरस्कोर स्ट्रिंग में बदलते हैं, उदाहरण के लिए, Integer integer में बदल जाता है या OpenStruct open_struct में। यह klass_name वेरिएबल तब original_ से प्रीफिक्स किया जाता है ताकि अंतिम एक्सेसर नाम उत्पन्न हो सके। एक बार जब हम मेथड का नाम जान लेते हैं, तो हम फिर से define_method का उपयोग करके इसे अपने मॉड्यूल में जोड़ते हैं, जैसा कि चरण 2 में दिखाया गया है।
यहां इस बिंदु तक पूरा कोड है। एक लचीले और कॉन्फ़िगरेबल रूबी मॉड्यूल के लिए 20 लाइन्स से कम; बुरा नहीं।
module Wrapper def self.for(klass, accessor_name: nil) Module.new do define_method :initialize do |object| raise TypeError, "not a #{klass}" unless object.is_a?(klass) @object = object end method_name = accessor_name || begin klass_name = klass.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase "original_#{klass_name}" end define_method(method_name) { @object } end end end
सतर्क पाठकों को याद हो सकता है कि Wrapper.for एक अनाम मॉड्यूल लौटाता है। यह कोई समस्या नहीं है, लेकिन ऑब्जेक्ट के इनहेरिटेंस चेन की जांच करते समय थोड़ा भ्रमित करने वाला हो सकता है:
StringWrapper.ancestors #=> [StringWrapper, #, Object, Kernel, BasicObject]
यहां # (यदि आप साथ चल रहे हैं तो नाम भिन्न होगा) हमारे अनाम मॉड्यूल को संदर्भित करता है।
सुधारा गया संस्करण
आइए अपने उपयोगकर्ताओं के लिए जीवन आसान बनाएं द्वारा एक अनाम के बजाय एक नामित मॉड्यूल लौटाकर। इसके लिए कोड बहुत समान है जो हमारे पास पहले था, कुछ मामूली परिवर्तनों के साथ:
module Wrapper def self.for(klass, accessor_name: nil) # 1 mod = const_set("#{klass}InstanceMethods", Module.new) # 2 mod.module_eval do define_method :initialize do |object| raise TypeError, "not a #{klass}" unless object.is_a?(klass) @object = object end method_name = accessor_name || begin klass_name = klass.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase "original_#{klass_name}" end define_method(method_name) { @object } end # 3 mod end end
पहले चरण में, हम "#{klass}InstanceMethods" नामक एक नेस्टेड मॉड्यूल बनाते हैं (उदाहरण के लिए IntegerInstanceMethods), जो सिर्फ एक "खाली" मॉड्यूल है।
जैसा कि चरण 2 में दिखाया गया है, हम for मेथड में module_eval का उपयोग करते हैं, जो मॉड्यूल के संदर्भ में कोड के एक ब्लॉक का मूल्यांकन करता है जिस पर इसे बुलाया जाता है। इस तरह, हम इसे लौटाने से पहले मॉड्यूल में व्यवहार जोड़ सकते हैं (चरण 3 में)।
यदि हम अब Wrapper शामिल करने वाली क्लास के पूर्वजों की जांच करते हैं, तो आउटपुट में एक उचित नामित मॉड्यूल शामिल होगा, जो पिछले अनाम मॉड्यूल की तुलना में कहीं अधिक सार्थक और डीबग करने में आसान है।
StringWrapper.ancestors #=> [StringWrapper, Wrapper::StringInstanceMethods, Object, Kernel, BasicObject]
जंगली में मॉड्यूल बिल्डर पैटर्न
इस पोस्ट के अलावा, हम मॉड्यूल बिल्डर पैटर्न या समान तकनीकों को और कहां पा सकते हैं?
एक उदाहरण dry-rb जेम्स का परिवार है, जहां, उदाहरण के लिए, dry-effects विभिन्न इफेक्ट हैंडलर्स को कॉन्फ़िगरेशन विकल्प पास करने के लिए मॉड्यूल बिल्डर्स का उपयोग करता है:
# This adds a `counter` effect provider. It will handle (eliminate) effects include Dry::Effects::Handler.State(:counter) # Providing scope is required # All cache values will be scoped with this key include Dry::Effects::Handler.Cache(:blog)
हम उत्कृष्ट Shrine जेम में समान उपयोग पा सकते हैं, जो रूबी एप्लिकेशन के लिए एक फाइल अपलोड टूलकिट प्रदान करता है:
class Photo < Sequel::Model include Shrine::Attachment(:image) end
यह पैटर्न अभी भी अपेक्षाकृत नया है, लेकिन मुझे उम्मीद है कि हम भविष्य में इसके अधिक देखेंगे, विशेष रूप से उन जेम्स में जो रेल्स वाले की तुलना में शुद्ध रूबी एप्लिकेशन पर अधिक ध्यान केंद्रित करते हैं।
सारांश
इस पोस्ट में, हमने रूबी में कॉन्फ़िगरेबल मॉड्यूल को इम्प्लीमेंट करने का पता लगाया, एक तकनीक जिसे कभी-कभी मॉड्यूल बिल्डर पैटर्न कहा जाता है। अन्य मेटाप्रोग्रामिंग तकनीकों की तरह, यह बढ़ी हुई जटिलता की कीमत पर आता है और इसलिए अच्छे कारण के बिना उपयोग नहीं किया जाना चाहिए। हालांकि, उन दुर्लभ मामलों में जहां ऐसी लचीलापन की आवश्यकता होती है, रूबी का ऑब्जेक्ट मॉडल एक बार फिर एक सुंदर और संक्षिप्त समाधान की अनुमति देता है। मॉड्यूल बिल्डर पैटर्न कुछ ऐसा नहीं है जिसकी अधिकांश रूबी डेवलपर्स को अक्सर आवश्यकता होगी, लेकिन यह किसी के टूलकिट में रखने के लिए एक बेहतरीन टूल है, विशेष रूप से लाइब्रेरी लेखकों के लिए।
P.S. यदि आप रूबी मैजिक पोस्ट को प्रेस से बाहर आते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूज़लेटर की सदस्यता लें और कभी भी एक भी पोस्ट मिस न करें!