রুবির স্ট্যান্ডার্ড লাইব্রেরির লুকানো রত্নগুলোর আজকের অভিযানে, আমরা ডেলিগেশন নিয়ে দেখব।
দুর্ভাগ্যক্রমে, এই টার্মটি — অনেকের মত — বছরের ধারে কিছুটা গুলগুলে হয়ে গেছে এবং আলাদা মানুষের কাছে আলাদা অর্থ বহন করে। উইকিপিডিয়ার arabera:
ডেলিগেশন বলতে একটি অবজেক্টের (রিসিভার) একটি মেম্বার (প্রপার্টি বা মেথড) যেকোনো অন্য মূল অবজেক্টের (সেন্ডার) কনটেক্স্টে ইভ্যালুয়েট করা বোঝায়। ডেলিগেশন eksplisite করা যায়, সেন্ডিং অবজেক্টকে রিসিভিং অবজেক্টে পাস করে, যা যেকোনো অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজে করা যায়; অথবা ইমপ্লিসিটলি, ল্যাঙ্গুয়েজের মেম্বার লুকআপ নিয়ম দ্বারা, যেটি ল্যাঙ্গুয়েজের ফিচার সাপোর্ট দিয়ে সম্ভব।
তবে, প্রায়শই লোকজন এই টার্মটি একটি অবজেক্ট অন্য অবজেক্টের সংশ্লিষ্ট মেথড কল করার বর্ণনা করার জন্য ব্যবহার করে, সেলফকে আর্গুমেন্ট হিসেবে পাস না করে, যা আরও সঠিকভাবে "ফরওয়ার্ডিং" বলা যায়।
এটা দূর করলে, আমরা আর্টিকেলের বাকি অংশে "ডেলিগেশন" শব্দটি এই দুটি প্যাটার্ন উভয়ের বর্ণনা করতে ব্যবহার করব।
Delegator
রুবির স্ট্যান্ডার্ড লাইব্রেরির Delegator ক্লাস দেখি যেটি ডেলিগেশনের বিভিন্ন প্যাটার্ন প্রদান করে।
SimpleDelegator
এগুলোর মধ্যে সবচেয়ে সহজ, এবং যা আমি জঙ্গলে সবচেয়ে বেশি দেখেছি, সেটি হল SimpleDelegator, যা ইনিশিয়ালাইজারের মাধ্যমে প্রদত্ত একটি অবজেক্ট র্যাপ করে এবং তারপর সব মিসিং মেথড সেটাতে ডেলিগেট করে। এটা একশনে দেখি:
require 'delegate' User = Struct.new(:first_name, :last_name) class UserDecorator < SimpleDelegator def full_name "#{first_name} #{last_name}" end end
প্রথমে, আমাদের require 'delegate' করতে হয়েছে SimpleDelegator আমাদের কোডে আপলোড করার জন্য। আমরা একটি Struct ব্যবহার করেছি একটি সিম্পল User ক্লাস তৈরি করতে first_name এবং last_name এক্সেসর সহ। তারপর আমরা UserDecorator যোগ করেছি যা full_name মেথড ডিফাইন করে इंडিভিজুয়াল নেম পার্টগুলোকে একটি স্ট্রিংয়ে কম্বাইন করে। এখানেই SimpleDelegator কাজে আসে: যেহেতু first_name এবং last_name করেন্ট ক্লাসে ডিফাইন করা নেই, সেগুলো বদলে র্যাপ করা অবজেক্টে কল করা হবে:
decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"
SimpleDelegator আমাদের ডেলিগেট করা মেথডগুলো super দিয়ে ওভাররাইড করার অনুমতিও দেয়, যা র্যাপ করা অবজেক্টের সংশ্লিষ্ট মেথড কল করে। আমরা আমাদের উদাহরণে এটি ব্যবহার করতে পারি শুধু ফার্স্ট নেমের ইনিশিয়াল দেখাতে পূর্ণ ফার্স্ট নেমের বদলে:
class UserDecorator < SimpleDelegator def first_name "#{super[0]}." end end
decorated_user.first_name #=> "J." decorated_user.full_name #=> "J. Doe"
Delegator
উপরের উদাহরণ পড়ার সময়, কি আপনি চিন্তা করেছিলেন আমাদের UserDecorator কীভাবে জানে কোন অবজেক্টে ডেলিগেট করতে হবে? উত্তরটি在于 SimpleDelegator-এর প্যারেন্ট ক্লাস — Delegator। এটি কাস্টম ডেলিগেশন স্কিম ডিফাইন করার জন্য একটি абстракт বেস ক্লাস যা __getobj__ এবং __setobj__ এর ইমপ্লিমেন্টেশন প্রদান করে ডেলিগেশন টার্গেট পাওয়া এবং সেট করার জন্য। এই জ্ঞান ব্যবহার করে, আমরা সহজে আমাদের নিজস্ব SimpleDelegator ভার্সন বানাতে পারি ডেমোনের জন্য:
class MyDelegator < Delegator attr_accessor :wrapped alias_method :__getobj__, :wrapped def initialize(obj) @wrapped = obj end end class UserDecorator < MyDelegator def full_name "#{first_name} #{last_name}" end end
এটি SimpleDelegator-এর রিয়েল ইমপ্লিমেন্টেশন থেকে কিছুটা আলাদা যেটি __setobj__ কল করে তার initialize মেথডে। যেহেতু আমাদের কাস্টম ডেলিগেটর ক্লাসের সেটির দরকার নেই, আমরা পুরো মেথডটি বাদ দিলাম।
এটি আগের উদাহরণের মতই কাজ করতে পারে; এবং এটি কাজ করে:
UserDecorator.superclass #=> MyDelegator < Delegator decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"
DelegateMethod
Delegate যেটি আমাদের জন্য শেষ ডেলিগেশন প্যাটার্ন প্রদান করে সেটি হল কিছুটা অদ্ভুত নামের Object.DelegateClass মেথড। এটি একটি নির্দিষ্ট ক্লাসের জন্য একটি ডেলিগেটর ক্লাস জেনারেট করে এবং রিটার্ন করে, যা আমরা তারপর ইনহেরিট করতে পারি:
class MyClass < DelegateClass(ClassToDelegateTo) def initialize super(obj_of_ClassToDelegateTo) end end
এটা প্রথমে কনফিউজিং লাগতে পারে — বিশেষ করে এই ফ্যাক্ট যে ইনহেরিটেন্সের রাইট-হ্যান্ড সাইডে আrbit্রারি রুবি কোড থাকতে পারে — কিন্তু আসলে এটি আগে এক্সপ্লোর করা প্যাটার্নগুলো ফলো করে, অর্থাৎ এটি SimpleDelegator-এ ইনহেরিট করার সাথে সাদৃশ্য রয়েছে।
রুবির স্ট্যান্ডার্ড লাইব্রেরি এই ফিচার ব্যবহার করে তার Tempfile ক্লাস ডিফাইন করতে যা এর অধিকাংশ কাজ File ক্লাসে ডেলিগেট করে যখন স্টোরেজ লোকেশন এবং ফাইল ডিলিশনের বিষয়ে কিছু স্পেশাল নিয়ম সেট আপ করে। আমরা একই মেকানিজম ব্যবহার করতে পারি একটি কাস্টম Logfile ক্লাস সেট আপ করতে এইভাবে:
class Logfile < DelegateClass(File) MODE = File::WRONLY|File::CREAT|File::APPEND def initialize(basename, logdir = '/var/log') # Create logfile in location specified by logdir path = File.join(logdir, basename) logfile = File.open(path, MODE, 0644) # This will call Delegator's initialize method, so below this point # we can call any method from File on our Logfile instances. super(logfile) end end
Forwardable
দিলচস্পভাবে, রুবির স্ট্যান্ডার্ড লাইব্রেরি আমাদের ডেলিগেশনের জন্য আরেকটি লাইব্রেরি দেয় Forwardable মডিউল এবং এর def_delegator ও def_delegators মেথড রূপে।
আমাদের মূল UserDecorator উদাহরণটি Forwardable দিয়ে রিরাইট করি।
require 'forwardable' User = Struct.new(:first_name, :last_name) class UserDecorator extend Forwardable def_delegators :@user, :first_name, :last_name def initialize(user) @user = user end def full_name "#{first_name} #{last_name}" end end decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"
সবচেয়ে নোটিশযোগ্য পার্থক্য হলো ডেলিগেশন অটোম্যাটিক্যালি method_missing এর মাধ্যমে প্রদান না হয়ে, প্রতিটি মেথড যা আমরা ফরওয়ার্ড করতে চাই সেটি এক্সপ্লিসিটলি ডিক্লেয়ার করতে হয়। এটি আমাদের র্যাপ করা অবজেক্টের যেকোনো মেথড যেগুলো আমরা আমাদের ক্লায়েন্টদের এক্সপোজ করতে চাই না সেগুলো "লুকাতে" দেয়, যা আমাদের পাবলিক ইন্টারফেসের ওপর বেশি কন্ট্রোল দেয় এবং এটি হলো মূল কারণ আমি সাধারণত Forwardable পছন্দ করি SimpleDelegator-এর চেয়ে।
Forwardable-এর আরেকটি সুন্দর ফিচার হলো ডেলিগেট করা মেথডগুলো def_delegator দিয়ে রিনেম করার ক্ষমতা, যা একটি অপশনাল থার্ড আর্গুমেন্ট নেয় ডিজায়ারড আলিয়াস স্পেসিফাই করার জন্য:
class UserDecorator extend Forwardable def_delegator :@user, :first_name, :personal_name def_delegator :@user, :last_name, :family_name def initialize(user) @user = user end def full_name "#{personal_name} #{family_name}" end end
উপরোক্ত UserDecorator শুধু আলিয়াসড personal_name এবং family_name মেথড এক্সপোজ করে, র্যাপ করা User অবজেক্টের first_name এবং last_name-এ ফরওয়ার্ড করা থাকলেও:
decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.first_name #=> NoMethodError: undefined method `first_name' for #<UserDecorator:0x000000010f995cb8> decorated_user.personal_name #=> "John"
এই ফিচার иногда খুব কাজে আসে। আমি এর আগে সফলভাবে এটি ব্যবহার کرده हूं লাইব্রেরি মাইগ্রেট করার সময় যেগুলোর ইন্টারফেস সিমিলার কিন্তু মেথড নামগুলোর বিষয়ে আলাদা এক্সপেক্টেশন ছিল।
স্ট্যান্ডার্ড লাইব্রেরির বাইরে
স্ট্যান্ডার্ড লাইব্রেরিতে বিদ্যমান ডেলিগেশন সমাধানগুলো থাকা সত্ত্বেও, রুবি কমিউনিটি বছরের ধারে বেশ কিছু বিকল্প তৈরি করেছে এবং আমরা দুটো এক্সপ্লোর করব।
delegate
Rails-এর জনপ্রিয়তাকে বিবেচনা করলে, এর delegate মেথড সম্ভবত রুবি ডেভেলপাররা সবচেয়ে বেশি ব্যবহৃত ডেলিগেশন ফর্ম। এখানে কিভাবে আমরা আমাদের পুরানো UserDecorator রিরাইট করতে পারি:
# In a real Rails app this would most likely be a subclass of ApplicationRecord User = Struct.new(:first_name, :last_name) class UserDecorator attr_reader :user delegate :first_name, :last_name, to: :user def initialize(user) @user = user end def full_name "#{first_name} #{last_name}" end end decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"
এটি Forwardable-এর সাথে খুব মিল, কিন্তু আমাদের extend ব্যবহার করতে হয় না যেহেতু delegate সরাসরি Module-এ ডিফাইন করা থাকে এবং তাই প্রতিটি ক্লাস বা মডিউল বডিতে উপলব্ধ (ভালো বা মন্দ, আপনি ডिसাইড করুন)। যাক, delegate-এর কয়েকটা সুন্দর ট্রিক আছে। প্রথমে, আছে :prefix অপশন যা ডেলিগেট করা মেথড নামগুলোর সাথে যে অবজেক্টে ডেলিগেট করছি তার নাম প্রিফিক্স হিসেবে যোগ করে। সুতরাং,
delegate :first_name, :last_name, to: :user, prefix: true
জেনারেট করবে user_first_name এবং user_last_name মেথড। অথবা আমরা কাস্টম প্রিফিক্স দিতে পারি:
delegate :first_name, :last_name, to: :user, prefix: :account
এখন আমরা ইউজারের নেমের বিভিন্ন পার্ট account_first_name এবং account_last_name হিসেবে অ্যাক্সেস করতে পারি।
delegate-এর আরেকটি দিলচস্প অপশন হলো এর :allow_nil অপশন। যদি যে অবজেক্টে ডেলিগেট করছি সেটি বর্তমানে nil হয় — উদাহরণস্বরূপ অনসেট ActiveRecord রিলেশন miatt — তবে সাধারণত আমরা NoMethodError পাই:
decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil
তবে, :allow_nil অপশন দিয়ে, এই কল সাকসেসফুল হবে এবং nil রিটার্ন করবে:
class UserDecorator delegate :first_name, :last_name, to: :user, allow_nil: true ... end decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> nil
Casting
শেষ ডেলিগেশন অপশন যা আমরা দেখব সেটি হল Jim Gay-এর Casting জেম, যা ডেভেলপারদের অনুমতি দেয় "রুবিতে মেথড ডেলিগেট করতে এবং self সংরক্ষণ করতে"। এটি সম্ভবত ডেলিগেশনের স্ট্রিক্ট ডেফিনিশনের সবচেয়ে কাছাকাছি, কারণ এটি রুবির ডাইনামিক প্রকৃতি ব্যবহার করে একটি মেথড কলের রিসিভারকে অস্থায়ীভাবে রিবাইন্ড করে, এর মত:
UserDecorator.instance_method(:full_name).bind(user).call #=> "John Doe"
সবচেয়ে দিলচস্প দিকটি হলো ডেভেলপাররা অবজেক্টগুলোর সুপারক্লাস হায়ারার্কি পরিবর্তন না করে আচরণ যোগ করতে পারেন।
require 'casting' User = Struct.new(:first_name, :last_name) module UserDecorator def full_name "#{first_name} #{last_name}" end end user = User.new("John", "Doe") user.extend(Casting::Client) user.delegate(:full_name, UserDecorator)
এখানে আমরা user-কে Casting::Client দিয়ে এক্সটেন্ড করেছি, যা আমাদের delegate মেথডে অ্যাক্সেস দেয়। অথবা, আমরা include Casting::Client ব্যবহার করতে পারতাম User ক্লাসের ভিতরে যাতে সব ইনস্ট্যান্সের এই ক্ষমতা থাকে।
অতিরিক্তভাবে, Casting অপশন প্রদান করে আচরণ অস্থায়ীভাবে যোগ করার জন্য একটি ব্লকের লাইফটাইম পর্যন্ত বা ম্যানুয়ালি রিমুভ করার 떼। এতে কাজ করার জন্য, আমাদের প্রথমে মিসিং মেথডের ডেলিগেশন ইয়েবল করতে হবে:
user.delegate_missing_methods
একটি সিঙ্গেল ব্লকের ডুরেশন জন্য আচরণ যোগ করতে, আমরা Casting-এর delegating ক্লাস মেথড ব্যবহার করতে পারি:
Casting.delegating(user => UserDecorator) do user.full_name #=> "John Doe" end user.full_name #NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">
অথবা, আমরা আচরণ যোগ করতে পারি যতক্ষণ না আমরা এক্সপ্লিসিটলি uncast কল করি:
user.cast_as(UserDecorator) user.full_name #=> "John Doe" user.uncast NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">
অন্য প্রস্তাবিত সমাধানগুলো থেকে কিছুটা জটিল হলেও, Casting অনেক কন্ট্রোল দেয় এবং Jim তার Clean Ruby বইতে এর বিভিন্ন ব্যবহার এবং আরও কিছু দেখিয়েছেন।
সারসংক্ষেপ
ডেলিগেশন এবং মেথড ফরওয়ার্ডিং সম্পর্কিত অবজেক্টগুলোর মধ্যে দায়িত্ব ভাগ করার জন্য উপযোগী প্যাটার্ন। প্লেইন রুবি প্রজেক্টে, Delegator এবং Forwardable উভয়ই ব্যবহার করা যায়, যেখানে রেইলস কোড তার delegate মেথডের দিকে ঝুকে। সর্বোচ্চ কন্ট্রোলের জন্য কী ডেলিগেট হবে, Casting জেম একটি চমৎকার পছন্দ, যদিও এটি অন্যদের চেয়ে কিছুটা জটিল।
গেস্ট অথর মাইকেল কোলের রুবির সাথে প্রেম সম্পর্ক ২০০৩ সালের দিকে শুরু হয়েছিল। তিনি লেখা এবং ভাষণ দেওয়াও পছন্দ করেন এবং Bangkok.rb এবং RubyConf Thailand কো-অর্গানাইজ করেন।