সরাসরি পর্বে যান
পর্ব ২৬ · ৫৫-এর মধ্যে · মডিউল ৪

abstract class ও interface — কাজের চুক্তি লেখা

Abstract Classes and Interfaces — Writing Down the Contract

Taught in Bangla, this lesson covers abstract classes and interfaces in C++: interfaces as contracts, pure virtual functions, depending on interfaces instead of concrete classes, swapping in fakes for testing, implementing multiple interfaces, pure virtual destructors and NVI, and compile-time interfaces with concepts.

এই পর্বে যা শিখবেন

  • pure virtual function কী, আর কোন class কেন abstract হয়ে যায়
  • C++-এ একটি পরিষ্কার interface কীভাবে লেখেন — আর virtual destructor কেন বাধ্যতামূলক
  • “interface-এর ওপর প্রোগ্রাম করা”: constructor দিয়ে dependency injection, আর Fake দিয়ে টেস্ট
  • একটি class-এ একাধিক interface — multiple inheritance-এর নিরাপদ ব্যবহার
  • pure virtual destructor, NVI রীতি, আর concepts ও type erasure-এর সাথে তুলনা — কখন কোনটি

১ · interface মানে চুক্তি — “কী করতে হবে”, “কীভাবে” নয়

আগের পর্বে আপনি দেখেছেন, একটি base class-এর virtual function-কে derived class নিজের মতো করে override করতে পারে, আর base-এর pointer বা reference দিয়ে ডাকলেও চলে আসল object-এর ফাংশন। এই পর্বে প্রশ্নটা একটু উল্টো দিক থেকে: base class যদি নিজে কাজটা আদৌ না জানে?

ধরুন একটি অনলাইন দোকান অর্ডার নিশ্চিত হলে গ্রাহককে খবর পাঠায়। আজ পাঠায় SMS-এ। কাল ব্যবসায়ী বললেন, “ইমেইলেও পাঠান।” পরশু এল WhatsApp. আর টেস্ট চালানোর সময় তো আসল SMS পাঠানোই যাবে না — প্রতিবার টাকা কাটবে। অর্ডারের কোডের কাছে আসলে একটি কথাই জরুরি: “যে-ই হোক, তাকে একটি বার্তা পাঠাতে পারতে হবে।” কীভাবে পাঠাবে — মোবাইল অপারেটরের সার্ভারে, ইমেইল সার্ভারে, নাকি শুধু খাতায় লিখে রাখবে — সেটা অর্ডারের মাথাব্যথা নয়।

দেয়ালের বিদ্যুতের সকেটের কথা ভাবুন। সকেট একটি মান (standard) ঠিক করে দেয়: পিন কয়টি, কত দূরে, কত ভোল্ট। সকেট জানে না আপনি ফ্যান লাগাবেন, ল্যাপটপের চার্জার, নাকি রাইস কুকার। আবার ফ্যানের কারখানাও জানে না কোন বাড়ির কোন দেয়ালে লাগানো হবে। দুই পক্ষ শুধু মানটা মানে — তাতেই যেকোনো যন্ত্র যেকোনো সকেটে চলে। সকেটের মানটাই interface. দেয়ালের ভেতরের তার কেমন, ফ্যানের মোটর কেমন — কেউ কারও খবর রাখে না, রাখার দরকারও নেই।

পর্ব ২-এ আপনি শিখেছিলেন, interface হলো একটি object বাইরের দুনিয়াকে যে কাজগুলোর প্রতিশ্রুতি দেয়। এতদিন সেটা ছিল শুধু একটি ধারণা — একটি class-এর public ফাংশনগুলো। আজ সেই প্রতিশ্রুতিকে আলাদা করে লিখে ফেলবেন, একটি নিজস্ব টাইপ হিসেবে, যার ভেতরে কোনো কাজের বিবরণ নেই — আছে শুধু চুক্তির শর্ত। এটাই abstraction-এর সবচেয়ে বিশুদ্ধ রূপ।

চুক্তি (interface)

“send(msg) নামে একটি কাজ থাকবে, একটি লেখা নেবে।” ব্যস। কোনো কোড নেই, কোনো ডেটা নেই।

Notifier

যারা চুক্তি মানে (implementation)

SMS পাঠানোর class, ইমেইল পাঠানোর class, টেস্টের Fake class — প্রত্যেকে send নিজের মতো করে লেখে।

SmsNotifier · EmailNotifier · FakeNotifier

যারা চুক্তি ব্যবহার করে (client)

অর্ডারের কোড শুধু চুক্তিটা চেনে। কে আসলে পাঠাচ্ছে, জানে না — আর জানে না বলেই কখনো বদলাতে হয় না।

OrderService
মূল ধারণা

interface দুই পক্ষকে আলাদা করে দেয়: যে কাজ চায় আর যে কাজ করে. দুজনেই শুধু চুক্তির ওপর নির্ভর করে, একে অপরের ওপর নয়। ফলে একপক্ষ বদলালে অন্যপক্ষে হাত দিতে হয় না — এটাই কম coupling-এর আসল মানে।

২ · pure virtual function আর abstract class

পর্ব ২৫-এর virtual function-এর একটি “ডিফল্ট” শরীর (body) থাকে — derived class override না করলে সেটাই চলে। কিন্তু Notifier-এর জন্য কোনো যুক্তিসঙ্গত ডিফল্ট নেই। “সাধারণভাবে বার্তা পাঠাও” — কোথায়? কীভাবে? এমন ক্ষেত্রে C++ বলতে দেয়: “এই ফাংশন থাকবে, কিন্তু এর শরীর আমি দিচ্ছি না — যে আমাকে উত্তরাধিকার নেবে, সে দেবে।” লেখার উপায় হলো declaration-এর শেষে = 0:

struct Notifier {
    virtual ~Notifier() = default;                    // পর্ব ২৫-এর নিয়ম: base-এ virtual destructor
    virtual void send(const std::string& msg) = 0;   // pure virtual — শরীর নেই, শুধু শর্ত
};

= 0 দেওয়া ফাংশনকে বলে pure virtual function. আর যে class-এ অন্তত একটি pure virtual function আছে (নিজে ঘোষণা করেছে, বা উত্তরাধিকার সূত্রে পেয়ে override করেনি), সেটি একটি abstract class. এর উল্টো, যে class-এর সব virtual function-এর শরীর আছে আর যার object বানানো যায়, তাকে বলে concrete class.

abstract class-এর নিয়ম একটাই, আর সেটা কঠোর: এর object বানানো যায় না। যুক্তিটা সহজ। Notifier n; লিখলে তারপর n.send("hi") ডাকলে কী চলবে? কোনো শরীর তো নেই! তাই compiler প্রশ্নটা উঠতেই দেয় না — object বানানোর চেষ্টাতেই error. কিন্তু pointer বা reference রাখা পুরোপুরি বৈধ, কারণ সেগুলো সবসময় কোনো concrete derived object-কেই দেখাবে। ধাপে ধাপে দেখুন compiler কোথায় রাজি, কোথায় নয়:

1struct Notifier {2    virtual ~Notifier() = default;3    virtual void send(const std::string& m) = 0;4};5struct SmsNotifier : Notifier { void send(const std::string& m) override; };6struct HalfNotifier : Notifier { };        // send লেখেনি7Notifier n;                                // ?8SmsNotifier sms;  Notifier& ref = sms;     // ?9HalfNotifier h;                            // ?10auto p = std::make_unique<Notifier>();      // ?
কোন টাইপ কেমন?
Notifier
SmsNotifier
HalfNotifier
  1. চুক্তি লেখা হলো। Notifier-এ একটি pure virtual send. শুধু class লেখায় কোনো সমস্যা নেই — abstract class সম্পূর্ণ বৈধ টাইপ। সমস্যা হবে শুধু কেউ এর object বানাতে চাইলে।
  2. সরাসরি object — error. Notifier n; লিখতেই compiler থামায়, আর বলেও দেয় কেন: কোন কোন ফাংশন এখনো pure. GCC-র এই “note” লাইনগুলো খুব কাজের — বড় interface-এ কোন ফাংশন override করতে ভুলে গেছেন, তা এখান থেকেই জানবেন।
  3. concrete derived — ঠিক আছে। SmsNotifier send-এর শরীর দিয়েছে, তাই সে concrete; তার object হয়। আর সেই object-কে Notifier& দিয়ে দেখা বৈধ — interface-এর আসল ব্যবহার এটাই।
  4. override না করলে derived-ও abstract. HalfNotifier উত্তরাধিকার নিয়েছে কিন্তু send লেখেনি। ফলে pure function-টি তার কাছেও pure রয়ে গেল, সে-ও abstract. শিক্ষা: “চুক্তিতে সই” করলে সব শর্ত পূরণ না হওয়া পর্যন্ত object বানাতে দেওয়া হবে না।
  5. heap-এও একই নিয়ম। new Notifier বা std::make_unique<Notifier>()-ও error. ঠিক লেখা হলো std::unique_ptr<Notifier> p = std::make_unique<SmsNotifier>(); — টাইপ interface-এর, object concrete-এর।

এই error-টা বিরক্তিকর মনে হতে পারে, কিন্তু আসলে এটা একটি নিরাপত্তা-জাল। চুক্তির একটি শর্ত পূরণ না করেই কেউ যদি object বানাতে পারত, তাহলে প্রোগ্রাম চলার সময় একদিন একটি “শরীরহীন” ফাংশন ডাকা পড়ত। C++ সেই সম্ভাবনাকে compile-এর সময়েই শেষ করে দেয়।

abstract মানে “খালি” নয়। একটি abstract class-এ ডেটা member, constructor, সাধারণ (non-virtual) ফাংশন আর শরীরসহ virtual function — সবই থাকতে পারে। শুধু একটি pure virtual থাকলেই সে abstract. যেমন একটি Shape class নিজে রং আর নাম রাখতে পারে, অথচ area() pure রেখে দিতে পারে — কারণ “সাধারণ আকৃতি”-র ক্ষেত্রফল হয় না। এমন class-কে interface বলা ঠিক নয়; এটা “আংশিক তৈরি base class”. পরের অংশে দেখবেন, interface আরও কড়া একটি রূপ।

৩ · C++-এ interface — keyword ছাড়াই

Java বা C#-এ interface নামে একটি আলাদা keyword আছে। C++-এ নেই। C++-এ interface একটি রীতি — একটি abstract class, যা কয়েকটি কড়া শর্ত মানে:

C++ Core Guidelines-এও এই কথা আছে (C.121): কোনো base class-কে interface হিসেবে ব্যবহার করলে তাকে “pure abstract class” বানান। অনেকে class-এর বদলে struct লেখেন, যাতে সবকিছু ডিফল্ট public থাকে — দুটোই চলে, পার্থক্য শুধু ডিফল্ট access specifier-এ।

virtual destructor কেন বাধ্যতামূলক

interface-এর পুরো উদ্দেশ্য হলো concrete object-কে interface-এর টাইপ দিয়ে ধরে রাখা — আর অনেক সময় তার মালিকও হওয়া: std::unique_ptr<Notifier> n = std::make_unique<SmsNotifier>();. এখন n ধ্বংস হলে সে Notifier* দিয়ে delete করবে। Notifier-এর destructor virtual না হলে C++ শুধু Notifier অংশের destructor চালানোর চেষ্টা করবে, SmsNotifier-এর নয় — আর standard অনুযায়ী এটা undefined behavior. SmsNotifier-এর ভেতরে যদি একটি নেটওয়ার্ক সংযোগ বা বড় buffer থাকে, সেটা কখনো ফেরত যাবে না, বা আরও খারাপ কিছু ঘটবে। তাই interface-এ virtual destructor লেখা কোনো ঐচ্ছিক ভদ্রতা নয়, নিয়ম। সুখবর: g++-এর -Wall চালু থাকলে নিজের লেখা এমন delete-এ সাধারণত -Wdelete-non-virtual-dtor warning আসে — কিন্তু সব ক্ষেত্রে warning-এর ভরসায় থাকবেন না।

pure virtual function আর abstract class C++-এ আসে ১৯৮৯ সালে, Cfront-এর Release 2.0-তে — multiple inheritance-ও একই সাথে। অদ্ভুত দেখতে = 0 লেখার পদ্ধতি নিয়ে Bjarne Stroustrup পরে তাঁর “The Design and Evolution of C++” বইয়ে লিখেছেন: pure বা abstract নামে নতুন keyword আনার কথা তিনি ভেবেছিলেন, কিন্তু তখন নতুন keyword গ্রহণযোগ্য হওয়ার সম্ভাবনা দেখেননি। তাই এমন একটি লেখা বেছে নেন, যা পুরোনো কোড ভাঙে না। একই মানসিকতায় আলাদা interface keyword-ও কখনো আসেনি — abstract class দিয়েই সব কাজ হয় বলে।

C জানলে

C-তে class বা virtual নেই, কিন্তু interface-এর ধারণা আছে: একটি struct, যার member-রা function pointer. Linux kernel-এর struct file_operations ঠিক এমন — প্রতিটি driver read, write ইত্যাদির নিজের ফাংশন বসিয়ে দেয়, আর kernel শুধু struct-টা চেনে। C++-এর virtual function আর vtable আসলে এই হাতে-লেখা টেবিলটাই compiler-কে দিয়ে বানিয়ে নেয়, সাথে টাইপ-যাচাই। বিস্তারিত দেখুন C কোর্সের function pointer ও callback পর্বে।

৪ · interface-এর ওপর নির্ভর করা — OrderService জানে না কে বার্তা পাঠায়

এবার চুক্তিটা কাজে লাগানো যাক। OrderService নামে একটি class অর্ডার নেয় আর গ্রাহককে খবর দেয়। সে নিজে কোনো Notifier বানায় না; বরং constructor-এ বাইরে থেকে একটি Notifier& নেয়। যে জিনিসের ওপর একটি class নির্ভর করে (এখানে Notifier), তাকে বাইরে থেকে ঢুকিয়ে দেওয়ার এই কৌশলের নাম dependency injection — শুনতে ভারী, কাজে সরল: “যা লাগবে, constructor-এ চেয়ে নিন।”

#include <print>
#include <string>
struct Notifier {                                  // interface: only the contract
    virtual ~Notifier() = default;
    virtual void send(const std::string& msg) = 0;
};
struct SmsNotifier : Notifier {
    void send(const std::string& msg) override { std::println("[SMS] {}", msg); }
};
struct FakeNotifier : Notifier {                   // for tests: remembers, sends nothing
    std::string last;  void send(const std::string& msg) override { last = msg; }
};
struct OrderService {
    explicit OrderService(Notifier& n) : notifier(n) {}
    void place(int id) { notifier.send("Order " + std::to_string(id) + " confirmed"); }
    Notifier& notifier;                            // which one? it does not know
};
int main() {
    SmsNotifier sms;   OrderService live(sms);   live.place(101);
    FakeNotifier fake; OrderService test(fake);  test.place(202);
    std::println("fake recorded: {}", fake.last);
}

আউটপুট দুই লাইন: [SMS] Order 101 confirmed আর fake recorded: Order 202 confirmed. খেয়াল করুন, OrderService-এর কোড একবারই লেখা, একটি অক্ষরও বদলায়নি — অথচ প্রথমবার বার্তা গেল “SMS”-এ, দ্বিতীয়বার কোথাও গেল না, শুধু Fake-এর খাতায় লেখা হলো। কোনটি চলবে, ঠিক হয়েছে main-এ, object বানানোর মুহূর্তে। OrderService-এর ভেতরে কোথাও SmsNotifier নামটিই নেই — এটাই উদ্দেশ্য। দ্বিতীয় অংশটা একটি ছোট unit test-এর বীজ: “অর্ডার দিলে ঠিক বার্তাটা পাঠানোর চেষ্টা হয় কি?” — কোনো ফোন বা টাকা ছাড়াই যাচাই হলো।

live.place(101) ডাকলে ভেতরে ঠিক কী ঘটে, ধাপে ধাপে দেখুন। পর্ব ২৫-এর vtable-এর গল্পটা এখানে একটি বাস্তব কাজে লাগছে:

  1. OrderServiceplace(101)অর্ডারের কাজ
  2. চুক্তিNotifier&শুধু send() চেনে
  3. vptr → vtableslot: sendচলার সময় খোঁজা
  4. SmsNotifiersend(msg)আসল কাজ
live.place(101);OrderService-এর ভেতরে
notifier.send("Order 101 confirmed");
// OrderService.cpp compile হওয়ার সময় compiler শুধু Notifier-এর
// ঘোষণা দেখেছে। SmsNotifier নামে কিছু আছে কি না, সে জানেই না।
Notifier& notifierreference — আসলে একটি concrete object-কে দেখছে
notifier  ──>  [ SmsNotifier object ]
                 vptr ──> ?
// টাইপ লেখা Notifier, কিন্তু মেমরিতে বসে আছে SmsNotifier
vtable lookupপর্ব ২৫-এর যন্ত্র
[ SmsNotifier object ]
   vptr ──>  SmsNotifier-এর vtable
               [0] ~SmsNotifier
               [1] send  ──>  SmsNotifier::send
// (vtable-এর বিন্যাস compiler-এর নিজস্ব; ছবিটা ধারণা দেওয়ার জন্য)
SmsNotifier::send(msg)আসল কাজ হলো
[SMS] Order 101 confirmed
// Fake দিলে একই slot দেখাত FakeNotifier::send-কে —
// OrderService-এর কোড একই, শুধু object আলাদা।
  1. ক্লায়েন্ট ডাকে। place চুক্তির ভাষায় কথা বলে: “বার্তা পাঠাও।” OrderService যখন compile হয়, তখন compiler-এর কাছে শুধু Notifier-এর ঘোষণা — কোনো concrete class-এর খবর নেই। তাই কাল নতুন একটি Notifier এলে OrderService নতুন করে compile করারও দরকার পড়ে না।
  2. reference আসলে একটি concrete object দেখে। abstract টাইপের object হয় না, তাই এই reference-এর পেছনে সবসময় কোনো derived object থাকবেই — এখানে sms. এই নিশ্চয়তা আগের অংশের compile error-গুলোই দিয়েছে।
  3. চলার সময় খোঁজা। send virtual, তাই কোন ফাংশন চলবে তা ঠিক হয় চলার সময়: object-এর ভেতরের vptr থেকে তার vtable, সেখান থেকে send-এর ঠিকানা। এই প্রক্রিয়ার নাম dynamic dispatch. খরচ সাধারণত দুটি মেমরি পড়া আর একটি পরোক্ষ ডাক — বেশিরভাগ কাজে নগণ্য।
  4. আসল কাজ। SmsNotifier-এর send চলল। একই কোড-পথ দিয়ে Fake-ও চলত, ইমেইলও চলত। OrderService কখনো জানতে চায়নি অন্য পাশে কে আছে — শুধু বলেছে “কাজটা করো।” এভাবেই একটি polymorphism নকশা একটি বড় সিস্টেমের অংশগুলোকে আলাদা রাখে।

reference, নাকি unique_ptr — কে মালিক?

উপরের কোডে OrderService একটি Notifier& রাখে, অর্থাৎ সে মালিক নয় — Notifier বেঁচে থাকে main-এ, আর তাকে OrderService-এর চেয়ে বেশি দিন বাঁচতে হবে (পর্ব ১৮-এর lifetime-এর নিয়ম)। যদি চান Notifier-টি OrderService-এর নিজের হোক, তাহলে std::unique_ptr নিন:

class OrderService {
    std::unique_ptr<Notifier> notifier;            // মালিক — সাথে জন্মায়, সাথে মরে
public:
    explicit OrderService(std::unique_ptr<Notifier> n) : notifier(std::move(n)) {}
};
OrderService svc(std::make_unique<SmsNotifier>());   // ডাকার লাইনেই বেছে নেওয়া

এখানেই virtual destructor-এর দাম স্পষ্ট: svc ধ্বংস হলে unique_ptr Notifier* দিয়ে delete করবে, আর শুধু virtual destructor থাকলেই ঠিকঠাক ~SmsNotifier চলবে। নিয়মটা মনে রাখুন: শুধু ব্যবহার করলে Notifier&, মালিক হলে std::unique_ptr<Notifier>.

৫ · বদলে দেওয়া যায় এমন অংশ — Storage, Fake আর টেস্ট

interface-এর সবচেয়ে বড় লাভটা চোখে পড়ে টেস্ট লেখার সময়। ধরুন একটি পরীক্ষার রেজাল্ট-অ্যাপ ছাত্রছাত্রীদের নম্বর জমা রাখে। আসল প্রোগ্রামে নম্বর যায় ডিস্কের ফাইলে। কিন্তু টেস্টে প্রতিবার ফাইল খোলা ধীর, আর এক টেস্টের রেখে যাওয়া ফাইল অন্য টেস্টকে গুলিয়ে দিতে পারে। সমাধান — জমা রাখার কাজটাকে একটি চুক্তি বানান:

struct Storage {
    virtual ~Storage() = default;
    virtual void save(const std::string& key, const std::string& value) = 0;
    virtual std::optional<std::string> load(const std::string& key) const = 0;
};
class FileStorage : public Storage { /* ডিস্কের ফাইলে লেখে-পড়ে (পর্ব ৪৪) */ };
class MemoryStorage : public Storage {                  // টেস্টের জন্য: শুধু মেমরিতে
    std::unordered_map<std::string, std::string> data;
    /* save আর load এই map-এ */
};
class ResultService {
    Storage& store;
public:
    explicit ResultService(Storage& s) : store(s) {}
    void record(const std::string& roll, int marks);   // শুধু Storage চেনে
};

যে class টেস্টে আসল জিনিসের জায়গা নেয়, তার সাধারণ নাম test double — সিনেমায় ঝুঁকির দৃশ্যে নায়কের জায়গায় যেমন “ডাবল” নামেন। এর কয়েকটি রকম আছে: Fake সত্যিই কাজ করে কিন্তু সহজ পথে (MemoryStorage — আসল ডেটাবেস নয়, একটি map); Stub শুধু ঠিক করা উত্তর ফেরত দেয়; Mock মনে রাখে কে কীভাবে ডাকল, যাতে টেস্ট পরে যাচাই করতে পারে। উপরের FakeNotifier আসলে Fake আর Mock-এর মাঝামাঝি। টেস্টিং-এর পুরো গল্প পর্ব ৪৮-এ — এখানে শুধু এটুকু বুঝুন: interface না থাকলে test double বসানোর কোনো জায়গাই থাকে না।

একই OrderService-কে তিন জায়গায় তিনভাবে জোড়া লাগানো দেখুন। প্রতিবার শুধু “জোড়া লাগানোর জায়গা” (সাধারণত main বা টেস্ট ফাইল) বদলায়:

SmsNotifierআসল দোকান
EmailNotifierনতুন চাহিদা
FakeNotifierটেস্ট
MemoryStorageটেস্ট
Notifier · Storageচুক্তি — কখনো বদলায় না
OrderService · ResultServiceএকবার লেখা, একবার compile
main.cpp — আসল দোকানবার্তা যায় SMS-এ
SmsNotifier sms;
OrderService shop(sms);
shop.place(101);        // [SMS] Order 101 confirmed
main.cpp — নতুন চাহিদা: ইমেইলশুধু একটি নতুন class আর একটি লাইন
EmailNotifier mail;     // নতুন ফাইলে নতুন class: send()-এ ইমেইল
OrderService shop(mail);
shop.place(102);        // OrderService-এ হাত দিতে হয়নি
order_test.cppকোনো SMS যায় না, টাকা কাটে না
FakeNotifier fake;
OrderService shop(fake);
shop.place(7);
check(fake.last == "Order 7 confirmed");   // মিলিসেকেন্ডে উত্তর
result_test.cppডিস্ক নেই — প্রতিটি টেস্ট পরিষ্কার শুরু
MemoryStorage mem;
ResultService rs(mem);
rs.record("roll-15", 78);
check(mem.load("roll-15") == "78");
হিসাবকী বদলাল, কী বদলাল না
নতুন class যোগ হলো          : 3টি (Email, Fake, Memory)
OrderService.cpp-এ পরিবর্তন   : 0 লাইন
ResultService.cpp-এ পরিবর্তন  : 0 লাইন
নতুন করে compile             : শুধু নতুন ফাইল আর জোড়ার জায়গা
  1. আসল দোকান। main-এ SmsNotifier বানিয়ে OrderService-কে দেওয়া হলো। “কোন অংশ কোনটির সাথে জুড়বে” — এই সিদ্ধান্ত একটিমাত্র জায়গায়, প্রোগ্রামের একেবারে বাইরের দিকে।
  2. নতুন চাহিদা — ইমেইল। একটি নতুন class EmailNotifier, যে Notifier-এর চুক্তি মানে। পুরোনো কোনো ফাইল খুলতে হয়নি। “পুরোনো কোড না ছুঁয়ে নতুন আচরণ যোগ” — পর্ব ২৭-এ এর নাম শুনবেন Open/Closed নীতি।
  3. টেস্টে Fake. টেস্ট ফাইলে FakeNotifier দেওয়া হলো। OrderService জানেই না যে সে টেস্টের মধ্যে আছে — সে আগের মতোই send ডাকছে। তাই যা পরীক্ষা হচ্ছে, তা আসল কোডই।
  4. Storage-এও একই কৌশল। রেজাল্ট-অ্যাপের টেস্টে FileStorage-এর বদলে MemoryStorage. ডিস্ক নেই, পুরোনো ফাইল নেই, প্রতিটি টেস্ট নতুন খালি map দিয়ে শুরু — তাই ফল প্রতিবার একই।
  5. হিসাব মেলান। তিনটি নতুন implementation এল, অথচ দুটি service-এর এক লাইনও বদলায়নি। বড় দলে এর মানে: যিনি OrderService লেখেন আর যিনি ইমেইল লেখেন, তাঁরা একে অপরের কোডে না ঢুকেই কাজ করতে পারেন — শুধু চুক্তিটা মেনে।

সবকিছুর জন্য interface নয়। interface বানানোর যুক্তি একটাই: একাধিক implementation আছে, বা নিশ্চিতভাবে লাগবে — যেমন আসল আর টেস্টের, বা কয়েকটি পেমেন্ট গেটওয়ে। শুধু একটি implementation থাকলে আর বদলানোর কোনো বাস্তব কারণ না থাকলে interface শুধু একটি বাড়তি স্তর: বেশি ফাইল, বেশি নাম, “আসল কোড কোথায়?” খুঁজতে বেশি সময়। এটাকে বলে over-engineering. পরে দরকার হলে interface বের করে আনা (extract) কঠিন কিছু নয়।

৬ · একটি class, দুটি interface — multiple inheritance-এর নিরাপদ রূপ

C++-এ একটি class একাধিক base class থেকে উত্তরাধিকার নিতে পারে — একে বলে multiple inheritance. পর্ব ২৪-এ দেখেছেন, ডেটা-ভরা দুটি base একসাথে নিলে ঝামেলা হয়: একই নামের দুটি member, “diamond” আকারের উত্তরাধিকারে একই base-এর দুটি কপি। কিন্তু base-গুলো যদি ডেটাহীন interface হয়, তাহলে এই ঝামেলার প্রায় সবই উধাও — কারণ ভাগাভাগি করার মতো কোনো অবস্থাই নেই, আছে শুধু প্রতিশ্রুতি। তাই বাস্তবে multiple inheritance-এর সবচেয়ে প্রচলিত আর নিরাপদ ব্যবহার এটাই: একটি concrete class, কয়েকটি interface.

উদাহরণ: একটি ই-কমার্স সাইট বিভিন্ন পেমেন্ট গেটওয়ে “plug-in” হিসেবে লাগায়। সব গেটওয়ে টাকা নিতে পারে, কিন্তু সবাই টাকা ফেরত (refund) দিতে পারে না। তাই দুটি ছোট, আলাদা চুক্তি — একটি বড় চুক্তি নয়:

#include <print>
struct PaymentGateway {                            // interface 1: take money
    virtual ~PaymentGateway() = default;
    virtual void pay(int taka) = 0;
};
struct Refundable {                                // interface 2: give money back
    virtual ~Refundable() = default;
    virtual void refund(int taka) = 0;
};
struct WalletGateway : PaymentGateway, Refundable {   // signs both contracts
    void pay(int taka) override    { std::println("wallet: paid {}", taka); }
    void refund(int taka) override { std::println("wallet: refunded {}", taka); }
};
void checkout(PaymentGateway& g) { g.pay(500); }     // knows only pay()
void cancel(Refundable& r)       { r.refund(200); }  // knows only refund()
int main() {
    WalletGateway w;
    checkout(w);  cancel(w);
    PaymentGateway& p = w;  Refundable& r = w;
    std::println("same address? {}", static_cast<void*>(&p) == static_cast<void*>(&r));
}

আউটপুট: wallet: paid 500, wallet: refunded 200, তারপর same address? false. প্রথম দুই লাইন প্রত্যাশিত — একটি object দুটি আলাদা চুক্তি মানছে, আর প্রতিটি ফাংশন শুধু নিজের দরকারি চুক্তিটুকু চেনে। checkout refund-এর কথা জানেই না। তৃতীয় লাইনটা চমক: একই object w, অথচ দুই interface-এর reference দুটি আলাদা ঠিকানা দেখায়। কারণ object-এর ভেতরে দুটি আলাদা base-অংশ (subobject) আছে, প্রত্যেকটির নিজের vptr — আর C++ নিয়ম অনুযায়ী এমন দুটি অংশের ঠিকানা এক হতে পারে না। struct-এ উত্তরাধিকার ডিফল্টভাবে public, তাই এখানে আলাদা করে public লিখতে হয়নি।

ঠিকানা আলাদা কেন, আর তাতে কিছু ভাঙে না কেন — মেমরির ভেতরটা ধাপে ধাপে দেখুন:

WalletGateway w;একটি object, ভেতরে দুটি base-অংশ
w @1000 ┌──────────────────────────────┐
        │ PaymentGateway অংশ: vptr → A │  @1000
        │ Refundable অংশ    : vptr → B │  @1008
        └──────────────────────────────┘
(ঠিকানা উদাহরণমাত্র; বিন্যাস compiler-এর ABI ঠিক করে)
checkout(w);PaymentGateway& পায় প্রথম অংশ
g  ──>  @1000  PaymentGateway অংশ
          vptr → A:  [pay → WalletGateway::pay]
checkout জানে শুধু pay — refund-এর অস্তিত্বই তার কাছে নেই
cancel(w);Refundable& পায় দ্বিতীয় অংশ
r  ──>  @1008  Refundable অংশ
          vptr → B:  [refund → WalletGateway::refund]
compiler নিজে থেকেই ঠিকানা 8 বাইট সরিয়ে দিয়েছে (this adjustment)
std::unique_ptr<Refundable> u = std::make_unique<WalletGateway>();দ্বিতীয় অংশ দিয়ে মালিকানা
u ধ্বংস হলে: delete (Refundable*)@1008
   → virtual ~Refundable → আসলে ~WalletGateway
   → ঠিকানা আবার @1000-এ সরিয়ে পুরো object মোছা হয়
virtual destructor না থাকলে: ভুল ঠিকানায় delete — undefined behavior
  1. একটি object, দুটি অংশ। WalletGateway-এর ভেতরে প্রতিটি interface-এর জন্য একটি করে base-অংশ, প্রতিটিতে নিজের vptr. আকারে দুটি pointer — interface ডেটাহীন হলেও vptr-এর জায়গা লাগে।
  2. প্রথম চুক্তি দিয়ে দেখা। PaymentGateway& দেখায় প্রথম অংশটিকে। সাধারণ বাস্তবায়নে এর ঠিকানা object-এর শুরুর ঠিকানাই। এই অংশের vtable-এ শুধু PaymentGateway-এর ফাংশনগুলো।
  3. দ্বিতীয় চুক্তি দিয়ে দেখা। Refundable&-এর জন্য compiler নিজে ঠিকানাটা দ্বিতীয় অংশে সরিয়ে দেয়। তাই ঠিকানা আলাদা — আর সেটাই সঠিক, কারণ Refundable-এর vptr ওখানেই বসে। আপনাকে কিছু করতে হয় না; compiler জানে কতটা সরাতে হবে।
  4. ধ্বংসের সময়ও নিরাপদ — শর্তসহ। দ্বিতীয় interface-এর pointer দিয়েও পুরো object ঠিকভাবে মোছা যায়, কারণ virtual destructor ডাক পৌঁছে দেয় আসল destructor-এ, আর সে ঠিকানা আবার শুরুতে ফিরিয়ে নেয়। এই কারণেই প্রতিটি interface-এ virtual destructor — একটিতেও বাদ দেবেন না।

এই নকশার আরেকটি নাম আছে, পর্ব ২৭-এ শুনবেন: Interface Segregation — “বড় এক চুক্তির চেয়ে ছোট কয়েকটি চুক্তি ভালো।” যে গেটওয়ে refund দেয় না, তাকে জোর করে একটি খালি refund() লিখতে হয় না, যা হয়তো চুপচাপ কিছুই করে না বা চলার সময় exception ছোড়ে।

৭ · তিনটি সূক্ষ্ম বিষয় — pure virtual destructor, NVI আর copy

pure virtual destructor — definition লাগবেই

কখনো একটি base class-কে abstract বানাতে চান, অথচ তার কোনো ফাংশনই pure করার মতো স্বাভাবিক নয়। তখন একটি কৌশল আছে: destructor-কেই pure করে দিন। কিন্তু মনে রাখুন — derived class-এর destructor শেষে সবসময় base-এর destructor ডাকে, তাই pure হলেও base-এর destructor-এর শরীর লিখতেই হবে, class-এর বাইরে:

struct Shape {
    virtual ~Shape() = 0;      // pure — তাই Shape abstract
    std::string color;
};
inline Shape::~Shape() {}      // তবু শরীর লাগবে; না দিলে link error

অদ্ভুত শোনালেও এটি বৈধ: C++-এ একটি pure virtual function-এর definition থাকতে পারে। “pure” মানে শুধু “derived class-কে override করতেই হবে আর এই class-এর object হবে না” — “শরীর নেই” নয়। definition না দিলে compile ঠিকঠাক হয়ে যায়, কিন্তু linker undefined reference to 'Shape::~Shape()' জাতীয় error দেয়। (header-এ লিখলে inline লাগবে, পর্ব ১১-এর ODR-এর কারণে।) সাধারণ কোডে এটা কম লাগে — সাধারণ ফাংশন pure রাখাই বেশি স্বাভাবিক।

NVI — বাইরে non-virtual, ভেতরে virtual

কখনো চান প্রতিটি implementation-এর আগে-পরে একই কাজ হোক — যেমন প্রতিটি রিপোর্ট export-এর আগে log লেখা আর পরে সময় মাপা। public ফাংশন virtual হলে প্রতিটি derived class এই নিয়ম নিজে মানবে, এই ভরসা করতে হয়। বিকল্প হলো NVI (Non-Virtual Interface) রীতি, যেটা Herb Sutter ২০০১ সালের একটি প্রবন্ধে জনপ্রিয় করেন:

class Exporter {
public:
    virtual ~Exporter() = default;
    void exportReport(const Report& r) {     // non-virtual: নিয়ম সবার জন্য এক
        log("start");
        doExport(r);                         // আসল কাজ derived class-এর
        log("done");
    }
private:
    virtual void doExport(const Report& r) = 0;   // private, তবু override করা যায়
};

derived class private virtual function-ও override করতে পারে — C++-এ access আর override আলাদা বিষয়। standard library-তেই এর উদাহরণ আছে: locale-এর facet-গুলোতে public put() ভেতরে ডাকে virtual do_put()-কে। খেয়াল করুন, NVI মানলে class আর “বিশুদ্ধ interface” থাকে না, কারণ তার ভেতরে কাজের কোড আছে। দুটোই বৈধ নকশা; NVI বেছে নিন যখন চুক্তির সাথে কিছু বাধ্যতামূলক নিয়মও চাপাতে চান।

interface কপি করবেন না

interface সবসময় reference বা pointer দিয়ে ব্যবহার হয়, value হিসেবে নয়। কেউ যদি একটি concrete object-কে base টাইপের value-তে কপি করতে চায়, derived অংশ কেটে বাদ পড়ে — একে বলে object slicing (পর্ব ২৫-এ দেখেছেন)। pure abstract class-এর ক্ষেত্রে এমন value বানানোই যায় না বলে বড় ঝুঁকি কম, তবে abstract-নয় এমন polymorphic base-এ Core Guidelines (C.67) পরামর্শ দেয় public copy আর move বন্ধ রাখতে। পর্ব ২২-এর Rule of 0/3/5-এর সাথে এটা মিলিয়ে নিন।

পরীক্ষা ও ইন্টারভিউতে
  • abstract class-এর object বানানো যায়? না — compile error. তবে তার pointer আর reference রাখা যায়, আর তার constructor থাকতে পারে (derived object তৈরির সময় চলে)।
  • virtual আর pure virtual function-এর পার্থক্য? virtual-এর একটি ডিফল্ট শরীর থাকে, override ঐচ্ছিক; pure virtual (= 0) class-কে abstract করে, আর concrete হতে হলে derived class-কে override করতেই হবে।
  • derived class pure virtual override না করলে? সে-ও abstract থেকে যায়, তার object-ও বানানো যায় না।
  • C++-এ interface কীভাবে বানান? আলাদা keyword নেই; শুধু pure virtual function আর virtual destructor নিয়ে একটি ডেটাহীন class.
  • interface-এ virtual destructor কেন? base pointer দিয়ে derived object delete করলে virtual destructor ছাড়া আচরণ undefined; virtual থাকলে আসল destructor চলে।

৮ · compile-time interface — template, concepts আর type erasure

এতক্ষণ যা দেখলেন, তা চলার সময়ের (runtime) চুক্তি: কোন implementation চলবে, তা ঠিক হয় প্রোগ্রাম চলার সময়, vtable দিয়ে। C++-এ চুক্তি লেখার আরেকটি বড় পথ আছে — compile-এর সময়ে। সেখানে কোনো base class লাগে না, inheritance লাগে না; লাগে শুধু “এই টাইপে send() ডাকা যায় কি?” — এই প্রশ্নের উত্তর।

template <typename T>
concept CanSend = requires(T n, const std::string& m) { n.send(m); };   // C++20

template <CanSend N>
void placeOrder(N& n, int id) { n.send("Order " + std::to_string(id)); }
// যেকোনো টাইপ, যাতে send(string) আছে — Notifier থেকে উত্তরাধিকার না নিলেও

এখানে চুক্তিটির নাম concept (C++20 থেকে, পুরো গল্প পর্ব ৪০-এ), আর template প্রতিটি টাইপের জন্য আলাদা ফাংশন বানিয়ে নেয় (পর্ব ২৮)। কোন send চলবে, তা compiler আগেই জানে, তাই কোনো vtable লাগে না, আর compiler ফাংশনটি সরাসরি ভেতরে বসিয়ে (inline করে) দিতে পারে। একে বলে static polymorphism.

তৃতীয় পথটি মাঝামাঝি: type erasure. বাইরে থেকে একটি সাধারণ value-টাইপ, ভেতরে যেকোনো জিনিস — আর টাইপটা “মুছে” ফেলা হয়। সবচেয়ে পরিচিত উদাহরণ std::function (পর্ব ৩৪-এ বিস্তারিত):

class OrderService {
    std::function<void(const std::string&)> notify;      // যেকোনো "ডাকা যায়" জিনিস
public:
    explicit OrderService(std::function<void(const std::string&)> f) : notify(std::move(f)) {}
    void place(int id) { notify("Order " + std::to_string(id) + " confirmed"); }
};
OrderService svc([](const std::string& m) { std::println("[SMS] {}", m); });   // lambda-ই চুক্তি মানে

কোনো base class নেই, কোনো inheritance নেই — যেকোনো lambda বা ফাংশন, যার স্বাক্ষর মেলে, চলবে। চুক্তি যখন একটিমাত্র ফাংশন, তখন প্রায়ই এটাই সবচেয়ে হালকা সমাধান। ভেতরে std::function নিজেই একধরনের virtual ডাক ব্যবহার করে, তাই খরচ runtime interface-এর কাছাকাছি।

বিষয়abstract class (runtime)template + concept (compile-time)std::function / type erasure
কখন বাছা হয়চলার সময়compile-এর সময়চলার সময়
inheritance লাগে?হ্যাঁ, চুক্তিতে “সই” করতে হয়না — ফাংশন থাকলেই চলেনা
ডাকের খরচএকটি পরোক্ষ ডাক, সাধারণত inline হয় নাসরাসরি, inline হতে পারেপরোক্ষ ডাক, কখনো heap বরাদ্দ
এক vector-এ নানা টাইপ রাখাহ্যাঁ: vector<unique_ptr<Notifier>>না, প্রতিটি টাইপ আলাদাহ্যাঁ: vector<function<...>>
নতুন implementation-এ client আবার compile?না — DLL/plugin-ও চলেহ্যাঁ, কোড header-এ থাকেনা
error messageপরিষ্কারconcept থাকলে মোটামুটি পরিষ্কারপরিষ্কার
ভালো কাজেবড় চুক্তি, plugin, টেস্টে বদলদ্রুতগতির library, গাণিতিক কোডএকটি ফাংশনের চুক্তি, callback

সহজ নিয়ম: implementation চলার সময় বদলায়, বা আলাদা করে compile করা অংশ (plugin, DLL) থেকে আসে — abstract class. সবকিছু compile-এর সময়েই জানা আর প্রতিটি ন্যানোসেকেন্ড দামি — template আর concept. চুক্তি মাত্র একটি ফাংশনের — std::function. তিনটিই “interface-এর ওপর প্রোগ্রাম করা”; পার্থক্য শুধু চুক্তিটা কোথায় আর কখন যাচাই হয়।

৯ · AI-এর যুগে — interface-এর বাড়াবাড়ি আর ফাঁক

“Clean architecture” বা “enterprise” ধাঁচের প্রচুর কোড দেখে শেখা AI টুল প্রায়ই প্রতিটি class-এর জন্য একটি interface বানিয়ে ফেলে: IUserService আর UserServiceImpl, IConfig আর ConfigImpl — যদিও প্রতিটির implementation মাত্র একটি। একে বলা হয় “interface explosion”. উল্টো দিকেও ভুল হয়: virtual destructor বাদ পড়ে, বা interface-এর মধ্যেই কোনো নির্দিষ্ট প্রযুক্তির নাম ঢুকে যায়। এগুলো compile হয়, চলে — সমস্যা দেখা দেয় পরে, leak বা “বদলানো যায় না” হিসেবে। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
প্রতিটি class-এর জন্য IFoo + FooImplঅকারণ স্তর, দ্বিগুণ ফাইল, কোড খুঁজে পাওয়া কঠিনinterface শুধু যেখানে একাধিক implementation বা টেস্টে বদল দরকার
interface-এ virtual destructor নেইunique_ptr<Base> দিয়ে মুছলে undefined behavior, leakvirtual ~Notifier() = default;
virtual void sendViaTwilio(const TwilioConfig&, ...)চুক্তিতে নির্দিষ্ট প্রযুক্তি ফাঁস — অন্য implementation অসম্ভবsend(const std::string& msg); কনফিগ যায় concrete class-এর constructor-এ
client-এর ভেতরে dynamic_cast<SmsNotifier*>interface-এর পেছনে লুকিয়ে concrete টাইপে নির্ভরতা — বদলালেই ভাঙেযা লাগে, চুক্তিতে যোগ করুন, বা আলাদা interface দিন
বিশাল একটি interface, ২০টি ফাংশনপ্রতিটি implementation-কে অদরকারি ফাংশনও লিখতে হয়কাজ অনুযায়ী ছোট ছোট interface
service-এর ভেতরে new SmsNotifier বা make_unique<SmsNotifier>interface থাকলেও বদলানো যায় না, টেস্টে Fake বসানো যায় নাconstructor-এ বাইরে থেকে নিন (dependency injection)
override করা ফাংশনে override লেখা নেইস্বাক্ষরে ছোট ভুল হলে নিঃশব্দে নতুন ফাংশন — class abstract থেকে যায় বা ভুল ফাংশন চলেপ্রতিটিতে override

AI-কে interface-সহ নকশা করতে বলার সময় কারণ দেখাতে বাধ্য করুন — কোন interface কেন আছে, আর কোনটি নেই কেন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছোট অনলাইন দোকানের অর্ডার-প্রক্রিয়া লেখো: OrderService অর্ডার নেবে, গ্রাহককে বার্তা পাঠাবে (আসলে SMS, টেস্টে Fake), আর অর্ডার জমা রাখবে (আসলে ফাইল, টেস্টে মেমরি)। নিয়ম: ১. interface বানাবে শুধু সেখানে, যেখানে অন্তত দুটি implementation আছে — প্রতিটি interface-এর পাশে এক লাইনে কারণ লেখো, আর যে class-এর interface বানাওনি, সেটাও বলো। ২. প্রতিটি interface-এ শুধু pure virtual function আর virtual ~X() = default; কোনো ডেটা member নয়। ৩. interface-এর নাম ও ফাংশনে কোনো নির্দিষ্ট প্রযুক্তি বা কোম্পানির নাম থাকবে না; কনফিগ যাবে concrete class-এর constructor-এ। ৪. OrderService নিজে কোনো concrete class বানাবে না; সব নির্ভরতা constructor-এ নেবে (reference হলে কে মালিক, তা বলো)। ৫. প্রতিটি override-এ override keyword; কোথাও dynamic_cast নয়। ৬. শেষে একটি ছোট টেস্ট-ফাংশন দাও, যা Fake দিয়ে OrderService যাচাই করে।

AI-এর interface-নকশা যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Java আর C#-এ interface keyword আছে, C++-এ নেই কেন? এটা কি C++-এর ঘাটতি?

উত্তরটা লুকিয়ে আছে দুই ভাষার ইতিহাসে। C++-এ abstract class আর multiple inheritance একসাথে আসে ১৯৮৯ সালের Release 2.0-তে। Stroustrup-এর লক্ষ্য ছিল যতটা সম্ভব কম নতুন ধারণা দিয়ে যতটা সম্ভব বেশি নকশা সম্ভব করা। একটি abstract class দিয়ে দুই কাজই হয়: চাইলে সেটা “আংশিক তৈরি base” — কিছু কোড আর কিছু pure function; চাইলে “বিশুদ্ধ চুক্তি” — শুধু pure function. যেহেতু C++ আগে থেকেই একাধিক base class অনুমতি দিত, interface-এর জন্য আলাদা নিয়মের দরকার পড়েনি। নতুন keyword আনতেও তিনি সতর্ক ছিলেন — = 0 লেখার পদ্ধতিটাই তার প্রমাণ।

Java (১৯৯৫-৯৬) নকশা হয়েছিল উল্টো সিদ্ধান্ত থেকে: class-এর multiple inheritance পুরোপুরি নিষেধ, কারণ তার নকশাকারীরা C++-এর diamond-সমস্যা আর জটিলতা এড়াতে চেয়েছিলেন। কিন্তু একটি class-কে একাধিক “ভূমিকা” দেওয়ার দরকার তো থাকেই। তাই এল আলাদা একটি জিনিস, interface, যেখানে (তখন) কোনো কোড বা ডেটা রাখা যেত না, আর একটি class যতগুলো খুশি interface “implement” করতে পারত। অর্থাৎ Java-র interface keyword আসলে একটি নিষেধাজ্ঞার পরিপূরক। মজার ব্যাপার, ২০১৪ সালের Java 8-এ interface-এ “default method” — অর্থাৎ শরীরসহ ফাংশন — যোগ হয়, আর C# 8-ও পরে একই পথে যায়। দুই ভাষা ধীরে ধীরে সেই জায়গাতেই আসছে, যেখানে C++-এর abstract class শুরু থেকেই ছিল।

তাহলে C++-এ কী হারাল? একটি স্পষ্ট লেবেল। একটি class দেখে সঙ্গে সঙ্গে বলা যায় না, এটা বিশুদ্ধ চুক্তি নাকি আংশিক base; রীতি মেনে চলার দায়িত্ব প্রোগ্রামারের। এজন্যই Core Guidelines-এর মতো নির্দেশিকা (C.121: interface হলে pure abstract class) আর নামের রীতি (কোনো কোনো দলে I দিয়ে শুরু, যেমন INotifier) এসেছে। Microsoft-এর compiler-এ একটি নিজস্ব __interface extension আছে, যা এই নিয়ম জোর করে চালায় — কিন্তু সেটা standard C++ নয়।

মূল উপলব্ধি: C++ একটি সাধারণ যন্ত্র দিয়েছে আর রীতিটা আপনার হাতে ছেড়েছে; Java একটি নিষেধাজ্ঞা দিয়েছে আর তার পরিপূরক হিসেবে keyword. ঘাটতি নয়, ভিন্ন দর্শন — তবে রীতি মানার শৃঙ্খলা C++-এ আপনার নিজের।

প্র ০২Microsoft-এর COM কীভাবে C++-এর abstract class দিয়ে দশকের পর দশক টিকে থাকা “বাইনারি চুক্তি” বানিয়েছে? ABI স্থিতিশীলতার সাথে এর সম্পর্ক কী?

১৯৯৩ সালে Microsoft আনে COM (Component Object Model). সমস্যাটা ছিল: আলাদা কোম্পানি, আলাদা compiler, আলাদা বছর — এমন কোডের টুকরো (DLL) কীভাবে একে অপরের object ব্যবহার করবে? C++-এর class সরাসরি DLL-এর সীমানা পার করানো বিপজ্জনক, কারণ class-এর বিন্যাস, name mangling, exception — সবই compiler-ভেদে আলাদা হতে পারে (পর্ব ৩-এর ABI-এর আলোচনা মনে করুন)।

COM-এর সমাধান চতুর: চুক্তিটা হবে শুধু একটি vtable-এর বিন্যাস। একটি COM interface হলো pure virtual function-এর একটি তালিকা, নির্দিষ্ট ক্রমে। Windows-এ MSVC একটি ডেটাহীন abstract class-এর vtable ঠিক এই ক্রমে বানায়, তাই C++-এ সাধারণ abstract class লিখলেই COM interface হয়ে যায় — আবার C বা অন্য ভাষাও একই টেবিল হাতে বানিয়ে কথা বলতে পারে। সব interface শুরু হয় IUnknown দিয়ে, যার তিনটি ফাংশন: QueryInterface (“এই object কি অমুক চুক্তিও মানে?”), AddRef আর Release (reference গোনা — পর্ব ১৭-এর shared_ptr-এর আদিপুরুষ বলা যায়)। প্রতিটি interface-এর একটি স্থায়ী GUID থাকে।

এখান থেকে আসে COM-এর সবচেয়ে কঠোর নিয়ম: প্রকাশিত interface কখনো বদলানো যাবে না। একটি নতুন virtual function শেষে যোগ করলেও পুরোনো client হয়তো টিকে যায়, কিন্তু মাঝখানে যোগ করলে বা ক্রম বদলালে vtable-এর slot সরে যায় — পুরোনো DLL তখন ভুল ফাংশন ডাকবে, কোনো error ছাড়াই। তাই নতুন সুবিধা আনতে নতুন interface বানানো হয়: DirectX-এ IDXGIFactory, তারপর IDXGIFactory1, IDXGIFactory2… পুরোনো প্রোগ্রাম পুরোনো চুক্তি নিয়েই চলতে থাকে, নতুনরা QueryInterface দিয়ে নতুনটা চায়। এভাবেই Windows-এর অনেক পুরোনো প্রোগ্রাম আজও চলে।

শিক্ষাটা COM-এর বাইরেও খাটে। আপনার নিজের লাইব্রেরি যদি অন্যরা compile-করা অবস্থায় ব্যবহার করে (plugin, SDK), তাহলে public abstract class-এর virtual function-এর ক্রম, স্বাক্ষর আর সংখ্যা — সবই ABI-র অংশ। অথচ সাধারণ source-কোড প্রজেক্টে এগুলো ইচ্ছামতো বদলানো যায়, কারণ সব একসাথে আবার compile হয়।

মূল উপলব্ধি: interface শুধু source-কোডের চুক্তি নয়; সীমানা পার হলে সেটা বাইনারি চুক্তি। আর বাইনারি চুক্তি ভাঙলে compiler আপনাকে সাবধান করে না — তাই প্রকাশিত চুক্তি বদলাবেন না, নতুন চুক্তি যোগ করবেন।

প্র ০৩“Program to an interface, not an implementation” — Gang of Four-এর এই বাক্যের আসল মানে কী, আর কোথায় এটা ভুলভাবে বোঝা হয়?

বাক্যটি ১৯৯৪ সালের বিখ্যাত বই “Design Patterns: Elements of Reusable Object-Oriented Software” থেকে, যার চার লেখক — Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — “Gang of Four” নামে পরিচিত। বইয়ের শুরুর অধ্যায়ে দুটি মূলনীতি আছে: “Program to an interface, not an implementation” আর “Favor object composition over class inheritance.” বইয়ের প্রায় সব pattern এই দুটির ওপর দাঁড়ানো।

প্রথমটির মানে: ভ্যারিয়েবল, প্যারামিটার আর member-এর টাইপ হিসেবে এমন টাইপ ব্যবহার করুন, যা শুধু আচরণের চুক্তি বলে — নির্দিষ্ট বাস্তবায়ন নয়। তাহলে client কোড জানে না কোন object-এর সাথে কথা বলছে, শুধু জানে কী চাইতে পারে। এই পর্বের OrderService ঠিক তাই করে। লাভ দুটি: implementation বদলানো যায় client না ছুঁয়ে, আর client-কে আলাদা করে পরীক্ষা করা যায়। ধরুন একটি দোকান আজ একটি পেমেন্ট গেটওয়ে ব্যবহার করে, কাল কম ফি দেখে আরেকটিতে যেতে চায় — চেকআউটের কোড যদি শুধু PaymentGateway& চেনে, বদলটা এক জায়গায়।

ভুল বোঝা হয় দুইভাবে। প্রথম: “interface” মানে আক্ষরিকভাবে প্রতিটি জিনিসের জন্য একটি abstract class — যা থেকে আসে AI-এর interface explosion. GoF-এর “interface” আসলে আরও সাধারণ ধারণা — একটি object-এর বাইরের চেহারা। C++-এ সেটা concept বা std::function-ও হতে পারে, এমনকি একটি concrete class-এর ভালোভাবে ভাবা public ফাংশনগুলোও। দ্বিতীয় ভুল: সবকিছুকে বদলানো যায় এমন বানাতে গিয়ে এমন সব জায়গায় স্তর যোগ করা, যা কখনো বদলাবে না। std::string-এর জন্য কেউ interface বানায় না — কারণ সেটা বদলানোর বাস্তব কোনো কারণ নেই।

পর্ব ২৭-এ এই চিন্তার পরের ধাপ দেখবেন: Robert C. Martin-এর SOLID-এর “D” — Dependency Inversion Principle: উঁচু স্তরের নীতি-কোড (অর্ডার নেওয়া) নিচু স্তরের খুঁটিনাটির (SMS পাঠানো) ওপর নির্ভর করবে না; দুজনেই নির্ভর করবে একটি চুক্তির ওপর। আর Martin Fowler ২০০৪ সালের একটি প্রবন্ধে “dependency injection” নামটি জনপ্রিয় করেন — constructor-এ নির্ভরতা দেওয়ার যে কৌশল আপনি আজ দেখলেন।

মূল উপলব্ধি: নীতিটা হলো নির্ভরতার দিক ঠিক করা — যেখানে বদল আসবে বলে জানেন, সেখানে চুক্তির ওপর নির্ভর করুন। “সবখানে abstract class” এর মানে নয়।

প্র ০৪runtime polymorphism (virtual) আর compile-time polymorphism (template/concept) — খরচ আর সুবিধার আসল হিসাবটা কী? কখন কোনটি?

খরচ থেকে শুরু করা যাক। virtual ডাকের সরাসরি খরচ ছোট: object থেকে vptr পড়া, vtable থেকে ঠিকানা পড়া, তারপর পরোক্ষ ডাক। কিন্তু আসল দাম লুকিয়ে থাকে অন্য জায়গায় — compiler সাধারণত ফাংশনটি inline করতে পারে না, কারণ সে জানে না কোন ফাংশন চলবে। inline না হলে তার পরের অনেক optimization-ও (যেমন লুপের ভেতরে হিসাব সরল করা, vectorization) বন্ধ হয়ে যায়। একটি লুপে কোটিবার ছোট একটি virtual ফাংশন ডাকলে পার্থক্যটা চোখে পড়ার মতো। তবে compiler কখনো কখনো আসল টাইপ নিজেই বুঝে ফেলে (যেমন class final হলে) আর ডাকটা সরাসরি করে দেয় — একে বলে devirtualization. তার ওপর, polymorphic object প্রায়ই heap-এ থাকে, যা cache-এর জন্য খারাপ।

template-এর দিকে ডাক সরাসরি, inline হয়, optimizer পুরো স্বাধীনতা পায়। C-এর qsort (function pointer দিয়ে তুলনা) আর C++-এর std::sort (template, তুলনা inline) — এর পুরোনো কিন্তু বিখ্যাত উদাহরণ; সাধারণত std::sort দ্রুততর। কিন্তু দামও আছে: প্রতিটি টাইপের জন্য আলাদা কপি বলে binary বড় হয় (code bloat), কোড header-এ রাখতে হয় বলে compile ধীর হয়, আর চলার সময় নতুন টাইপ যোগ করা অসম্ভব — সব টাইপ compile-এর সময়েই জানা থাকতে হবে। এক vector-এ নানা টাইপও রাখা যায় না।

runtime interface-এর আসল শক্তি সেখানে, যেখানে template পারে না: আলাদা করে compile করা plugin, চলার সময় ব্যবহারকারীর বাছাই (“কোন পেমেন্ট পদ্ধতি?”), এক তালিকায় নানা ধরনের object, আর স্থির বাইনারি সীমানা। আর একটি বড় সুবিধা প্রায়ই উপেক্ষিত: compile সময়। বড় প্রজেক্টে interface-এর পেছনে implementation লুকালে একটি ফাইল বদলালে সব কিছু আবার compile হয় না।

বাস্তবে ভালো C++ কোড দুটোই মেশায়। standard library-র কন্টেইনার আর algorithm template-ভিত্তিক, অথচ iostream-এর streambuf virtual-ভিত্তিক। একটি সাধারণ নিয়ম: সিস্টেমের বড় অংশগুলোর মধ্যের সীমানায় (module, plugin, টেস্টের জোড়া) runtime interface; ছোট, বারবার চলা, পারফরম্যান্স-জরুরি অংশে compile-time. আর আন্দাজে নয় — পর্ব ৪৯-এর মতো মেপে সিদ্ধান্ত নিন।

মূল উপলব্ধি: virtual-এর দাম মূলত “inline করতে না পারা”, ডাকের খরচ নয়; template-এর দাম মূলত compile সময় আর নমনীয়তা। সীমানায় runtime, গরম লুপে compile-time.

প্র ০৫Go ভাষার interface “implicit” — কোনো class-কে কোথাও লিখতে হয় না যে সে interface মানে। C++-এর পদ্ধতি থেকে এটা কীভাবে আলাদা, আর কোনটির কী লাভ-ক্ষতি?

Go (Google, ২০০৯ সালে প্রকাশ্যে ঘোষিত) একটি সাহসী সিদ্ধান্ত নেয়: কোনো টাইপের যদি interface-এর সব method থাকে, তাহলে সে নিজে থেকেই সেই interface মানে — implements বা inheritance জাতীয় কিছু লিখতে হয় না। একে বলে structural typing — “আকার মিললেই চলবে।” C++-এর abstract class এর উল্টো, nominal: SmsNotifier-কে স্পষ্ট লিখতে হয় : Notifier, নইলে তার send থাকলেও সে Notifier নয়।

Go-র পদ্ধতির সবচেয়ে বড় লাভ: interface ব্যবহারকারী নিজে বানাতে পারে, পরে। ধরুন আপনি অন্য কারও লাইব্রেরির একটি টাইপ ব্যবহার করছেন, যেটা আপনার interface-এর কথা কখনো শোনেনি। Go-তে আপনি নিজের কোডে একটি ছোট interface লিখলেই সেই টাইপ তাতে খাপ খায় — লাইব্রেরি বদলাতে হয় না। এজন্যই Go-তে interface সাধারণত ছোট আর ব্যবহারকারীর দিকে লেখা হয়; বিখ্যাত উদাহরণ io.Reader আর io.Writer, যাদের প্রত্যেকটিতে একটিমাত্র method. Rob Pike-এর একটি জনপ্রিয় “Go proverb” আছে: “The bigger the interface, the weaker the abstraction.” C++-এ একই কাজ করতে হলে একটি adapter class লিখতে হয়, যে বাইরের টাইপকে মুড়িয়ে আপনার interface মানায়।

ক্ষতিও আছে। আকস্মিক মিল সম্ভব — দুটি সম্পূর্ণ ভিন্ন অর্থের method-এর নাম আর স্বাক্ষর মিলে গেলে টাইপটি এমন চুক্তি “মানে”, যা তার লেখক কখনো ভাবেননি। আবার একটি টাইপ কোন কোন interface মানে, কোড দেখে এক নজরে বলা কঠিন। ভেতরে Go-র একটি interface value দুটি জিনিস বহন করে — আসল টাইপের তথ্য (method-এর টেবিলসহ) আর ডেটার pointer; এই টেবিল প্রয়োজনমতো চলার সময় তৈরি হয়, C++-এর মতো class-এর সাথে আগে থেকে বাঁধা থাকে না।

মজার ব্যাপার, C++-ও structural চুক্তি জানে — তবে compile-এর সময়ে। C++20-এর concept ঠিক Go-র মতো “আকার মিললেই চলবে”: CanSend concept-এর জন্য কোনো inheritance লাগে না। আর runtime-এ Go-এর মতো আচরণ চাইলে C++-এ type erasure (std::function বা নিজের বানানো) আছে, যা ঠিক Go-র interface value-এর মতো “টাইপ-তথ্য + ডেটা” জোড়া রাখে। Go-ও ২০২২ সালের 1.18 সংস্করণে generics আনার সময় interface-কেই template-শর্ত (constraint) হিসেবে ব্যবহার করে — দুই ভাষা একই সমস্যার দুই দিক থেকে এসেছে।

মূল উপলব্ধি: nominal (C++-এর abstract class) মানে “ইচ্ছা করে সই করা চুক্তি” — স্পষ্ট, কিন্তু আগে থেকে পরিকল্পনা লাগে; structural (Go, C++ concepts) মানে “আচরণ মিললেই চুক্তি” — নমনীয়, কিন্তু অর্থের মিল নিশ্চিত করে না।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. compile হবে কি? struct Shape { virtual ~Shape() = default; virtual double area() const = 0; virtual double perimeter() const = 0; }; আর struct Circle : Shape { double r = 1; double area() const override { return 3.14 * r * r; } };. এবার main-এ (ক) Circle c; (খ) Shape* p = nullptr; (গ) std::vector<Shape> v(3); — কোনগুলো compile হবে, কোনগুলো হবে না, আর কেন?

    (ক) হবে না। Circle শুধু area override করেছে, perimeter এখনো pure — তাই Circle-ও abstract, আর error-এর note-এ perimeter-এর নাম আসবে। (খ) হবে: abstract টাইপের pointer বৈধ। (গ) হবে না: v(3) মানে vector-কে নিজের ভেতরে ৩টি Shape object বানাতে হবে, যা abstract টাইপের জন্য অসম্ভব। নানা আকৃতি এক তালিকায় রাখতে std::vector<std::unique_ptr<Shape>> লিখুন।

  2. নকশা করুন: একটি কোচিং সেন্টারের অ্যাপ প্রতিদিন উপস্থিতি নেয়, অনুপস্থিত ছাত্রছাত্রীর অভিভাবককে বার্তা পাঠায়, আর সব রেকর্ড জমা রাখে। কোন কোন অংশের জন্য interface বানাবেন, প্রতিটিতে কোন ফাংশন থাকবে, আর কোনটির interface বানাবেন না? টেস্টে কী বসাবেন?

    একটি সম্ভাব্য উত্তর: Notifier (send(phone, msg)) — আসলে SMS, টেস্টে Fake; AttendanceStore (save(date, roll, present), absentees(date)) — আসলে ফাইল বা ডেটাবেস, টেস্টে MemoryStore. হয়তো Clock (today()) — টেস্টে নির্দিষ্ট তারিখ ফেরত দিতে, যাতে ফল প্রতিবার একই হয়। মূল নিয়মের class (AttendanceService: অনুপস্থিত খুঁজে বার্তা পাঠানো) — এর interface দরকার নেই, এর একটাই implementation; বরং এটাই constructor-এ উপরের তিনটি নেবে। ছাত্রের তথ্য রাখার Student struct-এরও interface নয় — ওটা শুধু ডেটা।

  3. বাগ শিকার: একটি AI এই কোড দিয়েছে — class IPayment { public: virtual void payWithBkashApi(std::string pin, int taka) = 0; }; class Checkout { std::unique_ptr<IPayment> p = std::make_unique<BkashPayment>(); public: void buy(int t) { if (auto b = dynamic_cast<BkashPayment*>(p.get())) b->payWithBkashApi("1234", t); } }; — কয়টি সমস্যা খুঁজে পান?

    অন্তত চারটি। (১) virtual destructor নেই — unique_ptr<IPayment> BkashPayment মুছবে IPayment* দিয়ে: undefined behavior. (২) interface-এ নির্দিষ্ট প্রযুক্তির নাম আর খুঁটিনাটি (payWithBkashApi, pin) ফাঁস — অন্য গেটওয়ে এই চুক্তি মানতেই পারবে না; হওয়া উচিত pay(int taka), আর পরিচয়ের তথ্য যাবে concrete class-এর constructor-এ। (৩) Checkout নিজেই make_unique<BkashPayment> বানাচ্ছে — বদলানো বা টেস্টে Fake বসানো যায় না; constructor-এ নিন। (৪) dynamic_cast দিয়ে concrete টাইপে ফেরা — interface থাকার পুরো অর্থই নষ্ট। বোনাস: PIN কোডে লিখে রাখা নিরাপত্তার দিক থেকেও মারাত্মক ভুল।

  4. নিজে লিখুন (কোড): উপরের notifier_di.cpp-এ একটি EmailNotifier যোগ করুন, যা [EMAIL] ... প্রিন্ট করে, আর main-এ তৃতীয় একটি OrderService দিয়ে অর্ডার 303 দিন। OrderService-এর ভেতরে কয় লাইন বদলাতে হলো?

    যোগ করুন: struct EmailNotifier : Notifier { void send(const std::string& msg) override { std::println("[EMAIL] {}", msg); } }; আর main-এ EmailNotifier mail; OrderService office(mail); office.place(303);. নতুন লাইন আসবে [EMAIL] Order 303 confirmed — main-এর ক্রম অনুযায়ী অবস্থান। OrderService-এ বদলাতে হয়েছে শূন্য লাইন। override লিখে একবার ইচ্ছা করে স্বাক্ষর ভুল করে দেখুন (যেমন std::string msg নিয়ে, const reference নয়) — compiler সঙ্গে সঙ্গে error দেবে; override না লিখলে নিঃশব্দে একটি নতুন ফাংশন হতো, আর EmailNotifier abstract থেকে যেত।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমC++-এ std::unique_ptr<Notifier> p = std::make_unique<SmsNotifier>(); — Notifier-এর destructor virtual. p-এর scope শেষ হলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. p-এর destructor চলে, Notifier* দিয়ে delete ডাকে
  2. virtual destructor ডাকটা পৌঁছে দেয় ~SmsNotifier-এ
  3. ~SmsNotifier-এর শরীর চলে
  4. SmsNotifier-এর নিজের ডেটা member-রা ধ্বংস হয়
  5. base অংশের ~Notifier চলে
  6. পুরো object-এর মেমরি ফেরত যায়

C++-এ struct A { virtual void f() = 0; }; ঘোষণার পর A a; লিখলে কী হয়?

  • a তৈরি হয়, আর a.f() কিছুই করে না
  • Compile error — A একটি abstract class
  • compile হয়, কিন্তু a.f() ডাকলে প্রোগ্রাম crash করে
  • শুধু stack-এ নিষেধ, new A দিয়ে বানানো যায়

একটি C++ abstract class Base-এ দুটি pure virtual function আছে। Derived : Base শুধু একটি override করেছে। Derived সম্পর্কে কোনটি সত্য?

  • Derived concrete; বাকি ফাংশনটি খালি শরীর পায়
  • Derived ঘোষণা করাই compile error
  • Derived-ও abstract; তার object বানানো যায় না
  • Derived concrete; বাকি ফাংশনে Base-এর সংস্করণ চলে

C++-এ একটি interface class-এ virtual destructor রাখা জরুরি কেন?

  • base pointer দিয়ে derived object মুছলে সঠিক destructor চালাতে
  • এটা ছাড়া class abstract হয় না
  • compile দ্রুত করতে
  • object কপি হওয়া বন্ধ করতে

standard C++-এ একটি “interface” সাধারণত কীভাবে লেখা হয়?

  • interface keyword দিয়ে
  • শুধু template দিয়ে
  • ডেটা member আর virtual ফাংশন মিশিয়ে একটি base class
  • শুধু pure virtual function আর virtual destructor নিয়ে একটি ডেটাহীন class

C++-এ struct Shape { virtual ~Shape() = 0; }; লেখা হলো, কিন্তু Shape::~Shape-এর কোনো definition কোথাও নেই। struct Box : Shape {}; আর Box b; ব্যবহার করলে কী হয়?

  • compile error — Box abstract
  • সব ঠিক চলে, কারণ pure destructor-এর শরীর লাগে না
  • link error — Shape::~Shape-এর definition পাওয়া যায় না
  • চলার সময় crash

একটি C++ প্রোগ্রামে নানা ধরনের পেমেন্ট plugin আলাদা DLL থেকে চলার সময় লোড হয়, আর ব্যবহারকারী চলার সময় বেছে নেন কোনটি দিয়ে পেমেন্ট হবে। চুক্তি লেখার সবচেয়ে উপযুক্ত উপায় কোনটি?

  • template আর C++20 concept
  • pure virtual function-ওয়ালা abstract class
  • #define macro
  • প্রতিটি plugin-এর নাম দেখে if-else

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. interface = চুক্তি: কী করতে হবে, কীভাবে নয়। client আর implementation দুজনেই শুধু চুক্তির ওপর নির্ভর করে।
  2. = 0 দিয়ে pure virtual; একটিও থাকলে class abstract — object হয় না, pointer আর reference হয়। override না করা derived-ও abstract.
  3. C++-এর interface: শুধু pure virtual function + virtual ~X() = default;, কোনো ডেটা নয়। virtual destructor কখনো বাদ নয়।
  4. নির্ভরতা constructor-এ নিন (dependency injection): তাতেই SMS, ইমেইল বা টেস্টের Fake — যেকোনোটি বসানো যায়। ছোট ছোট interface, একটি class চাইলে কয়েকটি মানতে পারে।
  5. বিকল্প মনে রাখুন: compile-time চুক্তি (template + concept) দ্রুত, std::function এক-ফাংশনের চুক্তিতে হালকা। আর একটিমাত্র implementation হলে interface প্রায়ই অপ্রয়োজনীয়।

পরের পর্বে: এই পর্বের বেশ কিছু ধারণা — ছোট interface, পুরোনো কোড না ছুঁয়ে নতুন আচরণ, চুক্তির ওপর নির্ভরতা — একসাথে একটি নাম পায়: SOLID. সাথে বড় প্রশ্নটা: কখন inheritance, আর কখন composition?

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান