रूबी की अंतर्निर्मित वस्तुओं में रेल कई चीजें जोड़ती हैं। इसे ही कुछ लोग रूबी की "बोली" कहते हैं और यही रेल डेवलपर्स को 1.day.ago
जैसी लाइनें लिखने की अनुमति देता है। ।
इनमें से अधिकतर अतिरिक्त विधियां ActiveSupport में रहती हैं। आज, हम शायद एक कम-ज्ञात विधि को देखने जा रहे हैं जिसे ActiveSupport सीधे कक्षा में जोड़ता है:descendants
. यह विधि बुलाए गए वर्ग के सभी उपवर्गों को लौटाती है। उदाहरण के लिए, ApplicationRecord.descendants
आपके ऐप में उन कक्षाओं को वापस कर देगा जो इससे प्राप्त होती हैं (उदाहरण के लिए, आपके आवेदन के सभी मॉडल)। इस लेख में, हम इस पर एक नज़र डालेंगे कि यह कैसे काम करता है, आप इसका उपयोग क्यों करना चाहते हैं, और यह रूबी की अंतर्निहित विरासत-संबंधी विधियों को कैसे बढ़ाता है।
वस्तु-उन्मुख भाषाओं में वंशानुक्रम
सबसे पहले, हम रूबी के इनहेरिटेंस मॉडल पर एक त्वरित रिफ्रेशर प्रदान करेंगे। अन्य ऑब्जेक्ट-ओरिएंटेड (OO) भाषाओं की तरह, रूबी उन वस्तुओं का उपयोग करती है जो एक पदानुक्रम के भीतर बैठती हैं। आप एक वर्ग बना सकते हैं, फिर उस वर्ग का उपवर्ग, फिर उस उपवर्ग का उपवर्ग, और इसी तरह। इस पदानुक्रम पर चलते हुए, हमें पूर्वजों की एक सूची मिलती है। रूबी में यह भी अच्छी विशेषता है कि सभी इकाइयाँ स्वयं वस्तुएँ हैं (वर्गों, पूर्णांकों और यहाँ तक कि शून्य सहित), जबकि कुछ अन्य भाषाएँ अक्सर "आदिम" का उपयोग करती हैं जो वास्तविक वस्तु नहीं हैं, आमतौर पर प्रदर्शन के लिए (जैसे पूर्णांक, युगल, बूलियन, आदि; I ' मैं आपको देख रहा हूँ, जावा)।
रूबी और, वास्तव में, सभी ओओ भाषाओं को पूर्वजों का ट्रैक रखना होगा ताकि यह जान सके कि तरीकों को कहां देखना है और कौन सी प्राथमिकता है।
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.ancestors
. पर कॉल करके पूर्वजों की सूची देख सकते हैं . रेल में, परिणाम कुछ इस तरह होगा:
[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
तल पर है; यह रूबी में शीर्ष-स्तरीय ऑब्जेक्ट है, इसलिए यह हमेशा स्टैक के निचले भाग में रहेगा।
मॉड्यूल (a.k.a. 'Mixins')
जब हम मिश्रण में मॉड्यूल जोड़ते हैं तो चीजें थोड़ी अधिक जटिल हो जाती हैं। एक मॉड्यूल वर्ग पदानुक्रम में पूर्वज नहीं है, फिर भी हम इसे अपनी कक्षा में "शामिल" कर सकते हैं, इसलिए रूबी को यह जानना होगा कि किसी विधि के लिए मॉड्यूल की जांच कब करनी है, या यहां तक कि कौन से मॉड्यूल को पहले जांचना है, जिसमें कई मॉड्यूल शामिल हैं। ।
कुछ भाषाएं इस तरह के "मल्टीपल इनहेरिटेंस" की अनुमति नहीं देती हैं, लेकिन रूबी हमें यह चुनने की अनुमति देकर एक कदम और आगे बढ़ जाती है कि मॉड्यूल को पदानुक्रम में कहां डाला जाता है, चाहे हम मॉड्यूल को शामिल करें या प्रीपेन्ड करें।
प्रीपेंडिंग मॉड्यूल
प्रीपेड मॉड्यूल, जैसा कि उनके नाम से कुछ पता चलता है, कक्षा से पहले पूर्वजों की सूची में डाला जाता है, मूल रूप से किसी भी वर्ग के तरीकों को ओवरराइड करता है। इसका मतलब यह भी है कि आप मूल वर्ग 'विधि को कॉल करने के लिए प्रीपेड मॉड्यूल की विधि में "सुपर" को कॉल कर सकते हैं।
module PrependedModule
def test
"module"
end
def super_test
super
end
end
# Re-using `BaseClass` from earlier
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
अब प्रथम-इन-लाइन है, जिसका अर्थ है कि रूबी किसी भी तरीके के लिए सबसे पहले वहां दिखेगी जिसे हम SubClass
पर कॉल करते हैं . यह भी इसका मतलब है कि अगर हम super
. कहते हैं PrependedModule
. के भीतर , हम इस विधि को 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,
...
]
अब, सबक्लास कॉल का पहला बिंदु है, इसलिए रूबी केवल IncludedModule
में विधियों को निष्पादित करेगी। अगर वे SubClass
. में मौजूद नहीं हैं . जहां तक super
का सवाल है , super
. को कोई भी कॉल SubClass
. में IncludedModule
. पर जाएगा सबसे पहले, जबकि कोई भी super
. पर कॉल करता है भीतर IncludedModule
BaseClass
पर जाएगा ।
एक और तरीका रखो, एक शामिल मॉड्यूल पूर्वज पदानुक्रम में एक उपवर्ग और उसके आधार वर्ग के बीच बैठता है। इसका प्रभावी ढंग से मतलब है कि उनका उपयोग उन तरीकों को 'अवरोधन' करने के लिए किया जा सकता है जिन्हें अन्यथा आधार वर्ग द्वारा नियंत्रित किया जाएगा:
> SubClass.new.test
=> "Subclass"
> SubClass.new.super_test
=> "Super calls BaseClass"
इस "कमांड की श्रृंखला" के कारण, रूबी को एक वर्ग पूर्वजों का ट्रैक रखना पड़ता है। हालांकि इसका उल्टा सच नहीं है। किसी विशेष वर्ग को देखते हुए, रूबी को अपने बच्चों, या "वंशजों" को ट्रैक करने की आवश्यकता नहीं है, क्योंकि किसी विधि को निष्पादित करने के लिए उसे कभी भी इस जानकारी की आवश्यकता नहीं होगी।
पूर्वजों का आदेश
होशियार पाठकों ने महसूस किया होगा कि यदि हम एक कक्षा में कई मॉड्यूल का उपयोग कर रहे थे, तो जिस क्रम में हम उन्हें शामिल करते हैं (या प्रीपेन्ड) करते हैं, वह अलग-अलग परिणाम दे सकता है। उदाहरण के लिए, विधियों के आधार पर, यह वर्ग:
class SubClass < BaseClass
include IncludedModule
include IncludedOtherModule
end
और यह वर्ग:
class SubClass < BaseClass
include IncludedOtherModule
include IncludedModule
end
काफी अलग व्यवहार कर सकता था। यदि इन दो मॉड्यूल में एक ही नाम के तरीके थे, तो यहां का क्रम यह निर्धारित करेगा कि कौन सा वरीयता ले रहा है और जहां super
. को कॉल किया जाता है का समाधान किया जाएगा। व्यक्तिगत रूप से, मैं ऐसे तरीकों से बचना चाहूंगा जो एक-दूसरे को इस तरह से ओवरलैप करते हैं, विशेष रूप से मॉड्यूल को शामिल करने के क्रम जैसी चीजों के बारे में चिंता करने से बचने के लिए।
असली दुनिया में इस्तेमाल
जबकि include
. के बीच अंतर जानना अच्छा है और prepend
मॉड्यूल के लिए, मुझे लगता है कि एक अधिक वास्तविक दुनिया का उदाहरण यह दिखाने में मदद करता है कि आप एक को दूसरे पर कब चुन सकते हैं। इस तरह के मॉड्यूल के लिए मेरा मुख्य उपयोग-मामला रेल इंजन के साथ है।
संभवत:सबसे लोकप्रिय रेल इंजनों में से एक वसीयत है। मान लें कि हम उपयोग किए जा रहे पासवर्ड डाइजेस्ट एल्गोरिथम को बदलना चाहते हैं, लेकिन पहले, एक त्वरित अस्वीकरण:
मॉड्यूल का मेरा दैनिक उपयोग रेल इंजन के व्यवहार को अनुकूलित करने के लिए किया गया है जिसमें हमारा डिफ़ॉल्ट व्यावसायिक तर्क है। हम कोड के व्यवहार को ओवरराइड कर रहे हैं हम नियंत्रित करते हैं . बेशक, आप रूबी के किसी भी टुकड़े पर वही विधि लागू कर सकते हैं, लेकिन मैं उस कोड को ओवरराइड करने की अनुशंसा नहीं करता जिसे आप नियंत्रित नहीं करते हैं (उदाहरण के लिए, अन्य लोगों द्वारा बनाए गए रत्नों से), क्योंकि उस बाहरी कोड में कोई भी परिवर्तन आपके परिवर्तनों के साथ असंगत हो सकता है।
डेविस का पासवर्ड डाइजेस्ट यहां डेविस ::मॉडल ::डेटाबेस प्रमाणीकरण योग्य मॉड्यूल में होता है:
def password_digest(password)
Devise::Encryptor.digest(self.class, password)
end
# and also in the password check:
def valid_password?(password)
Devise::Encryptor.compare(self.class, encrypted_password, password)
end
डेविस आपको अपना खुद का Devise::Encryptable::Encryptors
बनाकर यहां उपयोग किए जा रहे एल्गोरिदम को अनुकूलित करने की अनुमति देता है। , जो इसे करने का सही तरीका है। प्रदर्शन उद्देश्यों के लिए, हालांकि, हम एक मॉड्यूल का उपयोग करेंगे।
# app/models/password_digest_module
module PasswordDigestModule
def password_digest(password)
# Devise's default bcrypt is better for passwords,
# using sha1 here just for demonstration
Digest::SHA1.hexdigest(password)
end
def valid_password?(password)
Devise.secure_compare(password_digest(password), self.encrypted_password)
end
end
begin
User.include(PasswordDigestModule)
# Pro-tip - because we are calling User here, ActiveRecord will
# try to read from the database when this class is loaded.
# This can cause commands like `rails db:create` to fail.
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
end
इस मॉड्यूल को लोड करने के लिए, आपको Rails.application.eager_load!
पर कॉल करना होगा। विकास में या फ़ाइल लोड करने के लिए एक रेल प्रारंभकर्ता जोड़ें। इसका परीक्षण करके, हम देख सकते हैं कि यह अपेक्षा के अनुरूप काम करता है:
> User.create!(email: "[email protected]", name: "Test", password: "TestPassword")
=> #<User id: 1, name: "Test", created_at: "2021-05-01 02:08:29", updated_at: "2021-05-01 02:08:29", posts_count: nil, email: "[email protected]">
> User.first.valid_password?("TestPassword")
=> true
> User.first.encrypted_password
=> "4203189099774a965101b90b74f1d842fc80bf91"
यहां हमारे मामले में, दोनों include
और prepend
एक ही परिणाम होगा, लेकिन चलिए एक जटिलता जोड़ते हैं। क्या होगा यदि हमारा उपयोगकर्ता मॉडल अपना password_salt
. लागू करता है विधि, लेकिन हम इसे अपने मॉड्यूल विधियों में ओवरराइड करना चाहते हैं:
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts
def password_salt
# Terrible way to create a password salt,
# purely for demonstration purposes
Base64.encode64(email)[0..-4]
end
end
फिर, हम अपने मॉड्यूल को उसके अपने password_salt
. का उपयोग करने के लिए अपडेट करते हैं पासवर्ड डाइजेस्ट बनाते समय विधि:
def password_digest(password)
# Devise's default bcrypt is better for passwords,
# using sha1 here just for demonstration
Digest::SHA1.hexdigest(password + "." + password_salt)
end
def password_salt
# an even worse way of generating a password salt
"salt"
end
अब, include
और prepend
अलग तरह से व्यवहार करेगा क्योंकि हम किसका उपयोग करते हैं यह निर्धारित करेगा कि कौन सा password_salt
विधि रूबी निष्पादित करता है। prepend
. के साथ , मॉड्यूल को प्राथमिकता दी जाएगी, और हमें यह मिलेगा:
> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"
उपयोग करने के लिए मॉड्यूल को बदलना include
इसके बजाय इसका अर्थ यह होगा कि उपयोगकर्ता वर्ग के कार्यान्वयन को प्राथमिकता दी जाती है:
> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"
आम तौर पर, मैं prepend
. के लिए पहुंचता हूं सबसे पहले, क्योंकि मॉड्यूल लिखते समय, मुझे इसे उप-वर्ग की तरह व्यवहार करना आसान लगता है और मान लें कि मॉड्यूल में कोई भी विधि कक्षा के संस्करण को ओवरराइड कर देगी। जाहिर है, यह हमेशा वांछित नहीं होता है, यही वजह है कि रूबी हमें include
. भी देती है विकल्प।
वंशज
हमने देखा है कि रूबी कैसे विधियों को निष्पादित करते समय वरीयता क्रम जानने के लिए कक्षा पूर्वजों का ट्रैक रखती है, साथ ही साथ हम मॉड्यूल के माध्यम से इस सूची में प्रविष्टियां कैसे सम्मिलित करते हैं। हालांकि, प्रोग्रामर के रूप में, यह एक वर्ग के सभी वंशजों के माध्यम से पुनरावृति करना उपयोगी हो सकता है , बहुत। यहीं पर ActiveSupport का #descendants
. है विधि आती है। यदि आवश्यक हो तो विधि काफी छोटी है और आसानी से रेल के बाहर दोहराई जाती है:
class Class
def descendants
ObjectSpace.each_object(singleton_class).reject do |k|
k.singleton_class? || k == self
end
end
end
ऑब्जेक्टस्पेस रूबी का एक बहुत ही रोचक हिस्सा है जो स्मृति में वर्तमान में प्रत्येक रूबी ऑब्जेक्ट के बारे में जानकारी संग्रहीत करता है। हम यहां इसमें गोता नहीं लगाएंगे, लेकिन यदि आपके आवेदन में परिभाषित एक वर्ग है (और इसे लोड किया गया है), तो यह ऑब्जेक्टस्पेस में मौजूद होगा। ObjectSpace#each_object
, जब एक मॉड्यूल पास किया जाता है, तो केवल वही ऑब्जेक्ट लौटाता है जो मॉड्यूल के उपवर्ग से मेल खाते हैं या हैं; यहां ब्लॉक शीर्ष स्तर को भी अस्वीकार करता है (उदाहरण के लिए, यदि हम Numeric.descendants
कहते हैं , हम Numeric
. की अपेक्षा नहीं करते हैं परिणामों में होने के लिए)।
चिंता न करें अगर आपको यह नहीं पता कि यहां क्या हो रहा है, क्योंकि वास्तव में इसे प्राप्त करने के लिए ऑब्जेक्टस्पेस पर अधिक पढ़ने की आवश्यकता है। हमारे उद्देश्यों के लिए, यह जानना पर्याप्त है कि यह विधि Class
. पर रहती है और वंशज वर्गों की एक सूची देता है, या आप इसे उस वर्ग के बच्चों, पोते-पोतियों, आदि के "पारिवारिक वृक्ष" के रूप में सोच सकते हैं।
#वंशजों का वास्तविक-विश्व उपयोग
2018 RailsConf में, रयान लाफलिन ने 'चेकअप' पर बात की। वीडियो देखने लायक है, लेकिन हम केवल एक विचार निकालेंगे, जो समय-समय पर आपके डेटाबेस में सभी पंक्तियों के माध्यम से चलना है और जांचना है कि क्या वे आपके मॉडल की वैधता जांच पास करते हैं। आपको आश्चर्य हो सकता है कि आपके डेटाबेस में कितनी पंक्तियाँ #valid?
. को पास नहीं करती हैं परीक्षण।
तो सवाल यह है कि मॉडल की सूची को मैन्युअल रूप से बनाए रखने के बिना हम इस चेक को कैसे कार्यान्वित करते हैं? #descendants
उत्तर है:
# Ensure all models are loaded (should not be necessary in production)
Rails.application.load! if Rails.env.development?
ApplicationRecord.descendants.each do |model_class|
# in the real world you'd want to send this off to background job(s)
model_class.all.each do |record|
if !record.valid?
HoneyBadger.notify("Invalid #{model.name} found with ID: #{record.id}")
end
end
end
यहां, ApplicationRecord.descendants
हमें मानक रेल एप्लिकेशन में प्रत्येक मॉडल की एक सूची देता है। हमारे लूप में, तब, model
वर्ग है (उदा., User
या Product
) यहां कार्यान्वयन बहुत बुनियादी है, लेकिन परिणाम यह है कि यह प्रत्येक मॉडल (या, अधिक सटीक रूप से, ApplicationRecord के प्रत्येक उपवर्ग) के माध्यम से पुनरावृति करेगा और .valid?
पर कॉल करेगा। हर पंक्ति के लिए।
निष्कर्ष
अधिकांश रेल डेवलपर्स के लिए, मॉड्यूल का आमतौर पर उपयोग नहीं किया जाता है। यह एक अच्छे कारण के लिए है; यदि आप कोड के स्वामी हैं, तो आमतौर पर इसके व्यवहार को अनुकूलित करने के आसान तरीके होते हैं, और यदि आप नहीं करते हैं कोड के मालिक हैं, मॉड्यूल के साथ इसके व्यवहार को बदलने में जोखिम हैं। फिर भी, उनके पास उनके उपयोग के मामले हैं, और यह रूबी के लचीलेपन के लिए एक वसीयतनामा है कि न केवल हम किसी अन्य फ़ाइल से एक वर्ग को बदल सकते हैं, हमारे पास कहां चुनने का विकल्प भी है पूर्वजों की श्रृंखला में हमारा मॉड्यूल प्रकट होता है।
ActiveSupport तब #ancestors
. का विलोम प्रदान करने के लिए आता है #descendants
. के साथ . जहाँ तक मैंने देखा है, इस पद्धति का शायद ही कभी उपयोग किया जाता है, लेकिन एक बार जब आप जान जाते हैं कि यह वहाँ है, तो आप शायद इसके लिए अधिक से अधिक उपयोग पाएंगे। व्यक्तिगत रूप से, मैंने इसका उपयोग न केवल मॉडल की वैधता की जाँच के लिए किया है, बल्कि विशिष्टताओं के साथ भी यह सत्यापित करने के लिए कि हम attribute_alias
सही ढंग से जोड़ रहे हैं हमारे सभी मॉडलों के लिए तरीके।