রেল রুবির অন্তর্নির্মিত বস্তুতে অনেক কিছু যোগ করে। এটিকে কেউ কেউ রুবির "উপভাষা" বলে এবং এটিই রেল বিকাশকারীদের 1.day.ago
এর মতো লাইন লিখতে দেয় .
এই অতিরিক্ত পদ্ধতিগুলির বেশিরভাগই ActiveSupport-এ থাকে। আজ, আমরা সম্ভবত একটি স্বল্প পরিচিত পদ্ধতি দেখতে যাচ্ছি যা ActiveSupport সরাসরি ক্লাসে যোগ করে:descendants
. এই পদ্ধতিটি বলা ক্লাসের সমস্ত সাবক্লাস রিটার্ন করে। উদাহরণস্বরূপ, ApplicationRecord.descendants
আপনার অ্যাপের সেই ক্লাসগুলি ফিরিয়ে দেবে যা এটি থেকে উত্তরাধিকারসূত্রে পাওয়া যায় (যেমন, আপনার অ্যাপ্লিকেশনের সমস্ত মডেল)। এই নিবন্ধে, আমরা এটি কীভাবে কাজ করে, কেন আপনি এটি ব্যবহার করতে চান এবং কীভাবে এটি রুবির অন্তর্নির্মিত উত্তরাধিকার-সম্পর্কিত পদ্ধতিগুলিকে বাড়িয়ে তোলে তা দেখব৷
অবজেক্ট-ওরিয়েন্টেড ভাষায় উত্তরাধিকার
প্রথমে, আমরা রুবির উত্তরাধিকার মডেলে একটি দ্রুত রিফ্রেশার প্রদান করব। অন্যান্য অবজেক্ট-ওরিয়েন্টেড (OO) ভাষার মতো, রুবি এমন বস্তু ব্যবহার করে যা একটি শ্রেণিবিন্যাসের মধ্যে বসে। আপনি একটি ক্লাস তৈরি করতে পারেন, তারপর সেই ক্লাসের একটি সাবক্লাস, তারপর সেই সাবক্লাসের একটি সাবক্লাস এবং আরও অনেক কিছু। এই শ্রেণিবিন্যাসের দিকে হাঁটার সময়, আমরা পূর্বপুরুষদের একটি তালিকা পাই। রুবিরও চমৎকার বৈশিষ্ট্য রয়েছে যা সমস্ত সত্তাগুলি নিজেই বস্তু (শ্রেণী, পূর্ণসংখ্যা এবং এমনকি শূন্য সহ), যেখানে কিছু অন্যান্য ভাষা প্রায়ই "আদিম" ব্যবহার করে যেগুলি সত্য বস্তু নয়, সাধারণত কার্য সম্পাদনের জন্য (যেমন পূর্ণসংখ্যা, দ্বিগুণ, বুলিয়ান ইত্যাদি; আমি' আমি তোমাকে দেখছি, জাভা)।
রুবি এবং প্রকৃতপক্ষে, সমস্ত 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.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
সাবক্লাসের পূর্বপুরুষরা এখন এইরকম দেখাচ্ছে:
[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,
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
মডিউলগুলির জন্য, আমি মনে করি একটি বাস্তব-বিশ্বের উদাহরণ দেখাতে সাহায্য করে যখন আপনি অন্যটির উপর একটি বেছে নিতে পারেন। এই ধরনের মডিউলগুলির জন্য আমার প্রধান ব্যবহার-কেস হল রেল ইঞ্জিনগুলির সাথে৷
সম্ভবত সবচেয়ে জনপ্রিয় রেল ইঞ্জিনগুলির মধ্যে একটি হল উদ্ভাবন। ধরা যাক আমরা ব্যবহার করা পাসওয়ার্ড ডাইজেস্ট অ্যালগরিদম পরিবর্তন করতে চেয়েছিলাম, কিন্তু প্রথমে, একটি দ্রুত দাবিত্যাগ:
আমার প্রতিদিনের মডিউলগুলির ব্যবহার হল একটি রেল ইঞ্জিনের আচরণ কাস্টমাইজ করা যা আমাদের ডিফল্ট ব্যবসায়িক যুক্তি ধারণ করে। আমরা আমরা নিয়ন্ত্রণ করি কোডের আচরণ ওভাররাইড করছি . আপনি, অবশ্যই, রুবির যেকোনো অংশে একই পদ্ধতি প্রয়োগ করতে পারেন, কিন্তু আমি ওভাররাইডিং কোডের সুপারিশ করব না যা আপনি নিয়ন্ত্রণ করেন না (যেমন, অন্য লোকেদের দ্বারা রক্ষণাবেক্ষণ করা রত্ন থেকে), কারণ সেই বাহ্যিক কোডে যে কোনও পরিবর্তন আপনার পরিবর্তনের সাথে বেমানান হতে পারে৷
Devise এর পাসওয়ার্ড ডাইজেস্ট এখানে ঘটে Devise::Models::DatabaseAuthenticatable মডিউল:
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
-এ থাকে এবং বংশধর শ্রেণীর একটি তালিকা প্রদান করে, অথবা আপনি এটিকে সেই শ্রেণীর সন্তান, নাতি-নাতনি ইত্যাদির "পারিবারিক গাছ" হিসাবে ভাবতে পারেন৷
#descendants-এর বাস্তব-বিশ্ব ব্যবহার
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
যোগ করছি তা যাচাই করার জন্য চশমা সহ ব্যবহার করেছি। আমাদের সকল মডেলের জন্য পদ্ধতি।