इस पोस्ट में, हम देखेंगे कि रूबी मॉड्यूल कैसे बनाया जाता है जो हमारे कोड के उपयोगकर्ताओं द्वारा कॉन्फ़िगर करने योग्य हैं - एक पैटर्न जो रत्न लेखकों को अपने पुस्तकालयों में अधिक लचीलापन जोड़ने की अनुमति देता है।
अधिकांश रूबी डेवलपर्स व्यवहार साझा करने के लिए मॉड्यूल का उपयोग करने से परिचित हैं। आखिरकार, दस्तावेज़ीकरण के अनुसार, यह उनके मुख्य उपयोग के मामलों में से एक है:
<ब्लॉकक्वॉट>रूबी, नेमस्पेसिंग और मिक्सिन कार्यक्षमता में मॉड्यूल दो उद्देश्यों की पूर्ति करते हैं।
रेल ने कुछ वाक्यात्मक चीनी को 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 # create a module on the fly and assign it to variable mod = Module.new # modify module mod.module_eval do def hello "Hello, AppSignal world!" end end # explicitly return it mod end
-
include
. का तर्क याextend
कॉल का मॉड्यूल होना जरूरी नहीं है, यह एक वापसी करने वाली अभिव्यक्ति भी हो सकती है, उदा। एक विधि कॉल।class Test # include the module returned by make_module include make_module end Test.new.hello #=> "Hello, AppSignal world!"
कार्य में मॉड्यूल निर्माता
अब हम इस ज्ञान का उपयोग Wrapper
. नामक एक साधारण मॉड्यूल बनाने के लिए करेंगे , जो निम्नलिखित व्यवहार को लागू करता है:
- एक वर्ग जिसमें
Wrapper
शामिल है केवल एक विशिष्ट प्रकार की वस्तुओं को लपेट सकता है। कंस्ट्रक्टर तर्क प्रकार को सत्यापित करेगा और यदि प्रकार अपेक्षित से मेल नहीं खाता है तो एक त्रुटि उत्पन्न करेगा। - लिपटे हुए ऑब्जेक्ट
original_<class>
. नामक इंस्टेंस विधि के माध्यम से उपलब्ध होंगे , जैसे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
उठाया जाएगा। अंत में, आइए सत्यापित करें कि उपयोगकर्ता कस्टम एक्सेसर नाम निर्दिष्ट करने में सक्षम हैं।
इसके लिए, हम StringWrapper
. नामक एक नए वर्ग को परिभाषित करते हैं चरण 5 में और the_string
pass पास करें कीवर्ड तर्क के रूप में accessor_name
, जिसे हम चरण 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, #<Module:0x0000000107283680>, Object, Kernel, BasicObject]
यहां #<Module:0x0000000107283680>
(यदि आप अनुसरण कर रहे हैं तो नाम अलग-अलग होगा) हमारे अनाम मॉड्यूल को संदर्भित करता है।
बेहतर संस्करण
आइए एक अनाम मॉड्यूल के बजाय एक नामित मॉड्यूल लौटाकर अपने उपयोगकर्ताओं के लिए जीवन को आसान बनाते हैं। इसके लिए कोड बहुत कुछ वैसा ही है जैसा हमारे पास पहले था, कुछ मामूली बदलावों के साथ:
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 में दिखाया गया है, हम module_eval
. का उपयोग करते हैं for
. में विधि, जो उस मॉड्यूल के संदर्भ में कोड के एक ब्लॉक का मूल्यांकन करती है जिसे इसे कहा जाता है। इस तरह, हम चरण 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)
हम उत्कृष्ट श्राइन रत्न में समान उपयोग पा सकते हैं, जो रूबी अनुप्रयोगों के लिए एक फ़ाइल अपलोड टूलकिट प्रदान करता है:
class Photo < Sequel::Model
include Shrine::Attachment(:image)
end
यह पैटर्न अभी भी अपेक्षाकृत नया है, लेकिन मुझे उम्मीद है कि हम भविष्य में इसे और अधिक देखेंगे, विशेष रूप से रत्नों में जो शुद्ध रूबी अनुप्रयोगों पर रेल वाले की तुलना में अधिक ध्यान केंद्रित करते हैं।
सारांश
इस पोस्ट में, हमने पता लगाया कि रूबी में कॉन्फ़िगर करने योग्य मॉड्यूल को कैसे कार्यान्वित किया जाए, एक तकनीक जिसे कभी-कभी मॉड्यूल बिल्डर पैटर्न के रूप में जाना जाता है। अन्य मेटाप्रोग्रामिंग तकनीकों की तरह, यह बढ़ी हुई जटिलता की कीमत पर आता है और इसलिए इसे अच्छे कारण के बिना उपयोग नहीं किया जाना चाहिए। हालांकि, दुर्लभ मामलों में जहां इस तरह के लचीलेपन की आवश्यकता होती है, रूबी का ऑब्जेक्ट मॉडल एक बार फिर एक सुरुचिपूर्ण और संक्षिप्त समाधान की अनुमति देता है। मॉड्यूल बिल्डर पैटर्न ऐसा कुछ नहीं है जिसकी अधिकांश रूबी डेवलपर्स को अक्सर आवश्यकता होगी, लेकिन यह किसी के टूलकिट में विशेष रूप से लाइब्रेरी लेखकों के लिए एक अच्छा टूल है।
पी.एस. यदि आप रूबी मैजिक की पोस्ट प्रेस से बाहर होते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूजलेटर की सदस्यता लें और एक भी पोस्ट मिस न करें!