ইনহেরিটেন্স এবং পলিমরফিজম (Inheritance & Polymorphism)
বিদ্যমান জিনিসগুলোর ওপর ভিত্তি করে তৈরি (Building on What Exists)
ধরুন, আপনি ড্রয়িং (drawing) করার একটি অ্যাপ তৈরি করছেন। এর মধ্যে বিভিন্ন শেপ বা আকৃতি (shapes) রয়েছে — যেমন বৃত্ত বা সার্কেল (circles), চতুর্ভুজ বা রেক্ট্যাঙ্গেল (rectangles), কিংবা ত্রিভুজ বা ট্রায়াঙ্গেল (triangles)। এসকল শেপেরই মূলত নিজস্ব কিছু সাধারণ বৈশিষ্ট্য (shared traits) রয়েছে: যেমন নির্দিষ্ট একটি রঙ (color), একটি অবস্থান (position), বা একটি area() মেথড (method)। কিন্তু এই প্রতিটি শেপের (shape) নিজস্ব এরিয়া বা ক্ষেত্রফল (area) বের করার নিয়ম আলাদা।
এক্ষেত্রে আপনি চাইলেই এই সমস্ত শেপ ক্লাসে (shape class) ওই শেয়ার করা বা সাধারণ কোডগুলোকে কপি-পেস্ট (copy-paste) করে নিতে পারেন। অথবা এর সবচেয়ে ভালো উপায় হলো, একটি বেস ক্লাস (base class) তৈরি করা, যার নাম দেওয়া যেতে পারে Shape এবং যেখানে কোডটিকে শুধু একবারই (once) লেখা হবে, এবং এরপর ওই অন্যান্য নির্দিষ্ট শেপগুলোকে (specific shape) এর থেকে ইনহেরিট (inherit) করার সুযোগ দেওয়া হবে, যাতে করে সেগুলোতে শুধুমাত্র সেগুলোর নিজস্ব অনন্য (unique) অংশগুলোই যুক্ত করা যায়।
আর এটিকেই মূলত ইনহেরিটেন্স (inheritance) বা "ইজ-এ" ("is-a") রিলেশনশিপ বা সম্পর্ক বলা হয়। যেমন একটি Circle হলো একটি Shape। আবার একটি Rectangle-ও হলো একটি Shape। এরা মূলত একটি সাধারণ Shape-এর ইন্টারফেসগুলোকে (interface) শেয়ার করে, কিন্তু এদের প্রত্যেকেরই নিজস্ব কিছু আলাদা বাস্তবায়ন বা ইমপ্লিমেন্টেশন (implementation) থাকে।
বেসিক ইনহেরিটেন্স (Basic Inheritance) — শেপ হায়ারার্কি (Shape Hierarchy)
আসল সমস্যাটি হলো: একটি বেস পয়েন্টারের (Base Pointer) মাধ্যমে আসলে কী ঘটে?
আর এটিই হলো এখানকার সবচেয়ে ইন্টারেস্টিং (interesting) — এবং ট্রিকি (tricky) অংশ। যদি আপনি একটি Shape* পয়েন্টারের (pointer) মাধ্যমে কোনো একটি Circle-কে স্টোর বা সংরক্ষণ করেন এবং এর ওই area() মেথডটিকে কল করেন, তবে মূলত এর কোন ভার্সনটি রান (runs) করবে? এক্ষেত্রে কোনো virtual কিওয়ার্ড (keyword) ছাড়া এটি মূলত Shape-এর ভার্সনটিকেই (version) কল করবে — যদিও এখানকার আসল অবজেক্টটি (actual object) হলো একটি সার্কেল (Circle)। একে মূলত স্ট্যাটিক বাইন্ডিং (static binding) বলা হয়, এবং এটি কখনোই আপনার আশানুরূপ বা কাঙ্ক্ষিত ফল দেবে না।
তাহলে এর সমাধানটি কী (fix)? এখানকার এই বেস ক্লাসের (base class) মেথডটিকে মূলত virtual হিসেবে তৈরি করা। যা মূলত তার কম্পাইলারকে (compiler) এই নির্দেশটি দেয় যে: "এটিকে কম্পাইল টাইমের (compile time) সময় নির্ধারণ করো না — বরং এর রানটাইমের (runtime) সময় সরাসরি এর এই বর্তমান অবজেক্টের টাইপটিকে (actual object type) চেক (check) করো।" আর এটিকেই পলিমরফিজম (polymorphism) বলা হয়।
virtual ফাংশনগুলো — অ্যাকশন বা বা বাস্তবে (Action) পলিমরফিজম (Polymorphism)
এই override কিওয়ার্ডটি (Keyword) — আপনার নিজস্ব সুরক্ষা জালের (Safety Net) মতো
যখনই আপনি কোনো একটি ভার্চুয়াল ফাংশনকে (virtual function) ওভাররাইড (override) করতে চাইবেন, তখন সবসময়ই override কিওয়ার্ডটি ব্যবহার করুন। এই কিওয়ার্ডটি ছাড়া এর ফাংশনের সিগনেচারে (function signature) কোনো সাধারণ একটি ভুল বা টাইপোও (typo) আপনাকে নীরবে ওই বেসটিকে (base) ওভাররাইড করার বদলে একটি সম্পূর্ণ নতুন (new) ফাংশন তৈরি করে দেবে। কিন্তু এই override-এর সাহায্যে এখানকার কম্পাইলারটি (compiler) মূলত আপনার যেকোনো ধরনের ভুলকে ধরে (catches) ফেলতে পারবে।
// override ছাড়া — এর সিগনেচারটি (signature) না মিললে এটি মূলত সাইলেন্ট বাগ (silent bug) তৈরি করতে পারে
double area() const { ... } // এটি হয়তো আসলটিকে ঠিকমতো ওভাররাইড (override) নাও করতে পারে!
// override-এর সাহায্যে — এটি কোনো বেস ভার্চুয়ালের (base virtual) সাথে না মিললে তৎক্ষণাৎ কম্পাইলার এরর (compiler error) দেবে
double area() const override { ... } // এটি সম্পূর্ণ নিরাপদ বা Safe!পিওর ভার্চুয়াল ফাংশনগুলো (Pure Virtual Functions) ও অ্যাবস্ট্রাক্ট ক্লাসগুলো (Abstract Classes)
কখনো কখনো কোনো একটি বেস ক্লাসের মেথডে (base class method) কোনো যৌক্তিক ডিফল্ট ইমপ্লিমেন্টেশন বা বাস্তবায়ন (reasonable default implementation) থাকে না। যেমন এক্ষেত্রে প্রতিটি শেপেরই (shape) একটি ক্ষেত্রফল (area) থাকা দরকার, কিন্তু শুধু একটি "Shape" নিজে থেকে কখনোই জানতে পারে না যে কীভাবে সেটি গণনা বা ক্যালকুলেট (calculate) করতে হয়। আর ঠিক এখানেই পিওর ভার্চুয়াল ফাংশনগুলোর (pure virtual functions) প্রয়োজনীয়তাগুলো চলে আসে।
এক্ষেত্রে মূলত = 0 লেখার মাধ্যমে আপনি এটি বলে দিচ্ছেন যে: "আমি এটিকে নিজে থেকে ইমপ্লিমেন্ট বা প্রয়োগ করছি না — বরং এর প্রতিটি ডেরাইভড ক্লাসকেই (derived class) এটি অবশ্যই করতে হবে (MUST)।" অর্থাৎ এমন যেকোনো ক্লাস (class) যার অন্তত একটি পিওর ভার্চুয়াল ফাংশন (pure virtual function) রয়েছে তাকে মূলত অ্যাবস্ট্রাক্ট (abstract) বলা হয় — যেখানে আপনি এগুলোকে সরাসরি এর কোনো ইনস্ট্যান্স (instances) তৈরি করার জন্য ব্যবহার করতে পারবেন না।
অ্যাবস্ট্রাক্ট ক্লাসগুলো (Abstract Classes) — বিভিন্ন বাধ্যবাধকতা (Contract) তৈরি করা
কেন এই ভার্চুয়াল ডেস্ট্রাক্টরগুলো (Virtual Destructors) এত বেশি গুরুত্বপূর্ণ (Matter)
এটি মূলত সি++ (C++)-এর অন্যতম সবচেয়ে গুরুত্বপূর্ণ (most important) একটি নিয়ম, আর এটিকে উপেক্ষা (ignoring) করলে এটি সাইলেন্ট মেমরি লিকের (silent memory leaks) মতো অনেক বড় বড় সমস্যা তৈরি করতে পারে।
আপনি যখন কোনো একটি বেস ক্লাস পয়েন্টারের (base class pointer) (যেমন delete basePtr;) সাহায্যে কোনো একটি অবজেক্টকে (object) ডিলিট (delete) করেন, তখন কম্পাইলারটির (compiler) এটি জানার প্রয়োজন পড়ে যে এটি ঠিক কোন ডেস্ট্রাক্টরটিকে (destructor) কল (call) করবে। যদি এটি virtual না হয়, তবে এটি মূলত শুধুমাত্র ওই বেস ক্লাস ডেস্ট্রাক্টরটিকেই (base class destructor) কল করে, আর পুরো ডেরাইভড ক্লাসের (derived class) ক্লিনআপগুলোকে (cleanup) মূলত এড়িয়ে বা স্কিপ (skipping) করে চলে যায়।
ভার্চুয়াল ডেস্ট্রাক্টর (Virtual Destructor) — যেকোনো মেমরি লিককে (Memory Leaks) প্রতিরোধ করা বা Preventing
virtual ~Base() = default; ব্যবহার করা।দ্য স্লাইসিং প্রব্লেম বা সমস্যা (The Slicing Problem)
এটি হলো এখানকার একেবারে শেষ সমস্যা। আপনি যখন কোনো একটি ডেরাইভড অবজেক্টকে (derived object) বাই ভ্যালুর (by value) সাহায্যে কোনো একটি বেস অবজেক্টে (base object) অ্যাসাইন (assign) করেন, তখন এর ডেরাইভড অংশগুলো স্লাইস বা কাটা (sliced off) যায়। এক্ষেত্রে শুধু তার ওই বেস বা মূল অংশটুকুই (base portion) অবশিষ্ট (remains) থাকে। আর এটিকেই মূলত অবজেক্ট স্লাইসিং (object slicing) বলা হয়, এবং এটি অনেক বেশি সূক্ষ্ম একটি বাগ বা সমস্যা (sneaky bug)।
অবজেক্ট স্লাইসিং (Object Slicing) — এক ধরনের সূক্ষ্ম বাগ (Subtle Bug)
মূল কথাগুলো (Key Takeaways)
- যেকোনো বেস ক্লাস মেথডের (base class methods) ক্ষেত্রে
virtualব্যবহার করুন, যাতে করে এর ডেরাইভড ক্লাসগুলো (derived classes) তাকে ওভাররাইড (override) করতে সক্ষম হয় - কম্পাইল-টাইমের সুরক্ষার (compile-time safety) জন্য এর ওভাররাইডগুলোকে (overrides) সবসময়
overrideকিওয়ার্ডের সাহায্যে মার্ক বা চিহ্নিত (mark) করে রাখুন - পিওর ভার্চুয়াল ফাংশনগুলো (pure virtual functions) যখন এদের জন্য কোনো সঠিক বা যৌক্তিক (sensible default) ডিফল্ট খুঁজে না পায়, শুধু তখনই
= 0ব্যবহার করুন - যদি আপনার কাছে ভার্চুয়াল ফাংশন (virtual functions) থেকে থাকে, তবে নিশ্চিত করুন যে এর ডেস্ট্রাক্টরটিও (destructor) ভার্চুয়াল (virtual) কি না
- অবজেক্ট স্লাইসিং (object slicing) এড়াতে সবসময় পয়েন্টার (pointers) বা রেফারেন্সগুলো (references) (ভ্যালু টাইপ নয়) ব্যবহার করুন
- পলিমরফিজম অবজেক্টগুলো (polymorphic objects) ওয়েনিং বা নিজে ধারণ (owning) করার জন্য সবসময়
unique_ptr<Base>-টিকে অগ্রাধিকার বা Prefer দিন