অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজের বৈশিষ্ট্য (Characteristics of Object-Oriented Languages)
প্রোগ্রামিং কমিউনিটিতে কোনো একটি ল্যাঙ্গুয়েজকে অবজেক্ট-ওরিয়েন্টেড হিসেবে বিবেচনা করার জন্য তার কী কী বৈশিষ্ট্য থাকা আবশ্যক, সে বিষয়ে কোনো সর্বসম্মত মত নেই। Rust অনেকগুলো প্রোগ্রামিং প্যারাডাইম দ্বারা প্রভাবিত, যার মধ্যে OOP একটি; উদাহরণস্বরূপ, আমরা ১৩তম অধ্যায়ে ফাংশনাল প্রোগ্রামিং থেকে আসা বৈশিষ্ট্যগুলো দেখেছি। বলা যায়, OOP ল্যাঙ্গুয়েজগুলোর কিছু সাধারণ বৈশিষ্ট্য রয়েছে, যেমন—অবজেক্ট (objects), এনক্যাপসুলেশন (encapsulation), এবং ইনহেরিটেন্স (inheritance)। চলুন দেখি এই বৈশিষ্ট্যগুলোর প্রত্যেকটির অর্থ কী এবং Rust সেগুলোকে সমর্থন করে কিনা।
অবজেক্টে ডেটা এবং আচরণ (Behavior) দুটোই থাকে (Objects Contain Data and Behavior)
এরিক গামা, রিচার্ড হেলম, রালফ জনসন এবং জন ভিসাইডসের লেখা Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994) বইটি, যা কথোপকথনে "গ্যাং অফ ফোর" (The Gang of Four) বই হিসাবে পরিচিত, অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্নের একটি ক্যাটালগ। এটি OOP-কে এভাবে সংজ্ঞায়িত করে:
অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামগুলো অবজেক্ট দিয়ে তৈরি। একটি অবজেক্ট ডেটা এবং সেই ডেটার উপর কাজ করে এমন প্রসিডিউর (procedure) উভয়কেই প্যাকেজ করে। এই প্রসিডিউরগুলোকে সাধারণত মেথড (methods) বা অপারেশন (operations) বলা হয়।
এই সংজ্ঞা অনুসারে, Rust একটি অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজ: struct
এবং enum
-এর মধ্যে ডেটা থাকে, এবং impl
ব্লকগুলো struct
ও enum
-এর উপর মেথড সরবরাহ করে। যদিও মেথডসহ struct
এবং enum
-কে অবজেক্ট বলা হয় না, তবে "গ্যাং অফ ফোর"-এর সংজ্ঞা অনুসারে এগুলো একই কার্যকারিতা প্রদান করে।
এনক্যাপসুলেশন যা ভেতরের বিবরণ লুকিয়ে রাখে (Encapsulation That Hides Implementation Details)
OOP-এর সাথে জড়িত আরেকটি সাধারণ ধারণা হলো এনক্যাপসুলেশন (encapsulation), যার মানে হলো একটি অবজেক্টের ভেতরের কার্যকারিতার বিবরণ (implementation details) সেই অবজেক্ট ব্যবহারকারী কোডের কাছে সরাসরি অ্যাক্সেসযোগ্য থাকে না। সুতরাং, একটি অবজেক্টের সাথে ইন্টারঅ্যাক্ট করার একমাত্র উপায় হলো তার পাবলিক API; অবজেক্ট ব্যবহারকারী কোডের উচিত নয় অবজেক্টের গভীরে প্রবেশ করে সরাসরি ডেটা বা আচরণ পরিবর্তন করা। এটি প্রোগ্রামারকে অবজেক্ট ব্যবহারকারী কোড পরিবর্তন না করেই অবজেক্টের ভেতরের অংশ পরিবর্তন এবং রিফ্যাক্টর করার সুযোগ দেয়।
আমরা ৭ম অধ্যায়ে আলোচনা করেছি কীভাবে এনক্যাপসুলেশন নিয়ন্ত্রণ করতে হয়: আমরা pub
কীওয়ার্ড ব্যবহার করে ঠিক করতে পারি যে আমাদের কোডের কোন মডিউল, টাইপ, ফাংশন এবং মেথড পাবলিক হবে, এবং ডিফল্টভাবে বাকি সবকিছু প্রাইভেট থাকে। উদাহরণস্বরূপ, আমরা একটি AveragedCollection
struct সংজ্ঞায়িত করতে পারি যার একটি ফিল্ডে i32
মানের একটি ভেক্টর থাকবে। এই struct-এ এমন একটি ফিল্ডও থাকতে পারে যেখানে ভেক্টরের মানগুলোর গড় সংরক্ষিত থাকবে, যার মানে হলো যখনই কারো গড়ের প্রয়োজন হবে, তখন আর নতুন করে গণনা করতে হবে না। অন্য কথায়, AveragedCollection
আমাদের জন্য গণনা করা গড় ক্যাশ (cache) করে রাখবে। লিস্টিং ১৮-১ এ AveragedCollection
struct-এর সংজ্ঞা দেওয়া হলো।
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
Struct-টিকে pub
হিসেবে চিহ্নিত করা হয়েছে যাতে অন্য কোড এটি ব্যবহার করতে পারে, কিন্তু struct-এর ভেতরের ফিল্ডগুলো প্রাইভেট থাকে। এক্ষেত্রে এটি গুরুত্বপূর্ণ কারণ আমরা নিশ্চিত করতে চাই যে যখনই তালিকা থেকে কোনো মান যোগ বা حذف করা হবে, তখন গড়ও আপডেট করা হবে। আমরা এটি করি struct-এর উপর add
, remove
, এবং average
মেথড প্রয়োগ করে, যা লিস্টিং ১৮-২ এ দেখানো হয়েছে।
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
পাবলিক মেথড add
, remove
, এবং average
হলো AveragedCollection
-এর একটি ইনস্ট্যান্সের ডেটা অ্যাক্সেস বা পরিবর্তন করার একমাত্র উপায়। যখন add
মেথড ব্যবহার করে list
-এ একটি আইটেম যোগ করা হয় বা remove
মেথড ব্যবহার করে حذف করা হয়, তখন প্রতিটির ইমপ্লিমেন্টেশন প্রাইভেট update_average
মেথডকে কল করে, যা average
ফিল্ড আপডেট করার কাজ করে।
আমরা list
এবং average
ফিল্ড দুটিকে প্রাইভেট রেখেছি যাতে বাইরের কোনো কোড সরাসরি list
ফিল্ডে আইটেম যোগ বা حذف করতে না পারে; অন্যথায়, list
পরিবর্তন হলে average
ফিল্ডটি অসামঞ্জস্যপূর্ণ (out of sync) হয়ে যেতে পারে। average
মেথডটি average
ফিল্ডের মান রিটার্ন করে, যা বাইরের কোডকে average
পড়ার সুযোগ দেয় কিন্তু পরিবর্তন করার নয়।
যেহেতু আমরা AveragedCollection
struct-এর ভেতরের বিবরণ এনক্যাপসুলেট করেছি, তাই আমরা ভবিষ্যতে ডেটা স্ট্রাকচারের মতো বিভিন্ন দিক সহজেই পরিবর্তন করতে পারব। উদাহরণস্বরূপ, list
ফিল্ডের জন্য Vec<i32>
-এর পরিবর্তে HashSet<i32>
ব্যবহার করতে পারি। যতক্ষণ পর্যন্ত add
, remove
, এবং average
পাবলিক মেথডগুলোর সিগনেচার একই থাকবে, AveragedCollection
ব্যবহারকারী কোড পরিবর্তন করার কোনো প্রয়োজন হবে না। যদি আমরা list
-কে পাবলিক করতাম, তবে এটি সম্ভব হতো না: HashSet<i32>
এবং Vec<i32>
-তে আইটেম যোগ এবং حذف করার জন্য ভিন্ন ভিন্ন মেথড রয়েছে, তাই বাইরের কোড যদি সরাসরি list
পরিবর্তন করত, তবে সম্ভবত সেটিও পরিবর্তন করতে হতো।
যদি কোনো ল্যাঙ্গুয়েজকে অবজেক্ট-ওরিয়েন্টেড হিসেবে বিবেচনা করার জন্য এনক্যাপসুলেশন একটি প্রয়োজনীয় দিক হয়, তাহলে Rust সেই শর্ত পূরণ করে। কোডের বিভিন্ন অংশের জন্য pub
ব্যবহার করার বা না করার বিকল্পটি ভেতরের বিবরণ এনক্যাপসুলেট করতে সক্ষম করে।
টাইপ সিস্টেম এবং কোড শেয়ারিং হিসাবে ইনহেরিটেন্স (Inheritance as a Type System and as Code Sharing)
ইনহেরিটেন্স (Inheritance) হলো এমন একটি প্রক্রিয়া যার মাধ্যমে একটি অবজেক্ট অন্য একটি অবজেক্টের সংজ্ঞা থেকে বিভিন্ন উপাদান উত্তরাধিকার সূত্রে পেতে পারে, ফলে প্যারেন্ট অবজেক্টের ডেটা এবং আচরণ পুনরায় কোড না লিখেই পাওয়া যায়।
যদি কোনো ল্যাঙ্গুয়েজকে অবজেক্ট-ওরিয়েন্টেড হতে হলে ইনহেরিটেন্স থাকতেই হয়, তবে Rust সেই ধরনের ল্যাঙ্গুয়েজ নয়। ম্যাক্রো ব্যবহার না করে এমন কোনো struct সংজ্ঞায়িত করার উপায় নেই যা প্যারেন্ট struct-এর ফিল্ড এবং মেথড ইমপ্লিমেন্টেশন উত্তরাধিকার সূত্রে পাবে।
তবে, আপনি যদি আপনার প্রোগ্রামিং টুলবক্সে ইনহেরিটেন্স ব্যবহারে অভ্যস্ত হন, তবে Rust-এ আপনি অন্য সমাধান ব্যবহার করতে পারেন, যা নির্ভর করবে আপনি কী কারণে ইনহেরিটেন্স ব্যবহার করতে চাইছেন তার উপর।
আপনি মূলত দুটি প্রধান কারণে ইনহেরিটেন্স বেছে নেবেন। একটি হলো কোড পুনঃব্যবহার (reuse of code): আপনি একটি টাইপের জন্য নির্দিষ্ট আচরণ ইমপ্লিমেন্ট করতে পারেন এবং ইনহেরিটেন্স আপনাকে সেই ইমপ্লিমেন্টেশনটি অন্য একটি টাইপের জন্য পুনঃব্যবহারের সুযোগ দেয়। আপনি Rust কোডে ডিফল্ট ট্রেইট মেথড ইমপ্লিমেন্টেশন ব্যবহার করে সীমিত আকারে এটি করতে পারেন, যা আপনি লিস্টিং ১০-১৪-তে দেখেছেন যখন আমরা Summary
trait-এ summarize
মেথডের একটি ডিফল্ট ইমপ্লিমেন্টেশন যোগ করেছিলাম। Summary
trait ইমপ্লিমেন্ট করা যেকোনো টাইপ কোনো অতিরিক্ত কোড ছাড়াই summarize
মেথডটি ব্যবহার করতে পারবে। এটি অনেকটা একটি প্যারেন্ট ক্লাসের কোনো মেথডের ইমপ্লিমেন্টেশন থাকার মতো, যা উত্তরাধিকার সূত্রে পাওয়া চাইল্ড ক্লাসেও সেই মেথডটি থাকে। আমরা Summary
trait ইমপ্লিমেন্ট করার সময় summarize
মেথডের ডিফল্ট ইমপ্লিমেন্টেশনটি ওভাররাইডও করতে পারি, যা একটি চাইল্ড ক্লাসের প্যারেন্ট ক্লাস থেকে উত্তরাধিকার সূত্রে পাওয়া মেথডের ইমপ্লিমেন্টেশন ওভাররাইড করার মতো।
ইনহেরিটেন্স ব্যবহারের অন্য কারণটি টাইপ সিস্টেমের সাথে সম্পর্কিত: একটি চাইল্ড টাইপকে প্যারেন্ট টাইপের জায়গায় ব্যবহার করতে সক্ষম করা। একে পলিমরফিজম (polymorphism) বলা হয়, যার মানে হলো আপনি রানটাইমে একাধিক অবজেক্টকে একে অপরের বিকল্প হিসেবে ব্যবহার করতে পারবেন যদি তাদের মধ্যে নির্দিষ্ট কিছু বৈশিষ্ট্য থাকে।
পলিমরফিজম (Polymorphism)
অনেকের কাছে পলিমরফিজম এবং ইনহেরিটেন্স সমার্থক। কিন্তু এটি আসলে একটি আরও সাধারণ ধারণা যা এমন কোডকে বোঝায় যা একাধিক টাইপের ডেটা নিয়ে কাজ করতে পারে। ইনহেরিটেন্সের ক্ষেত্রে, এই টাইপগুলো সাধারণত সাব-ক্লাস (subclass) হয়।
এর পরিবর্তে, Rust বিভিন্ন সম্ভাব্য টাইপের জন্য জেনেরিক (generics) ব্যবহার করে এবং সেই টাইপগুলোকে কী সরবরাহ করতে হবে তার উপর সীমাবদ্ধতা আরোপ করার জন্য ট্রেইট বাউন্ড (trait bounds) ব্যবহার করে। একে কখনও কখনও বাউন্ডেড প্যারামেট্রিক পলিমরফিজম (bounded parametric polymorphism) বলা হয়।
ইনহেরিটেন্সের সুবিধা প্রদান না করে Rust ভিন্ন একটি পথ বেছে নিয়েছে। ইনহেরিটেন্স প্রায়শই প্রয়োজনের চেয়ে বেশি কোড শেয়ার করার ঝুঁকিতে থাকে। সাব-ক্লাসগুলোর সবসময় তাদের প্যারেন্ট ক্লাসের সমস্ত বৈশিষ্ট্য শেয়ার করা উচিত নয়, কিন্তু ইনহেরিটেন্সের মাধ্যমে তারা তা করে ফেলে। এটি একটি প্রোগ্রামের ডিজাইনকে কম নমনীয় করে তুলতে পারে। এটি সাব-ক্লাসের উপর এমন মেথড কল করার সম্ভাবনা তৈরি করে যা অর্থহীন বা ত্রুটির কারণ হতে পারে কারণ মেথডগুলো সাব-ক্লাসের জন্য প্রযোজ্য নয়। এছাড়াও, কিছু ল্যাঙ্গুয়েজ শুধুমাত্র সিঙ্গেল ইনহেরিটেন্স (single inheritance) অনুমোদন করে (অর্থাৎ একটি সাব-ক্লাস শুধুমাত্র একটি ক্লাস থেকে ইনহেরিট করতে পারে), যা একটি প্রোগ্রামের ডিজাইনের নমনীয়তাকে আরও সীমাবদ্ধ করে।
এই কারণগুলোর জন্য, Rust পলিমরফিজম সক্ষম করার জন্য ইনহেরিটেন্সের পরিবর্তে ট্রেইট অবজেক্ট (trait objects) ব্যবহারের ভিন্ন পদ্ধতি গ্রহণ করে। চলুন দেখি ট্রেইট অবজেক্ট কীভাবে কাজ করে।