Rails, Ruby के बिल्ट-इन ऑब्जेक्ट्स में कई चीजें जोड़ता है। इसे कुछ लोग Ruby की एक "बोली" कहते हैं और यही वह है जो Rails डेवलपर्स को 1.day.ago जैसी लाइनें लिखने की अनुमति देता है।
इनमें से अधिकांश अतिरिक्त विधियां ActiveSupport में रहती हैं। आज, हम एक शायद कम-ज्ञात विधि को देखने जा रहे हैं जो ActiveSupport सीधे Class में जोड़ता है: descendants। यह विधि कॉल की गई क्लास के सभी सबक्लासेस को लौटाती है। उदाहरण के लिए, ApplicationRecord.descendants आपके ऐप में उन क्लासेस को लौटाएगा जो इससे विरासत में मिली हैं (उदाहरण के लिए, आपके एप्लिकेशन में सभी मॉडल्स)। इस लेख में, हम देखेंगे कि यह कैसे काम करता है, आप इसका उपयोग क्यों करना चाह सकते हैं, और यह Ruby के बिल्ट-इन इनहेरिटेंस-संबंधी विधियों को कैसे बढ़ाता है।
ऑब्जेक्ट-ओरिएंटेड भाषाओं में इनहेरिटेंस
सबसे पहले, हम Ruby के इनहेरिटेंस मॉडल पर एक त्वरित पुनश्चर्या प्रदान करेंगे। अन्य ऑब्जेक्ट-ओरिएंटेड (OO) भाषाओं की तरह, Ruby ऑब्जेक्ट्स का उपयोग करता है जो एक पदानुक्रम के भीतर बैठते हैं। आप एक क्लास बना सकते हैं, फिर उस क्लास का एक सबक्लास, फिर उस सबक्लास का एक सबक्लास, और इसी तरह। इस पदानुक्रम को ऊपर चलते समय, हमें पूर्वजों की एक सूची मिलती है। Ruby में यह अच्छी सुविधा भी है कि सभी एंटिटीज स्वयं ऑब्जेक्ट्स हैं (क्लासेस, इंटीजर्स, और यहां तक कि nil सहित), जबकि कुछ अन्य भाषाएं अक्सर "प्रिमिटिव्स" का उपयोग करती हैं जो वास्तविक ऑब्जेक्ट्स नहीं हैं, आमतौर पर प्रदर्शन के लिए (जैसे इंटीजर्स, डबल्स, बूलियन्स, आदि.; मैं आपको देख रहा हूं, Java)।
Ruby और, वास्तव में, सभी OO भाषाओं को पूर्वजों का ट्रैक रखना होता है ताकि यह पता चल सके कि मेथड्स को कहां देखना है और कौन से प्राथमिकता लेते हैं।
class BaseClass
def base
"base"
end
def overridden
"Base"
end
end
class SubClass < BaseClass
def overridden
"Subclass"
end
end
यहां, SubClass.new.overridden कॉल करने पर हमें "SubClass" मिलता है। हालांकि, SubClass.new.base हमारे SubClass परिभाषा में मौजूद नहीं है, इसलिए Ruby यह देखने के लिए प्रत्येक पूर्वज के माध्यम से जाएगा कि कौन सा मेथड लागू करता है (यदि कोई हो)। हम केवल SubClass.ancestors कॉल करके पूर्वजों की सूची देख सकते हैं। Rails में, परिणाम ऐसा कुछ होगा:
[SubClass, BaseClass, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]
हम यहां पूरी सूची का विश्लेषण नहीं करेंगे; हमारे उद्देश्यों के लिए, यह ध्यान देना पर्याप्त है कि SubClass शीर्ष पर है, जिसके नीचे BaseClass है। साथ ही, ध्यान दें कि BasicObject नीचे है; यह Ruby में शीर्ष-स्तरीय ऑब्जेक्ट है, इसलिए यह हमेशा स्टैक के नीचे होगा।
मॉड्यूल्स (उर्फ 'मिक्सिन्स')
जब हम मॉड्यूल्स को मिश्रण में जोड़ते हैं तो चीजें थोड़ी अधिक जटिल हो जाती हैं। एक मॉड्यूल क्लास पदानुक्रम में पूर्वज नहीं है, फिर भी हम इसे अपनी क्लास में "शामिल" कर सकते हैं ताकि Ruby को पता चले कि मेथड के लिए मॉड्यूल की जांच कब करनी है, या यहां तक कि कौन सा मॉड्यूल पहले जांचना है यदि कई मॉड्यूल शामिल हैं।
कुछ भाषाएं इस तरह के "मल्टीपल इनहेरिटेंस" की अनुमति नहीं देती हैं, लेकिन Ruby एक कदम आगे जाता है और हमें यह चुनने देता है कि हम मॉड्यूल को शामिल करते हैं या प्रीपेंड करते हैं, इसके आधार पर मॉड्यूल पदानुक्रम में कहां डाला जाता है।
मॉड्यूल्स प्रीपेंड करना
प्रीपेंडेड मॉड्यूल्स, जैसा कि उनके नाम से कुछ हद तक पता चलता है, क्लास से पहले पूर्वजों की सूची में डाले जाते हैं, मूल रूप से क्लास के किसी भी मेथड को ओवरराइड करते हैं। इसका यह भी अर्थ है कि आप प्रीपेंडेड मॉड्यूल के मेथड में "super" कॉल करके मूल क्लास के मेथड को कॉल कर सकते हैं।
module PrependedModule
def test
"module"
end
def super_test
super
end
end
# पहले से पहले `BaseClass` का पुनः उपयोग
class SubClass < BaseClass
prepend PrependedModule
def test
"Subclass"
end
def super_test
"Super calls SubClass"
end
end
SubClass के पूर्वज अब इस तरह दिखते हैं:
[PrependedModule, SubClass, BaseClass, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ... ]
पूर्वजों की इस नई सूची के साथ, हमारा PrependedModule अब फर्स्ट-इन-लाइन है, जिसका अर्थ है कि Ruby किसी भी मेथड के लिए जिसे हम SubClass पर कॉल करते हैं, वहां पहले देखेगा। इसका भी अर्थ है कि यदि हम PrependedModule के भीतर super कॉल करते हैं, तो हम SubClass पर मेथड कॉल करेंगे:
> SubClass.new.test
=> "module"
> SubClass.new.super_test
=> "Super calls SubClass"
मॉड्यूल्स शामिल करना
दूसरी ओर, शामिल मॉड्यूल्स क्लास के बाद पूर्वजों में डाले जाते हैं। यह उन्हें उन मेथड्स को इंटरसेप्ट करने के लिए आदर्श बनाता है जिन्हें अन्यथा बेस क्लास द्वारा संभाला जाएगा।
class BaseClass
def super_test
"Super calls base class"
end
end
module IncludedModule
def test
"module"
end
def super_test
super
end
end
class SubClass < BaseClass
include IncludedModule
def test
"Subclass"
end
end
इस व्यवस्था के साथ, SubClass के पूर्वज अब इस तरह दिखते हैं:
[SubClass, IncludedModule, BaseClass, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ... ]
अब, SubClass कॉल का पहला बिंदु है, इसलिए Ruby केवल IncludedModule में मेथड्स को निष्पादित करेगा यदि वे SubClass में मौजूद नहीं हैं। जहां तक super का सवाल है, SubClass में super के किसी भी कॉल पहले IncludedModule पर जाएंगे, जबकि IncludedModule के भीतर super के कॉल BaseClass पर जाएंगे।
दूसरे शब्दों में, एक शामिल मॉड्यूल पूर्वज पदानुक्रम में एक सबक्लास और उसके बेस क्लास के बीच बैठता है। इसका प्रभावी अर्थ है कि उनका उपयोग उन मेथड्स को 'इंटरसेप्ट' करने के लिए किया जा सकता है जिन्हें अन्यथा बेस क्लास द्वारा संभाला जाएगा:
> SubClass.new.test
=> "Subclass"
> SubClass.new.super_test
=> "Super calls BaseClass"
इस "कमांड की श्रृंखला" के कारण, Ruby को एक क्लास के पूर्वजों का ट्रैक रखना होता है। रिवर्स सच नहीं है, हालांकि। दी गई एक विशेष क्लास को देखते हुए, Ruby को उसके बच्चों, या "वंशजों" को ट्रैक करने की आवश्यकता नहीं होती है, क्योंकि इसे मेथड निष्पादित करने के लिए इस जानकारी की कभी आवश्यकता नहीं होगी।
पूर्वज क्रम
चतुर पाठकों ने महसूस किया होगा कि यदि हम एक क्लास में कई मॉड्यूल्स का उपयोग कर रहे थे, तो जिस क्रम में हम उन्हें शामिल करते हैं (या प्रीपेंड करते हैं) वह अलग परिणाम उत्पन्न कर सकता है। उदाहरण के लिए, मेथड्स के आधार पर, यह क्लास:
class SubClass < BaseClass
include IncludedModule
include IncludedOtherModule
end
और यह क्लास:
class SubClass < BaseClass
include IncludedOtherModule
include IncludedModule
end
काफी अलग व्यवहार कर सकते हैं। यदि इन दो मॉड्यूल्स में समान नाम वाले मेथड्स थे, तो यहां क्रम यह निर्धारित करेगा कि कौन सा प्राथमिकता ले रहा है और super के कॉल कहां हल होंगे। व्यक्तिगत रूप से, मैं जितना संभव हो सके इस तरह एक-दूसरे को ओवरलैप करने वाले मेथड्स होने से बचूंगा, विशेष रूप से मॉड्यूल्स के शामिल किए गए क्रम जैसी चीजों के बारे में चिंता करने से बचने के लिए।
वास्तविक दुनिया का उपयोग
जबकि include और prepend के बीच अंतर जानना मॉड्यूल्स के लिए अच्छा है, मुझे लगता है कि एक अधिक वास्तविक दुनिया का उदाहरण यह दिखाने में मदद करता है कि आप एक को दूसरे पर कब चुन सकते हैं। मॉड्यूल्स जैसे इनके लिए मेरा मुख्य उपयोग-केस Rails इंजन्स के साथ है।
सबसे लोकप्रिय Rails इंजन्स में से एक Devise है। मान लीजिए हम उपयोग किए जा रहे पासवर्ड डाइजेस्ट एल्गोरिदम को बदलना चाहते थे, लेकिन पहले, एक त्वरित अस्वीकरण:
मेरा दिन-प्रतिदिन मॉड्यूल्स का उपयोग एक Rails इंजन के व्यवहार को अनुकूलित करने के लिए रहा है जो हमारे डिफ़ॉल्ट व्यावसायिक तर्क को रखता है। हम हमारे द्वारा नियंत्रित कोड के व्यवहार को ओवरराइड कर रहे हैं। आप निश्चित रूप से, Ruby के किसी भी टुकड़े पर समान विधि लागू कर सकते हैं, लेकिन मैं उस कोड को ओवरराइड करने की अनुशंसा नहीं करूंगा जिसे आप नियंत्रित नहीं करते (उदाहरण के लिए, अन्य लोगों द्वारा मेंटेन किए गए जेम्स से), क्योंकि उस बाहरी कोड में कोई भी परिवर्तन आपके परिवर्तनों के साथ असंगत हो सकता है।
Devise का पासवर्ड डाइजेस्ट यहां Devise::Models::DatabaseAuthenticatable मॉड्यूल में होता है:
def password_digest(password)
Devise::Encryptor.digest(self.class, password)
end
# और पासवर्ड चेक में भी:
def valid_password?(password)
Devise::Encryptor.compare(self.class, encrypted_password, password)
end
Devise आपको यहां उपयोग किए जा रहे एल्गोरिदम को कस्टमाइज़ करने की अनुमति देता है अपने स्वयं के Devise::Encryptable::Encryptors बनाकर, जो इसे करने का सही तरीका है। प्रदर्शन उद्देश्यों के लिए, हालांकि, हम एक मॉड्यूल का उपयोग करेंगे।
# app/models/password_digest_module
module PasswordDigestModule
def password_digest(password)
# Devise का डिफ़ॉल्ट bcrypt पासवर्ड के लिए बेहतर है,
# यहां केवल प्रदर्शन के लिए sha1 का उपयोग
Digest::SHA1.hexdigest(password)
end
def valid_password?(password)
Devise.secure_compare(password_digest(password), self.encrypted_password)
end
end
begin
User.include(PasswordDigestModule)
# प्रो-टिप - क्योंकि हम यहां User कॉल कर रहे हैं, ActiveRecord
# इस क्लास के लोड होने पर डेटाबेस से पढ़ने का प्रयास करेगा।
# इससे `rails db:create` जैसे कमांड विफल हो सकते हैं।
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
end
इस मॉड्यूल को लोड करने के लिए, आपको डेवलपमेंट में Rails.application.eager_load! कॉल करना होगा या फाइल को लोड करने के लिए एक Rails इनिशियलाइज़र जोड़ना होगा। इसे टेस्ट करके, हम देख सकते हैं कि यह अपेक्षा के अनुरूप काम करता है:
> User.create!(email: "one@test.com", name: "Test", password: "TestPassword")
=> #
> User.first.valid_password?("TestPassword")
=> true
> User.first.encrypted_password
=> "4203189099774a965101b90b74f1d842fc80bf91"
हमारे मामले में यहां, include और prepend दोनों का परिणाम समान होता, लेकिन आइए एक जटिलता जोड़ें। क्या होगा यदि हमारा User मॉडल अपना स्वयं का password_salt मेथड लागू करता है, लेकिन हम इसे अपने मॉड्यूल मेथड्स में ओवरराइड करना चाहते हैं:
class User < ApplicationRecord
# डिफ़ॉल्ट devise मॉड्यूल्स शामिल करें। अन्य उपलब्ध हैं:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
has_many :posts
def password_salt
# पासवर्ड सॉल्ट बनाने का भयानक तरीका,
# विशुद्ध रूप से प्रदर्शन उद्देश्यों के लिए
Base64.encode64(email)[0..-4]
end
end
फिर, हम अपने मॉड्यूल को अपडेट करते हैं ताकि पासवर्ड डाइजेस्ट बनाते समय यह अपने स्वयं के password_salt मेथड का उपयोग करे:
def password_digest(password)
# Devise का डिफ़ॉल्ट bcrypt पासवर्ड के लिए बेहतर है,
# यहां केवल प्रदर्शन के लिए sha1 का उपयोग
Digest::SHA1.hexdigest(password + "." + password_salt)
end
def password_salt
# पासवर्ड सॉल्ट उत्पन्न करने का एक और भी बुरा तरीका
"salt"
end
अब, include और prepend अलग-अलग व्यवहार करेंगे क्योंकि हम किसका उपयोग करते हैं यह निर्धारित करेगा कि Ruby कौन सा password_salt मेथड निष्पादित करता है। prepend के साथ, मॉड्यूल प्राथमिकता लेगा, और हमें यह मिलेगा:
> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"
मॉड्यूल को include का उपयोग करने के लिए बदलने का अर्थ होगा कि User क्लास कार्यान्वयन प्राथमिकता लेता है:
> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"
आम तौर पर, मैं पहले prepend की ओर जाता हूं क्योंकि, एक मॉड्यूल लिखते समय, मुझे लगता है कि इसे सबक्लास की तरह अधिक मानना आसान है और मान लें कि मॉड्यूल में कोई भी मेथड क्लास के संस्करण को ओवरराइड करेगा। जाहिर है, यह हमेशा वांछित नहीं होता, इसीलिए Ruby हमें include विकल्प भी देता है।
वंशज
हमने देखा है कि Ruby कैसे क्लास पूर्वजों का ट्रैक रखता है ताकि मेथड्स निष्पादित करते समय प्राथमिकता के क्रम को जाना जा सके, साथ ही हम मॉड्यूल्स के माध्यम से इस सूची में प्रविष्टियों को कैसे डालते हैं। हालांकि, प्रोग्रामर्स के रूप में, एक क्लास के वंशजों के माध्यम से पुनरावृति करना भी उपयोगी हो सकता है। यहीं पर ActiveSupport की #descendants विधि आती है। यह विधि काफी छोटी है और यदि आवश्यक हो तो Rails के बाहर आसानी से डुप्लिकेट की जा सकती है:
class Class
def descendants
ObjectSpace.each_object(singleton_class).reject do |k|
k.singleton_class? || k == self
end
end
end
ObjectSpace Ruby का एक बहुत ही दिलचस्प हिस्सा है जो वर्तमान में मेमोरी में हर Ruby ऑब्जेक्ट के बारे में जानकारी संग्रहीत करता है। हम इसमें यहां गोता नहीं लगाएंगे, लेकिन यदि आपके एप्लिकेशन में एक क्लास परिभाषित है (और यह लोड हो चुका है), तो यह ObjectSpace में मौजूद होगा। ObjectSpace#each_object, जब एक मॉड्यूल पास किया जाता है, तो केवल उन ऑब्जेक्ट्स को लौटाता है जो मॉड्यूल से मेल खाते हैं या उसके सबक्लास हैं; यहां ब्लॉक शीर्ष स्तर को भी अस्वीकार करता है (उदाहरण के लिए, यदि हम Numeric.descendants कॉल करते हैं, तो हम Numeric को परिणामों में होने की उम्मीद नहीं करते)।
चिंता न करें अगर आप पूरी तरह से समझ नहीं पाते कि यहां क्या हो रहा है, क्योंकि ObjectSpace पर अधिक पढ़ना शायद इसे वास्तव में समझने के लिए आवश्यक है। हमारे उद्देश्यों के लिए, यह जानना पर्याप्त है कि यह विधि Class पर रहती है और वंशज क्लासेस की एक सूची लौटाती है, या आप इसे उस क्लास के बच्चों, पोते, आदि के "परिवार के पेड़" के रूप में सोच सकते हैं।
#descendants का वास्तविक दुनिया उपयोग
2018 RailsConf में, Ryan Laughlin ने 'चेकअप' पर एक बात दी। वीडियो देखने लायक है, लेकिन हम सिर्फ एक विचार निकालेंगे, जो है समय-समय पर अपने डेटाबेस में सभी पंक्तियों के माध्यम से चलाना और जांचना कि क्या वे आपके मॉडल्स के वैधता चेक पास करते हैं। आप आश्चर्यचकित हो सकते हैं कि आपके डेटाबेस में कितनी पंक्तियां #valid? टेस्ट पास नहीं करतीं।
सवाल तो यह है कि हम मॉडल्स की सूची को मैन्युअली मेंटेन किए बिना इस चेक को कैसे लागू करते हैं? #descendants उत्तर है:
# सुनिश्चित करें कि सभी मॉडल लोड हैं (उत्पादन में आवश्यक नहीं होना चाहिए)
Rails.application.load! if Rails.env.development?
ApplicationRecord.descendants.each do |model_class|
# वास्तविक दुनिया में आप इसे बैकग्राउंड जॉब(एस) पर भेजना चाहेंगे
model_class.all.each do |record|
if !record.valid?
HoneyBadger.notify("Invalid #{model.name} found with ID: #{record.id}")
end
end
end
यहां, ApplicationRecord.descendants हमें एक मानक Rails एप्लिकेशन में हर मॉडल की सूची देता है। हमारे लूप में, तब, model क्लास है (उदाहरण के लिए, User या Product)। यहां कार्यान्वयन काफी बुनियादी है, लेकिन परिणाम यह है कि यह हर मॉडल (या, अधिक सटीक रूप से, ApplicationRecord का हर सबक्लास) के माध्यम से पुनरावृति करेगा और हर पंक्ति के लिए .valid? कॉल करेगा।
निष्कर्ष
अधिकांश Rails डेवलपर्स के लिए, मॉड्यूल्स आमतौर पर उपयोग नहीं किए जाते हैं। यह एक अच्छे कारण के लिए है; यदि आप कोड के मालिक हैं, तो आमतौर पर इसके व्यवहार को अनुकूलित करने के आसान तरीके हैं, और यदि आप नहीं कोड के मालिक हैं, तो मॉड्यूल्स के साथ इसके व्यवहार को बदलने में जोखिम हैं। फिर भी, उनके उपयोग-मामले हैं, और यह Ruby के लचीलेपन का एक प्रमाण है कि न केवल हम दूसरी फाइल से एक क्लास को बदल सकते हैं, हमारे पास यह विकल्प भी है कि हम अपने मॉड्यूल को पूर्वज श्रृंखला में कहां प्रकट होने दें।
ActiveSupport तब #ancestors के व्युत्क्रम को #descendants के साथ प्रदान करता है। यह विधि जहां तक मैंने देखा है, कम ही उपयोग की जाती है, लेकिन एक बार जब आप जानते हैं कि यह वहां है, तो आप शायद इसके लिए अधिक से अधिक उपयोग पाएंगे। व्यक्तिगत रूप से, मैंने इसका उपयोग न केवल मॉडल वैधता की जांच के लिए किया है, बल्कि स्पेक्स के साथ भी यह मान्य करने के लिए कि हम अपने सभी मॉडल्स के लिए attribute_alias मेथड्स को सही ढंग से जोड़ रहे हैं।