কম্পিউটার টিউটোরিয়াল

ActiveSupport::Descendants ব্যাখ্যা: Rails ডেভেলপারদের জন্য একটি কমপ্রيهেনসিভ গাইড

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-র টপ-লেভেল Object, তাই এটি সবসময় স্ট্যাকের বটমে থাকবে।

মডিউলস (অর্থাৎ 'Mixins')

জিনিসগুলো একটু কমপ্লিকেটেড হয়ে যায় যখন আমরা মডিউলসকে মিক্সে আনে। একটি মডিউল ক্লাস হায়ারার্কিতে একটি এনসেস্টার নয়, फिरও আমরা এটিকে আমাদের ক্লাসে "ইনক্লুড" করতে পারি যাতে Ruby জানতে পারে কখন মডিউলটি চেক করতে হবে একটি মেথডের জন্য, অথবা এমনকি কোন মডিউলটি প্রথমে চেক করতে হবে কেসে একাধিক মডিউল ইনক্লুড করা হয়।

কিছু ল্যাঙ্গুয়েজ এই ধরনের "মাল্টিপল ইনহেরিটেন্স" আউজ করবে না, কিন্তু Ruby এক ধাপ বাড়িয়ে যায় আমাদের চয়েস দিয়ে যে মডিউলটি হায়ারার্কিতে কোথায় ইনসার্ট হবে সেটি ডিফাইন করতে, সেটি ডিপেন্ড করে আমরা মডিউলটি ইনক্লুড করি না প্রিপেন্ড করি।

মডিউলস প্রিপেন্ড করা

প্রিপেন্ডেড মডিউলস, তাদের নাম কিছুটা সাজেস্ট করে, এনসেস্টারস লিস্টে ক্লাসের আগে ইনসার্ট হয়, বেসিক্যালি ক্লাসের যেকোনো মেথড ওভাররাইড করে। এর মানে আপনি একটি প্রিপেন্ডেড মডিউলের মেথডে "super" কল করতে পারেন অরিজিনাল ক্লাসের মেথড কল করতে।

module PrependedModule
  def test
    "module"
  end
  def super_test
    super
  end
end

# Earlier `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-এ কল করি। এটি অর্থাৎ যদি আমরা 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, ... ]

এখন, SubClass ফার্স্ট পয়েন্ট অফ কল, তাই Ruby শুধু IncludedModule-এ মেথড এক্সিকিউট করবে যদি সেগুলো SubClass-এ প্রেজেন্ট না থাকে। super-এর জন্য, SubClass-এ যেকোনো কল super-এ যাবে IncludedModule-এ প্রথমে, در khi 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-এ কলগুলো কোথায় রিজল্ভ হবে। পারসোনালি, আমি এইরকম ওভারল্যাপিং মেথড থাকা থেকে যেন《》 éviter করি যতটা সম্ভব, স্পেসিফিক্যালি এইরকম বিষয়ে চিন্তা করতে না হয়ে যেতে মডিউল ইনক্লুড করার অর্ডার।

রিয়েল-ওয়ার্ল্ড ইউজেজ

যদি include এবং prepend-এর পার্থক্য মডিউলসের জন্য জানা ভালো, আমি মনে করি একটি আরও রিয়েল-ওয়ার্ল্ড উদাহরণ দেখাতে সাহায্য করবে কখন আপনি একটা বরাবর অন্যটা বেছে নেবেন। মডিউলসের মত এইগুলোর সাথে আমার মেইন ইউজ-কেস হচ্ছে Rails ইঞ্জিনস।

সম্ভবত সবচেয়ে জনপ্রিয় Rails ইঞ্জিন হচ্ছে devise। ধরুন আমরা পাসওয়ার্ড ডাইজেস্ট অ্যালগরিদম পরিবর্তন করতে চাই যা ব্যবহার হচ্ছে, কিন্তু প্রথমে, একটি কুইক ডিসক্লেমার:

মডিউলসের সাথে আমার দিনদিনের ব্যবহার হচ্ছে একটি Rails ইঞ্জিনের বিহেভিয়ার কাস্টোমাইজ করা যেটি আমাদের ডিফল্ট বিজনেস লজিক ধারণ করে। আমরা কোডের বিহেভিয়ার ওভাররাইড করছি যেটি আমরা কন্ট্রোল করি। আপনি অবশ্যই একই মেথড অ্যাপ্লাই করতে পারেন যেকোনো Ruby টুকরোতে, কিন্তু আমি পরামর্শ দিচ্ছি না কোড ওভাররাইড করা যা আপনি কন্ট্রোল করেন না (যেমন, অন্য লোকের মেইনটেইন করা জেমস থেকে), কারণ সেই এক্সটার্নাল কোডে যেকোনো পরিবর্তন আপনার পরিবর্তনlarla ইনকম্প্যাটিবল হতে পারে।

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
  # 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
    # পাসওয়ার্ড সল্ট তৈরি করার খারাপ উপায়,
    # পিউরলি ডেমো উদ্দেশ্যে
    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 ভিন্নভাবে বিহেইভ করবে কারণ আমরা কোনটি ব্যবহার করি সেটি নির্ধারণ করবে কোন password_salt মেথড Ruby এক্সিকিউট করবে। prepend-এর সাথে, মডিউলটি প্রেসিডেন্স নেবে, এবং আমরা পাব:

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"

মডিউলটি include ব্যবহার করতে পরিবর্তন করলে বদলে User ক্লাস ইমপ্লিমেন্টেশন প্রেসিডেন্স নেবে:

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"

সাধারণত, আমি prepend-এ প্রথমে হাত দিয়ে থাকি কারণ, একটি মডিউল লিখতে গিয়ে, আমি এটাকে আরও সাবক্লাসের মত টリট করা সহজ পাই এবং অনুমান করি মডিউলের যেকোনো মেথড ক্লাসের ভার্সন ওভাররাইড করবে। অবশ্যই, এটি সবসময় ডিজায়ারড নয়, যাইহোক Ruby আমাদের include অপশনও দিয়েছে।

Descendants

আমরা দেখেছি কীভাবে 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-এর রিয়েল-ওয়ার্ল্ড ইউজ

২০১৮ RailsConf-এ, Ryan Laughlin 'checkups' ऊपर একটি টক দিয়েছিলেন। ভিডিওটি দেখা যাক, কিন্তু আমরা শুধু একটি আইডিয়া এক্সট্রাক্ট করব, যা হলো পিরিওডিক্যালি আপনার ডেটাবেসের সব সারি রান থ্রু করে এবং চেক করা যে সেগুলো আপনার মডেলের ভ্যালিডিটি চেক পাস করে কিনা। আপনি চকিত হবেন জানতে আপনার ডেটাবেসের কত সারি #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? কল করবে।

উপসংহার

  1. রুবি মেথড ব্যাখ্যা: কাস্টম ফাংশন সহজে তৈরি করুন

  2. রুবির `select` মেথড মাস্টার করুন: প্র্যাক্টিক্যাল উদাহরণ ও ব্যবহার গাইড

  3. রুবি মাস্টারিং: মেথড এলিয়াস তৈরি করার জন্য Alias কিওয়ার্ড ব্যবহার

  4. রুবির Freeze মেথড বোঝা: অবজেক্ট মিউটেবিলিটি ব্যাখ্যা