এই পর্বে যা শিখবেন
- একটি বাস্তব সমস্যার বিবরণ থেকে ধাপে ধাপে class, দায়িত্ব, সম্পর্ক আর invariant বের করা
- Object-রা কীভাবে একে অপরকে বার্তা পাঠিয়ে কাজ সারে — টাকা পাঠানোর উদাহরণে
- ফাংশন + ছড়ানো ডেটা বনাম নিজের ডেটা পাহারা দেওয়া object — প্রোগ্রাম বড় হলে কোনটা কেন টেকে
- CRC কার্ড আর সহজ বাক্স-ছবি দিয়ে কাগজে-কলমে নকশা করা
- কখন object বানাবেন না, আর AI-এর দেওয়া class ডিজাইন কীভাবে যাচাই করবেন
১ · চার কৌশলের ঝটপট স্মরণ — আর C++-এর নতুন প্রশ্ন
আগের পর্বে দেখেছেন, C++ জন্ম নিয়েছিল একটি প্রশ্ন থেকে — “C-এর গতি রেখে কীভাবে বড় প্রোগ্রাম সামলানো যায়?” এই পর্বে সেই প্রশ্নের চিন্তার দিকটা দেখব। এখানে প্রায় কোনো কোড নেই। C++-এর ব্যাকরণ শুরু হবে পর্ব ০৪ থেকে; আজকের কাজ হলো ভাবার অভ্যাস তৈরি করা, যাতে পরে প্রতিটি ব্যাকরণের কেন দরকার, সেটা নিজেই বুঝতে পারেন।
যেকোনো প্রোগ্রাম লেখার আগে চারটি পরিচিত কৌশল কাজে লাগে। C কোর্সে এগুলো বিস্তারিত আছে; এখানে শুধু এক লাইনে মনে করিয়ে দিই:
🧩 Decomposition
বড় সমস্যাকে ছোট টুকরোয় ভাঙা। “অনলাইন ওয়ালেট বানাও” — এটা একবারে হয় না; টাকা জমা, তোলা, পাঠানো, হিসাব রাখা আলাদা টুকরো।
🔁 Pattern
বারবার আসা মিল খোঁজা। Cash in, send money, bill pay — সবগুলোতেই “একজনের টাকা কমে, আরেকজনের বাড়ে।”
🎯 Abstraction
অদরকারি খুঁটিনাটি বাদ দেওয়া। ব্যবহারকারীর জন্মতারিখ টাকা পাঠানোর হিসাবে লাগে না।
📋 Algorithm
ধাপগুলো নির্ভুল ক্রমে লেখা — আগে ব্যালান্স যাচাই, তারপর কাটা, তারপর জমা।
C-এর মতো ভাষায় decomposition সাধারণত হয় কাজ ধরে: “প্রোগ্রামটা কোন কোন ধাপে কাজ করবে?” উত্তর হয় কিছু ফাংশন — send_money(), cash_out(), print_statement() — আর পাশে আলাদা করে রাখা ডেটা। একে বলে procedural programming.
C++ আপনাকে আরেকটি প্রশ্ন করতে শেখায়: “এই দুনিয়ায় কারা আছে, আর কে কোন কাজের দায়িত্বে?” ধাপ নয়, প্রথমে চরিত্র। ওয়ালেট, লেনদেন, খতিয়ান, এজেন্ট — প্রত্যেকে একেকটি চরিত্র, যার নিজের তথ্য আছে আর নিজের দায়িত্ব আছে। প্রোগ্রাম মানে এই চরিত্রদের মধ্যে কাজ ভাগ করে দেওয়া।
একটি ভালো রেস্তোরাঁর কথা ভাবুন। ম্যানেজার যদি নিজেই রান্না, পরিবেশন, ক্যাশ আর বাজার — সব একা করেন (procedural ধারা), ছোট দোকানে সেটা চলে। কিন্তু ৫০ টেবিলের রেস্তোরাঁয় দরকার আলাদা দায়িত্ব: বাবুর্চি রান্না জানেন, ওয়েটার অর্ডার নেন, ক্যাশিয়ার টাকা সামলান। ওয়েটার রান্নাঘরে ঢুকে নিজে রান্না করেন না — তিনি বাবুর্চিকে অর্ডার পাঠান। ক্যাশবাক্সে ক্যাশিয়ার ছাড়া কেউ হাত দেন না। Object দিয়ে ভাবা ঠিক এই রকম।
এই চিন্তার শুরু নরওয়েতে। ১৯৬০-এর দশকে Ole-Johan Dahl ও Kristen Nygaard জাহাজঘাট, কারখানার মতো বাস্তব ব্যবস্থার simulation লিখতে গিয়ে Simula ভাষা বানান; Simula 67-এ প্রথম class আর object-এর ধারণা আসে। ১৯৭৯ সালে Bell Labs-এ Bjarne Stroustrup নিজের গবেষণায় Simula-র এই সুবিধা আর C-এর গতি একসাথে চাইলেন — তৈরি করলেন “C with Classes”, যার নাম ১৯৮৩ সালে হয় C++.
২ · Object মানে কী — অবস্থা আর আচরণ একসাথে
রিনার ফোনের মোবাইল ওয়ালেটটা ভাবুন। এর দুটি দিক আছে:
- সে কী জানে — মালিকের নাম, ফোন নম্বর, বর্তমান ব্যালান্স ৳১,৫০০, আজ কত টাকা পাঠানো হয়েছে। এগুলো তার state বা অবস্থা।
- সে কী করতে পারে — টাকা জমা নেওয়া, টাকা তোলা, টাকা পাঠানো, ব্যালান্স জানানো। এগুলো তার behaviour বা আচরণ।
এই দুটিকে একসাথে বাঁধলেই পাওয়া যায় একটি object. আর “ওয়ালেট” জিনিসটা কেমন হয় — কী কী তথ্য রাখে, কী কী কাজ করে — তার সাধারণ নকশাকে বলে class. এক নকশা থেকে লাখো object: রিনার ওয়ালেট, করিমের ওয়ালেট, আপনার ওয়ালেট — নকশা এক, কিন্তু প্রত্যেকের ব্যালান্স আলাদা।
Class হলো ছাঁচ, object হলো সেই ছাঁচে বানানো আসল জিনিস। পিঠার ছাঁচ একটা, পিঠা অনেক — প্রতিটি পিঠার ভেতরের পুর আলাদা হতে পারে। আর object-এর সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য: নিজের ডেটা কীভাবে বদলাবে, সেটা সে নিজেই ঠিক করে।
শেষ কথাটাই আসল। ওয়ালেটের ব্যালান্স বাইরের কেউ সরাসরি লিখে বদলাতে পারে না। বদলাতে হলে ওয়ালেটকে অনুরোধ করতে হয় — “৫০০ টাকা তোলো।” ওয়ালেট তখন নিজের নিয়ম মিলিয়ে দেখে: যথেষ্ট টাকা আছে কি? আজকের সীমা পার হচ্ছে কি? তারপর রাজি হয় বা না বলে। ডেটা আর ডেটার নিয়ম একই ঘরে রেখে বাইরের হাত আটকানো — একে বলে encapsulation.
প্রতিটি object-এর তিনটি প্রশ্ন
| প্রশ্ন | ওয়ালেটের উত্তর | নাম |
|---|---|---|
| আমি কী জানি? | ব্যালান্স, মালিক, আজকের মোট পাঠানো টাকা | State (ডেটা) |
| আমি কী করি? | জমা নিই, তুলি, পাঠাই, ব্যালান্স জানাই | Behaviour (কাজ) |
| আমি কোন নিয়ম কখনো ভাঙি না? | ব্যালান্স কখনো শূন্যের নিচে নয়; দৈনিক সীমার বেশি পাঠাই না | Invariant |
C-তে আপনি struct দিয়ে ডেটা বাঁধতেন, আর আলাদা ফাংশনে সেই struct-এর pointer পাঠাতেন — ভাবনাটা একই, কিন্তু নিয়ম রক্ষার দায় ছিল প্রতিটি ফাংশনের সততার ওপর। C++-এ ফাংশনগুলো struct-এর ভেতরেই থাকে (member function), আর private লিখে ডেটাকে বাইরের হাত থেকে আটকে দেওয়া যায় — নিয়ম ভাঙলে compiler নিজেই ধরে। C-এর দিকটা আবার দেখতে চাইলে C কোর্সের struct ও union পর্বটি পড়ুন। C না জানলে এই বাক্সটি বাদ দিয়ে এগোতে পারেন।
৩ · একটি মোবাইল ওয়ালেট — সমস্যার বিবরণ থেকে class
এবার পুরো প্রক্রিয়াটা হাতে-কলমে করি। ধরুন একটি স্টার্টআপ “দ্রুতপে” নামে bKash বা Nagad-এর মতো একটি কাল্পনিক মোবাইল ওয়ালেট বানাতে চায়। প্রোডাক্ট ম্যানেজার আপনাকে চার লাইনের একটি বিবরণ দিলেন। এই বিবরণ থেকে নকশায় যাওয়ার একটি পুরোনো, কার্যকর কৌশল আছে — বিশেষ্য দাগান, ক্রিয়া দাগান। নিচের ডায়াগ্রামে ধাপে ধাপে দেখুন:
প্রতিটি গ্রাহকের একটি ওয়ালেট থাকবে। গ্রাহক এজেন্টের কাছে টাকা জমা দিতে ও তুলতে পারবেন। গ্রাহক অন্য গ্রাহককে টাকা পাঠাতে পারবেন; প্রতিটি লেনদেন খতিয়ানে লেখা থাকবে। ব্যালান্সের বেশি টাকা কখনো পাঠানো যাবে না। মাস শেষে গ্রাহক স্টেটমেন্ট দেখতে পারবেন।
প্রতিটি গ্রাহকের একটি ওয়ালেট থাকবে। গ্রাহক এজেন্টের কাছে টাকা জমা দিতে ও তুলতে পারবেন। গ্রাহক অন্য গ্রাহককে টাকা পাঠাতে পারবেন; প্রতিটি লেনদেন খতিয়ানে লেখা থাকবে। ব্যালান্সের বেশি টাকা কখনো পাঠানো যাবে না। মাস শেষে গ্রাহক স্টেটমেন্ট দেখতে পারবেন।
প্রতিটি গ্রাহকের একটি ওয়ালেট থাকবে। গ্রাহক এজেন্টের কাছে টাকা জমা দিতে ও তুলতে পারবেন। গ্রাহক অন্য গ্রাহককে টাকা পাঠাতে পারবেন; প্রতিটি লেনদেন খতিয়ানে লেখা থাকবে। ব্যালান্সের বেশি টাকা কখনো পাঠানো যাবে না। <- নিয়ম! মাস শেষে গ্রাহক স্টেটমেন্ট দেখতে পারবেন।
Customer গ্রাহক -> class (নিজের তথ্য আছে) Wallet ওয়ালেট -> class (ব্যালান্স + নিয়ম) Agent এজেন্ট -> class (cash in / out করেন) Transaction লেনদেন -> class (কে, কাকে, কত, কখন) Ledger খতিয়ান -> class (সব লেনদেনের খাতা) balance ব্যালান্স -> Wallet-এর ডেটা, আলাদা class নয় taka টাকা -> শুধু একটি সংখ্যা (Money) statement স্টেটমেন্ট -> Ledger থেকে বানানো রিপোর্ট month মাস -> শুধু একটি তারিখের সীমা
- শুরু — সাধারণ ভাষার বিবরণ। প্রোডাক্ট ম্যানেজার কোড বোঝেন না, বোঝেন ব্যবসা। তাঁর ভাষায় লেখা এই কয়েক লাইনই আপনার কাঁচামাল। খেয়াল করুন, এখানে কোনো “ফাংশন” বা “ভ্যারিয়েবল” শব্দ নেই — আছে মানুষ, জিনিস আর কাজ।
- বিশেষ্য দাগান। প্রতিটি নাম-শব্দ — গ্রাহক, ওয়ালেট, এজেন্ট, টাকা, লেনদেন, খতিয়ান, ব্যালান্স, মাস, স্টেটমেন্ট — একেকটি সম্ভাব্য class. এখনই বাছবেন না; আগে সবগুলো তুলে আনুন।
- ক্রিয়া দাগান। কাজের শব্দগুলো — জমা দেওয়া, তোলা, পাঠানো, লেখা, দেখা — হবে কারও না কারও দায়িত্ব। আর “পাঠানো যাবে না” লাইনটা কাজ নয়, একটা নিয়ম — এটাই পরে invariant হবে।
- তালিকা ছাঁকুন। প্রতিটি বিশেষ্যকে জিজ্ঞেস করুন: “এর কি নিজের তথ্য আর নিজের কাজ আছে?” Wallet-এর আছে (ব্যালান্স + নিয়ম), তাই class. কিন্তু “ব্যালান্স” কারও একটি তথ্য মাত্র, আর “মাস” শুধু একটি তারিখের সীমা — এগুলো আলাদা class নয়, অন্য class-এর ডেটা।
- প্রথম খসড়া। পাঁচটি class আর একটি সবচেয়ে গুরুত্বপূর্ণ নিয়ম। প্রতিটি ক্রিয়া একজন মালিক পেয়েছে: deposit/withdraw/send → Wallet, record/statement → Ledger. সবুজ চিহ্নিত Wallet হলো নকশার কেন্দ্র, কারণ টাকার নিয়ম সে-ই রক্ষা করে। এটা প্রথম খসড়া — পরের অংশগুলোতে এটাকেই যাচাই করব।
বিশেষ্য-ক্রিয়ার কৌশল একটি শুরুর হাতিয়ার, যান্ত্রিক নিয়ম নয়। সব বিশেষ্য class হয় না (“মাস”), আবার কিছু গুরুত্বপূর্ণ class বিবরণে লুকিয়ে থাকে — যেমন “লেনদেন ব্যর্থ হলে আগের অবস্থায় ফেরা” লাইনটা থাকলে হয়তো একটি আলাদা দায়িত্ব লাগত। বিবরণ পড়ে প্রশ্ন করুন, প্রোডাক্ট ম্যানেজারকে জিজ্ঞেস করুন।
সম্পর্ক — কে কার সাথে কীভাবে যুক্ত
Class পাওয়া গেল। এবার তাদের মধ্যে সম্পর্ক। প্রায় সব নকশায় তিন ধরনের সম্পর্কই ঘুরেফিরে আসে:
has-a · “এর আছে”
Customer-এর একটি Wallet আছে। Ledger-এর ভেতরে অনেক Transaction আছে। একটি জিনিস আরেকটির অংশ — একে বলে composition.
সবচেয়ে বেশি ব্যবহৃতis-a · “এটি এক ধরনের”
MerchantWallet (দোকানদারের ওয়ালেট) এক ধরনের Wallet — সব নিয়ম একই, শুধু ফি আলাদা। একে বলে inheritance.
সাবধানে, কম ব্যবহার করুনuses · “ব্যবহার করে”
Agent cash in করার সময় গ্রাহকের Wallet ব্যবহার করেন, কিন্তু Wallet এজেন্টের অংশ নয়। একে বলে association.
কাজের জন্য সাময়িক যোগাযোগএকটি সহজ পরীক্ষা: বাক্যটা জোরে বলুন। “Customer is a Wallet” — অর্থহীন, তাই is-a নয়। “Customer has a Wallet” — ঠিক। “MerchantWallet is a Wallet” — ঠিক। বাংলায় বা ইংরেজিতে বাক্যটা স্বাভাবিক শোনালে সম্পর্কটা সাধারণত ঠিক।
Invariant — যে নিয়ম কখনো ভাঙবে না
বিবরণের সবচেয়ে মূল্যবান লাইনটি ছিল: “ব্যালান্সের বেশি টাকা কখনো পাঠানো যাবে না।” নকশার ভাষায় এটি Wallet-এর invariant: balance ≥ 0, সবসময়। Object-ওরিয়েন্টেড নকশার বড় প্রতিশ্রুতি হলো — এই নিয়মটা লেখা থাকবে একটি জায়গায়, Wallet-এর ভেতরে, আর প্রোগ্রামের অন্য কোনো অংশ এটা ভাঙার সুযোগই পাবে না। আরও কয়েকটি invariant ভাবুন:
- Transaction-এর amount সবসময় শূন্যের বেশি — শূন্য বা ঋণাত্মক টাকা পাঠানো অর্থহীন।
- Ledger-এর সব লেনদেনের যোগফল মিলিয়ে দেখলে মোট টাকা হারায়ও না, হঠাৎ তৈরিও হয় না।
- একবার লেখা লেনদেন খতিয়ান থেকে মোছা যায় না; ভুল হলে উল্টো একটি লেনদেন (refund) যোগ হয়।
৪ · Object-রা বার্তা পাঠায় — টাকা পাঠানোর ভেতরের গল্প
নকশা তো হলো, কিন্তু কাজ হয় কীভাবে? Object-দের জগতে কেউ কারও ডেটায় হাত দেয় না — একে অপরকে বার্তা পাঠায়: “৫০০ টাকা তোলো”, “এই লেনদেনটা লিখে রাখো।” গ্রহণকারী নিজেই ঠিক করে কীভাবে সাড়া দেবে। একে বলে message passing. C++-এ বার্তা পাঠানো মানে সাধারণত একটি member function ডাকা — rina.withdraw(1000) — যার ব্যাকরণ পর্ব ২০-এ শিখবেন।
রিনা তার ৳১,৫০০ ব্যালান্স থেকে করিমকে ৳১,০০০ পাঠাচ্ছে। নিচের ডায়াগ্রামে বাঁ দিকে বার্তাগুলো (কোড নয়, বার্তার তালিকা), ডান দিকে প্রতিটি object-এর অবস্থা। “পরে” চেপে দেখুন কোন বার্তায় কার অবস্থা বদলায়:
1app -> rina : send(karim, 1000)2rina -> rina : canPay(1000)? 1500 >= 10003rina -> tx : create(rina, karim, 1000)4tx -> rina : withdraw(1000)5tx -> karim : deposit(1000)6tx -> ledger : record(tx)7tx -> app : status = DONE
| rina.balance | |
| karim.balance | |
| tx.status | |
| ledger.entries |
- অ্যাপ অনুরোধ পাঠাল। রিনা “Send Money” চাপলেন। অ্যাপের পর্দা রিনার ওয়ালেট object-কে একটি বার্তা পাঠাল: “করিমকে ১০০০ পাঠাও।” খেয়াল করুন — অ্যাপ নিজে ব্যালান্স থেকে কিছু বিয়োগ করছে না; সে শুধু অনুরোধ করছে।
- Wallet নিজের নিয়ম যাচাই করল। রিনার ওয়ালেট নিজের invariant মিলিয়ে দেখল: ১৫০০ ≥ ১০০০? হ্যাঁ। ব্যালান্স কম থাকলে এখানেই “না” বলে ফিরিয়ে দিত — আর কোনো object-এ কিছুই বদলাত না। নিয়ম পাহারা দেওয়ার দায়িত্ব ওয়ালেটের, অ্যাপের নয়।
- একটি Transaction object জন্ম নিল। “কে, কাকে, কত” — এই তথ্য নিয়ে একটি লেনদেন তৈরি হলো, অবস্থা PENDING. এখন থেকে পুরো কাজটা ঠিকমতো শেষ করার দায়িত্ব এই লেনদেনের।
- রিনার টাকা কাটা হলো। লেনদেন রিনার ওয়ালেটকে বার্তা পাঠাল:
withdraw(1000). ব্যালান্স হলো ৫০০ (ডান দিকে বদলানো ঘরটি সবুজ)। আবারও — ব্যালান্স বদলেছে ওয়ালেট নিজেই, লেনদেন শুধু অনুরোধ করেছে। - করিমের টাকা জমা হলো। করিমের ওয়ালেট ২০০ থেকে ১২০০. একটি গভীর প্রশ্ন ভাবুন: ধাপ ৪ সফল কিন্তু ধাপ ৫ ব্যর্থ হলে (যেমন করিমের অ্যাকাউন্ট বন্ধ)? টাকা হাওয়া হয়ে যাবে! তাই আসল ব্যবস্থায় লেনদেন object-কে দায়িত্ব নিতে হয় “হয় দুটোই হবে, নয় কোনোটাই না” — ব্যর্থ হলে রিনার টাকা ফেরত। এই দায়িত্ব কার, সেটা নকশার সময়েই ঠিক করতে হয়।
- খতিয়ানে লেখা হলো। লেনদেন নিজেকে Ledger-এ লিখে দিল — এন্ট্রি ৪১ থেকে ৪২. মাস শেষের স্টেটমেন্ট এখান থেকেই তৈরি হবে। Ledger জানে না ব্যালান্স কীভাবে হিসাব হয়; সে শুধু নির্ভুলভাবে লিখে রাখতে জানে।
- কাজ শেষ, ফল জানানো হলো। লেনদেনের অবস্থা DONE, অ্যাপ পর্দায় দেখাল “Sent 1000 to Karim.” পুরো গল্পে কেউ অন্যের ডেটায় সরাসরি হাত দেয়নি — শুধু বার্তা গেছে। প্রতিটি object নিজের অবস্থার একমাত্র মালিক।
যার ডেটা, নিয়ম রক্ষার দায়িত্বও তার।
একটি পুরোনো নকশা-নীতি এটাকে সুন্দর করে বলে: “Tell, don’t ask” — object-কে তার ডেটা জিজ্ঞেস করে বাইরে সিদ্ধান্ত নেবেন না (“ব্যালান্স কত আছে? ঠিক আছে, আমিই বিয়োগ করে দিচ্ছি”); বরং তাকে কাজটা করতে বলুন (“১০০০ তোলো”), সিদ্ধান্ত সে নিক।
৫ · Procedural বনাম object — একই সমস্যা, দুই চিন্তা
“Object” শুনতে সুন্দর, কিন্তু আসল লাভটা কোথায়? সেটা বোঝার সেরা উপায় একই সমস্যা দুভাবে দেখা। প্রথমে procedural ধারা — ডেটা একটি সাধারণ বাক্সে, নিয়ম একটি আলাদা ফাংশনে। কোডের ব্যাকরণ নিয়ে এখন চিন্তা করবেন না (পর্ব ০৪ থেকে শুরু হবে); শুধু মন্তব্যগুলো পড়ুন আর চালিয়ে আউটপুট দেখুন:
#include <print>
struct Wallet { // just a box of data, no guard
long balance = 0;
};
// the rule lives OUTSIDE the data, in a free function
bool withdraw(Wallet& w, long taka) {
if (taka <= 0 || taka > w.balance) return false;
w.balance -= taka;
return true;
}
int main() {
Wallet rina{500};
std::println("withdraw 800 -> {}", withdraw(rina, 800));
rina.balance -= 800; // but any code can skip the rule!
std::println("balance = {}", rina.balance);
}নিয়মটা (withdraw ফাংশন) ঠিকঠাক কাজ করেছে — ৮০০ তুলতে চাইলে false দিয়েছে। কিন্তু পরের লাইনে কেউ নিয়ম এড়িয়ে সরাসরি rina.balance -= 800 লিখে দিল, আর ব্যালান্স হয়ে গেল -300! Compiler কোনো আপত্তি করেনি, কারণ struct-এর ভেতরের ডেটা সবার জন্য খোলা। দশজনের দলে কেউ একজন তাড়াহুড়োয় এমন একটা লাইন লিখলেই invariant শেষ।
এবার একই জিনিস object হিসেবে। নিয়ম আর ডেটা একই ঘরে, আর ডেটার গায়ে লেখা private — অর্থাৎ শুধু Wallet নিজেই এটা ছুঁতে পারে:
#include <print>
class Wallet {
public: // the promise (interface)
bool deposit(long taka) { // cash in
if (taka <= 0) return false; // reject a bad amount
balance_ += taka; return true;
}
bool withdraw(long taka) { // cash out
if (taka <= 0 || taka > balance_) return false; // keep balance >= 0
balance_ -= taka; return true;
}
long balance() const { return balance_; } // read-only view
private:
long balance_ = 0; // only Wallet may change this
};
int main() {
Wallet rina;
rina.deposit(500);
std::println("withdraw 800 -> {}", rina.withdraw(800));
std::println("withdraw 200 -> {}", rina.withdraw(200));
std::println("balance = {}", rina.balance());
}ফলাফল: ৮০০ তোলা প্রত্যাখ্যাত (false), ২০০ সফল (true), শেষে ব্যালান্স ৩০০. এখন কেউ main-এর ভেতরে rina.balance_ -= 800; লিখলে প্রোগ্রাম compile-ই হবে না — compiler বলবে balance_ হলো private. অর্থাৎ নিয়ম রক্ষার পাহারাদার এখন মানুষের সতর্কতা নয়, compiler নিজে। public অংশটা ওয়ালেটের প্রতিশ্রুতি (interface), private অংশটা তার ভেতরের ব্যাপার। এই শব্দগুলোকে বলে access specifier; বিস্তারিত পর্ব ২০-এ।
আসল পরীক্ষা — একটি পরিবর্তনের অনুরোধ
ছোট প্রোগ্রামে দুই ধারার পার্থক্য সামান্য। পার্থক্য বিশাল হয় যখন প্রোগ্রাম বড় হয় আর পরিবর্তন আসে — আর বাস্তবে পরিবর্তন আসবেই। ধরুন ছয় মাস পর ব্যবসা বিভাগ বলল: “প্রতিটি টাকা তোলায় ১% ফি কাটতে হবে” (সংখ্যাটা কাল্পনিক)। দুই ধারায় কোথায় কোথায় হাত দিতে হয়, দেখুন:
- Procedural প্রোগ্রাম। ব্যালান্স একটি খোলা ডেটা। চারটি আলাদা ফাংশন — টাকা পাঠানো, ক্যাশ আউট, বিল দেওয়া, দোকানে পেমেন্ট — প্রত্যেকে নিজে নিজে
balance -= amtকরে। দুই বছর ধরে চার-পাঁচজন মিলে লিখেছেন। - অনুরোধ এলো: ফি যোগ করুন। কোথায় বদলাবেন? যেখানে যেখানে ব্যালান্স কমে — সব জায়গায়। প্রথম কাজ: পুরো প্রজেক্টে খুঁজে বের করা কোথায় কোথায়
balanceবদলানো হয়েছে। এখানে চারটি, আসল প্রজেক্টে হয়তো চল্লিশটি। - তিনটি খুঁজে পেলেন, বদলালেন। send_money, cash_out, pay_bill-এ ফি যোগ হলো। পরীক্ষা করে দেখলেন সব ঠিক। কোড রিভিউ পাস, প্রকাশ হলো।
- একটি বাদ পড়ে গেল। merchant_pay নতুন একজন লিখেছিলেন অন্য নামে (
w.bal_now -= x), তাই খোঁজে আসেনি। এক মাস পর হিসাব মেলাতে গিয়ে দেখা গেল দোকানের পেমেন্টে কোনো ফি কাটা হয়নি — লাখ টাকার গরমিল। এটা কাল্পনিক, কিন্তু এমন বাগ বাস্তবের প্রায় প্রতিটি বড় procedural প্রজেক্টে কোনো না কোনো রূপে দেখা যায়। - Object প্রোগ্রাম। এখানে ব্যালান্স Wallet-এর private ডেটা। চারটি কাজই ব্যালান্স কমাতে বাধ্য হয়ে
wallet.withdraw()ডাকে, কারণ অন্য কোনো পথ খোলা নেই। ফি যোগ করতে বদলাতে হয় শুধু একটি জায়গা — Wallet-এর withdraw. - পার্থক্যটা কোথায়? Procedural ধারায় নিয়ম রক্ষা নির্ভর করে সবার মনে রাখার ওপর। Object ধারায় নির্ভর করে নকশার ওপর — নতুন কেউ ভুল পথে যেতে চাইলে compiler আটকায়। দল যত বড়, কোড যত পুরোনো, এই পার্থক্য তত বড়।
এই উদাহরণ থেকে দুটি পেশাদার শব্দ শিখে রাখুন। Coupling — অংশগুলো একে অপরের ভেতরের খবর কতটা জানে; procedural সংস্করণে চারটি ফাংশনই ব্যালান্সের ভেতরের রূপ জানত, তাই coupling বেশি। Cohesion — একটি অংশের কাজগুলো একটি লক্ষ্যে কতটা জড়ো; Wallet-এর সব কাজই “টাকার নিয়ম রক্ষা”, তাই cohesion বেশি। ভালো নকশার সূত্র: কম coupling, বেশি cohesion.
ন্যায্য থাকি: procedural ধারা খারাপ নয়। C-তেও ভালো প্রোগ্রামাররা ডেটা লুকিয়ে রাখেন — struct-এর ভেতরটা header-এ না দেখিয়ে শুধু ফাংশন দেখান (অনেক C library এভাবে লেখা — যেমন SQLite-এর sqlite3 টাইপটি)। Global variable আর খোলা ডেটাই আসল শত্রু, ফাংশন নয়। C++ শুধু এই ভালো অভ্যাসকে ভাষার নিয়মে পরিণত করেছে, যাতে compiler সেটা জোর করে মানায়।
৬ · Abstraction ও interface — কী দেবে, কীভাবে দেবে নয়
আপনি রিকশায় ওঠেন, বলেন “ধানমন্ডি ২৭”. চালক কোন গলি দিয়ে যাবেন, কোথায় জ্যাম এড়াবেন, ব্যাটারি-রিকশা হলে মোটর কীভাবে চলে — এসব আপনার জানার দরকার নেই। আপনার আর চালকের মধ্যে চুক্তি সহজ: গন্তব্য দেবেন, ভাড়া দেবেন, পৌঁছে যাবেন। এই চুক্তিটাই interface. চালক যদি কাল প্যাডেল রিকশা বদলে ব্যাটারি রিকশা আনেন, আপনার চুক্তি বদলায় না।
এটাই abstraction-এর মূল কথা: ব্যবহারকারীকে শুধু দরকারি অংশটুকু দেখাও, বাকি জটিলতা ভেতরে লুকিয়ে রাখো। ATM আরও ভালো উদাহরণ:
| Interface — ATM যা প্রতিশ্রুতি দেয় | Implementation — ভেতরে যা ঘটে (আপনি দেখেন না) |
|---|---|
| কার্ড ঢোকান, PIN দিন | কার্ডের চিপ পড়া, PIN এনক্রিপ্ট করে ব্যাংকের সার্ভারে পাঠানো |
| টাকার পরিমাণ লিখুন | অ্যাকাউন্টের ব্যালান্স ও দৈনিক সীমা যাচাই, অন্য ব্যাংকের কার্ড হলে নেটওয়ার্কের মাধ্যমে অনুমতি নেওয়া |
| টাকা ও রসিদ নিন | কোন নোট কয়টা দেবে হিসাব, নোট গোনা, খতিয়ানে লেখা, ব্যর্থ হলে টাকা ফেরত |
ব্যাংক যদি ভেতরের সার্ভার বদলায়, নতুন নোট-গোনার যন্ত্র বসায় — আপনাকে কিছুই নতুন করে শিখতে হয় না। কারণ বাঁ দিকের কলামটা বদলায়নি। সফটওয়্যারেও ঠিক তাই: Wallet-এর withdraw(taka) প্রতিশ্রুতি একই রেখে ভেতরে ব্যালান্স long থেকে পয়সা-ভিত্তিক হিসাবে বদলান, ডেটাবেস বদলান, ফি যোগ করুন — Wallet ব্যবহারকারী বাকি সব কোড যেমন ছিল তেমনই চলবে।
Interface = প্রতিশ্রুতি (কী করা যায়, কী দিতে হয়, কী ফেরত আসে, কোন অবস্থায় “না” বলবে)। Implementation = কৌশল (কীভাবে করে)। ভালো নকশায় interface ধীরে বদলায়, implementation যত খুশি বদলায়। Encapsulation হলো এই দুইয়ের মধ্যে দেয়াল, আর abstraction হলো দেয়ালের সামনে কোন দরজাগুলো রাখবেন, সেই সিদ্ধান্ত।
১৯৭২ সালে David Parnas “On the Criteria To Be Used in Decomposing Systems into Modules” নামে একটি প্রবন্ধ লেখেন, যা আজও সফটওয়্যার প্রকৌশলের ক্লাসিক। তাঁর যুক্তি: প্রোগ্রামকে কাজের ধাপ ধরে ভাগ না করে ভাগ করুন সিদ্ধান্ত ধরে — যে সিদ্ধান্ত পরে বদলাতে পারে (ডেটা কীভাবে রাখা হবে, কোন ফরম্যাট), সেটাকে একটি মডিউলের ভেতরে লুকিয়ে রাখুন। একে বলা হয় information hiding. Encapsulation-এর পেছনের চিন্তা মূলত এটাই।
৭ · CRC কার্ড আর বাক্স-ছবি — কাগজে-কলমে নকশা
নকশা করতে দামি সফটওয়্যার লাগে না। ১৯৮৯ সালে Kent Beck ও Ward Cunningham OOPSLA সম্মেলনে একটি প্রবন্ধে দেখান, কীভাবে সাধারণ ইনডেক্স কার্ড দিয়ে object-ওরিয়েন্টেড চিন্তা শেখানো যায়। কার্ডের নাম CRC card — তিনটি অংশ:
- C — Class: ওপরে class-এর নাম।
- R — Responsibility: বাঁ দিকে, সে কী জানে আর কী করে (ক্রিয়াপদে, অল্প কথায়)।
- C — Collaborator: ডান দিকে, দায়িত্ব পালনে সে কাদের সাহায্য নেয়।
কার্ড ছোট — এটাই এর শক্তি। একটি কার্ডে দায়িত্ব না ধরলে বুঝবেন class-টা বেশি কাজ নিচ্ছে, ভাগ করতে হবে। নিচে আমাদের ওয়ালেটের কার্ডগুলো বানাই, তারপর একটি দৃশ্য “অভিনয়” করে যাচাই করি:
- প্রথম কার্ড — Wallet. ওপরে নাম, বাঁ দিকে দায়িত্ব: ব্যালান্স জানে, জমা নেয়, তোলে, আর সবচেয়ে বড় দায়িত্ব — invariant রক্ষা করে। দায়িত্ব লিখুন ক্রিয়াপদে, ডেটার তালিকা নয়।
- Collaborator যোগ। টাকা পাঠাতে গিয়ে Wallet একা পারে না — সে একটি Transaction তৈরি করে। তাই ডান দিকে লেখা হলো Transaction. Collaborator মানে “আমার দায়িত্ব পালনে যাকে বার্তা পাঠাতে হয়।”
- দ্বিতীয় কার্ড — Transaction. সে জানে কে, কাকে, কত টাকা। তার দায়িত্ব: দুই দিক (কাটা ও জমা) একসাথে সফল করা, নয়তো সব আগের অবস্থায় ফেরানো। সাহায্য নেয় Wallet (টাকা সরাতে) আর Ledger (লিখে রাখতে)।
- তৃতীয় কার্ড — Ledger. দায়িত্ব খুব সংকীর্ণ: লিখে রাখা, আর চাইলে মাসের স্টেটমেন্ট বানানো। সংকীর্ণ দায়িত্ব মানে উচ্চ cohesion — এটা ভালো লক্ষণ।
- দৃশ্য অভিনয় — “ফি কে কাটবে?” দলের তিনজন তিনটি কার্ড হাতে নিন, একজন বলুন “রিনা করিমকে ১০০০ পাঠাল, ১% ফি।” কার্ড হাতে থাকা মানুষটি জোরে বলবেন তিনি কী করবেন। দেখা যাবে প্রশ্ন ওঠে: ফি হিসাব কি Wallet-এর দায়িত্ব, না Transaction-এর? যেহেতু ফি লেনদেনের ধরনের ওপর নির্ভর করে (send, cash out, merchant), Transaction-এর কার্ডে লেখাই যুক্তিসঙ্গত — আর ফি কেটে নেওয়ার কাজটা তখনও Wallet-এর withdraw দিয়েই হবে। কার্ডে অভিনয় করেই নকশার ফাঁক ধরা পড়ল, এক লাইন কোড লেখার আগে।
বাক্স-ছবি — class diagram-এর সহজ রূপ
কার্ড দিয়ে আলোচনা শেষ হলে নকশা লিখে রাখতে একটি সহজ বাক্স-ছবি আঁকা হয়। এর প্রমিত রূপের নাম UML class diagram, কিন্তু শুরুতে নিয়মকানুন মানার দরকার নেই — তিন ঘরের বাক্সই যথেষ্ট: ওপরে নাম, মাঝে ডেটা (- মানে private), নিচে কাজ (+ মানে public).
ওয়ালেট ব্যবস্থার বাক্স-ছবি
A simple class diagram: name · data · operations
খেয়াল করুন, ছবিতে ডেটার সব ঘরে - চিহ্ন: বাইরের কেউ ছুঁতে পারবে না। বাইরের দুনিয়ার জন্য খোলা শুধু + চিহ্নের কাজগুলো — অর্থাৎ interface. কোনো setBalance() নেই, কারণ “যে কেউ ইচ্ছেমতো ব্যালান্স বসাবে” — এমন কাজ ব্যবসায় নেই।
৮ · সবকিছু object নয় — over-engineering-এর ফাঁদ, আর সামনের পথ
Object দিয়ে ভাবা শিখলে প্রথম কয়েক মাস সবকিছুতেই class দেখতে ইচ্ছে করে। এটা স্বাভাবিক, কিন্তু বিপজ্জনক। মনে রাখুন, C++ শুধু object-ওরিয়েন্টেড ভাষা নয় — এখানে সাধারণ ফাংশন, generic প্রোগ্রামিং আর functional ধারাও সমান গুরুত্বপূর্ণ। Stroustrup নিজেই C++-কে “multi-paradigm” ভাষা বলেন। কখন class লাগে না:
সাধারণ ডেটা → struct
একটি বিন্দুর x, y; একটি তারিখের দিন-মাস-বছর — এখানে রক্ষা করার মতো কোনো জটিল নিয়ম নেই। সব ডেটা খোলা রাখা একটি সাধারণ struct-ই যথেষ্ট (পর্ব ১২)।
হিসাব → সাধারণ ফাংশন
নম্বরের গড়, তালিকা সাজানো, ভ্যাট হিসাব — এগুলো কোনো object-এর নিজস্ব অবস্থা বদলায় না। average(marks) একটি free function হিসেবেই সবচেয়ে পরিষ্কার। C++-এর standard library-র অসংখ্য algorithm এভাবেই লেখা (পর্ব ৩৩)।
নিয়ম আছে → class
যেখানে ডেটার এমন সম্পর্ক আছে যা ভাঙা চলবে না (ব্যালান্স ≥ ০, সিট একবারই বিক্রি), সেখানেই class-এর আসল মূল্য। প্রশ্ন করুন: “এর invariant কী?” উত্তর না থাকলে হয়তো class দরকার নেই।
Over-engineering-এর লক্ষণ: একটি চায়ের দোকানের হিসাবের জন্য AbstractBeverageFactory, TeaManager, TeaManagerImpl, ITeaService — চারটি class, অথচ কাজ শুধু “কাপ গুনে দাম যোগ করা।” প্রতিটি ডেটার জন্য অন্ধভাবে getter/setter জোড়া — getBalance() আর setBalance() দুটোই থাকলে আসলে ডেটা খোলাই রইল, শুধু বেশি টাইপ করতে হলো। পাঁচ-ছয় স্তরের inheritance, যেখানে একটি বদল সব স্তরে ধাক্কা দেয়। নিয়ম: আজকের প্রয়োজনের জন্য নকশা করুন, কাল্পনিক ভবিষ্যতের জন্য নয়। সত্যিই প্রয়োজন এলে তখন ভাঙা ও বদলানো সহজ হয় ছোট, পরিষ্কার নকশাতেই।
আজকের চিন্তা, কোর্সের কোন পর্বে কোড হবে
এই পর্বের প্রতিটি ধারণা সামনে C++-এর নির্দিষ্ট ব্যাকরণ হয়ে ফিরে আসবে। এখন মুখস্থ করার দরকার নেই, শুধু মানচিত্রটা দেখে রাখুন:
| আজকের ধারণা | C++-এ যা হবে | কোথায় শিখবেন |
|---|---|---|
| সাধারণ ডেটার বাক্স | struct, enum class | পর্ব ১২ |
| Class, object, private ডেটা | class, public/private, member function | পর্ব ২০ |
| Object জন্মের সময় invariant ঠিক করা | constructor | পর্ব ২১ |
| is-a সম্পর্ক | inheritance | পর্ব ২৪ |
| একই বার্তা, ভিন্ন সাড়া | polymorphism, virtual | পর্ব ২৫ |
| শুধু প্রতিশ্রুতি, কোনো কৌশল নয় | abstract class ও interface | পর্ব ২৬ |
| has-a বনাম is-a, ভালো নকশার নীতি | composition, SOLID | পর্ব ২৭ |
বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:
- Class আর object-এর পার্থক্য কী? — Class হলো নকশা বা টাইপ; object হলো সেই নকশা থেকে তৈরি আসল জিনিস, যার নিজের ডেটা আছে।
- OOP-এর চারটি স্তম্ভ কী কী? — Encapsulation, abstraction, inheritance, polymorphism.
- Encapsulation আর abstraction কি একই? — না। Abstraction ঠিক করে কী দেখাবেন (interface); encapsulation সেই সিদ্ধান্তকে জোর করে মানায় (ডেটা লুকিয়ে, নিয়ম এক জায়গায় রেখে)।
- C++-এ struct আর class-এর পার্থক্য? — ভাষার দিক থেকে প্রায় একই; struct-এ সদস্যরা ডিফল্টভাবে public, class-এ private.
- has-a আর is-a-এর উদাহরণ দিন। — Car has an Engine (composition); Car is a Vehicle (inheritance).
৯ · AI-এর যুগে — AI-এর বানানো class ডিজাইন যাচাই
AI-কে “একটা মোবাইল ওয়ালেটের class ডিজাইন দাও” বললে কয়েক সেকেন্ডে দশ-বারোটি class, প্রতিটিতে getter-setter, কয়েক স্তরের inheritance আর গোটা কয়েক “Manager” পাবেন। দেখতে খুব পেশাদার লাগে। কিন্তু বেশিরভাগ সময় এটা ওজনদার, অথচ সবচেয়ে গুরুত্বপূর্ণ জিনিস — invariant — প্রায়ই অরক্ষিত থাকে। কারণটা বোঝা সহজ: AI শিখেছে লাখো টিউটোরিয়াল আর পুরোনো এন্টারপ্রাইজ কোড থেকে, যেখানে এই ধরনটাই বেশি। সে জানে না আপনার ব্যবসায় কোন নিয়মটা সবচেয়ে দামি।
AI-এর class ডিজাইনে যেসব ভুল প্রায়ই দেখা যায়
| AI যা দেয় | সমস্যা | যা চাইবেন |
|---|---|---|
প্রতিটি ডেটার জন্য getX()/setX() | setBalance(-500) ডাকা যায় — invariant শেষ | ব্যবসার কাজ: deposit, withdraw |
WalletManager, TransactionHandler | “Manager” সব কাজ নিজে করে, Wallet শুধু ডেটার বাক্স — আসলে procedural কোড class-এর পোশাকে | যার ডেটা, কাজও তার |
Account → BankAccount → SavingsWallet → PremiumWallet | গভীর inheritance; ওপরের একটি বদল নিচের সবাইকে ভাঙে | এক-দুই স্তর, বা composition |
টাকার জন্য double | দশমিকের ছোট গরমিল জমে বড় হিসাব-ভুল | পয়সায় পূর্ণসংখ্যা (long long) |
| “ভবিষ্যতের জন্য” interface-এর পর interface | একটিমাত্র বাস্তবায়নের জন্য বাড়তি স্তর | প্রয়োজন এলে তখন যোগ |
AI-কে ডিজাইন চাইলে আগে নিজের ভাবনাটা দিন, আর সীমা বেঁধে দিন। এতে AI আপনার সহকারী হয়, আপনার বদলে ভাবার যন্ত্র নয়:
AI-এর ডিজাইন হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভালো নকশা ছোট হয়, আর নিয়ম ভাঙতে দেয় না।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১“Object-oriented” শব্দটি যিনি তৈরি করেছিলেন, তিনি কেন বলেছিলেন তাঁর মাথায় C++ ছিল না? OOP আসলে কী — বার্তা পাঠানো, না class?
Alan Kay ১৯৬০-এর দশকের শেষে “object-oriented” শব্দটি ব্যবহার শুরু করেন, আর ১৯৭০-এর দশকে Xerox PARC-এ তাঁর দল তৈরি করে Smalltalk. ১৯৯৭ সালের OOPSLA সম্মেলনের বক্তৃতায় তিনি বলেন, তিনিই শব্দটা বানিয়েছিলেন, আর তাঁর মাথায় C++ ছিল না। পরে এক চিঠিতে তিনি নিজের সংজ্ঞা দেন — তাঁর কাছে OOP মানে মূলত তিনটি জিনিস: বার্তা পাঠানো (messaging), প্রতিটি object-এর নিজের অবস্থা লুকিয়ে রাখা, আর যতটা সম্ভব দেরিতে সিদ্ধান্ত নেওয়া কোন কোড চলবে (late binding).
Kay-এর ছবিটা ছিল জীববিজ্ঞান থেকে নেওয়া: প্রতিটি object যেন একটি কোষ বা নেটওয়ার্কের একটি কম্পিউটার — ভেতরে কী আছে কেউ জানে না, শুধু বার্তা আদান-প্রদান হয়। Smalltalk-এ সংখ্যা ৩ পর্যন্ত একটি object; 3 + 4 মানে ৩-কে “+ 4” বার্তা পাঠানো।
C++ এসেছে অন্য ঐতিহ্য থেকে — Simula, যেখানে মূল ধারণা ছিল class আর টাইপ। Stroustrup-এর লক্ষ্য ছিল C-এর গতি রেখে নিজের টাইপ বানানো, আর “যা ব্যবহার করবেন না, তার দাম দেবেন না” (zero-overhead). তাই C++-এ member function ডাকা সাধারণত compile-এর সময়েই নির্দিষ্ট হয়ে যায়, প্রায় সাধারণ ফাংশনের মতো দ্রুত; late binding চাইলে virtual লিখে আলাদা করে চাইতে হয় (পর্ব ২৫)।
মজার ব্যাপার, Kay-এর চিন্তা আজ অন্য জায়গায় বেঁচে আছে: Erlang-এর process, microservice যারা নেটওয়ার্কে বার্তা পাঠায়, actor model — এগুলো Kay-এর “object”-এর অনেক কাছাকাছি।
মূল উপলব্ধি: OOP একটি একক জিনিস নয়, দুটি ঐতিহ্য — Kay-এর “বার্তা পাঠানো স্বাধীন চরিত্র” আর Simula/C++-এর “নিয়ম রক্ষা করা টাইপ”। এই পর্বে আমরা দুটোই নিয়েছি: বার্তা পাঠানোর চিন্তা (অংশ ৪) আর invariant রক্ষা করা টাইপ (অংশ ৫)।
প্র ০২১৯৯০-এর দশকে OOP-কে কেন সব সমস্যার সমাধান ভাবা হতো? পরে কেন এর বিরুদ্ধে এত সমালোচনা উঠল?
১৯৯০-এর দশকে সফটওয়্যার হঠাৎ বিশাল হতে থাকে — উইন্ডো, বোতাম, মেনুসহ graphical অ্যাপ্লিকেশন, ব্যাংক ও এয়ারলাইনের বড় সিস্টেম। GUI-র জগতে object দারুণ মানিয়ে যায়: প্রতিটি বোতাম, প্রতিটি উইন্ডো সত্যিই একটি “বস্তু” যার অবস্থা আছে। একই সময়ে আসে কয়েকটি বড় ঘটনা: ১৯৯৪ সালে “Gang of Four”-এর Design Patterns বই; ১৯৯৫ সালে Java, যেখানে সবকিছু class-এর ভেতরে লিখতে হয়; ১৯৯৭ সালে OMG-এর মাধ্যমে UML প্রমিত হয়। কোম্পানিগুলো প্রতিশ্রুতি পেল — “object বানাও, reuse করো, খরচ কমবে।”
বাস্তবে কিছু প্রতিশ্রুতি পূরণ হয়নি। “Reuse” — অর্থাৎ এক প্রজেক্টের class অন্য প্রজেক্টে লাগানো — আশার চেয়ে অনেক কম সফল হলো, কারণ প্রতিটি ব্যবসার নিয়ম আলাদা। গভীর inheritance গাছ বদলানো কঠিন হয়ে পড়ল। Erlang-এর স্রষ্টা Joe Armstrong-এর বিখ্যাত রসিকতা: আপনি চেয়েছিলেন একটা কলা, পেলেন কলা হাতে একটা গরিলা আর তার সাথে পুরো জঙ্গল — অর্থাৎ একটি class নিলে তার সব নির্ভরতা টেনে আনতে হয়। ২০০৬ সালে Steve Yegge-এর প্রবন্ধ “Execution in the Kingdom of Nouns” ব্যঙ্গ করল, Java-য় প্রতিটি ক্রিয়াকেও একটা বিশেষ্য (class) বানাতে হয়।
এই সমালোচনার ফলেই আজকের ভারসাম্যপূর্ণ ধারণা এসেছে: “inheritance-এর চেয়ে composition পছন্দ করুন” (Design Patterns বইতেই এই কথা ছিল!), ছোট class, আর প্রয়োজনমতো সাধারণ ফাংশন। Go-র মতো নতুন ভাষায় class inheritance-ই রাখা হয়নি।
মূল উপলব্ধি: প্রতিটি নতুন কৌশল প্রথমে অতিরঞ্জিত হয়, তারপর সমালোচিত হয়, শেষে তার সঠিক জায়গা খুঁজে পায়। আজ AI নিয়েও একই চক্র চলছে। প্রকৌশলীর কাজ হলো হাইপ নয়, trade-off বোঝা।
প্র ০৩গেম ইঞ্জিনের প্রোগ্রামাররা কেন অনেক সময় “object দিয়ে ভাবা” ছেড়ে “ডেটা দিয়ে ভাবা”-য় যান?
একটি গেমে ধরুন দশ হাজার সৈন্য, প্রতি সেকেন্ডে ৬০ বার সবার অবস্থান হালনাগাদ করতে হয়। Object-এর স্বাভাবিক নকশা: প্রতিটি সৈন্য একটি object, যার ভেতরে নাম, ছবি, স্বাস্থ্য, অস্ত্র, অবস্থান, গতি — সব একসাথে। অবস্থান হালনাগাদ করতে CPU প্রতিটি সৈন্যের পুরো object মেমরি থেকে আনে, অথচ লাগে শুধু অবস্থান আর গতি।
সমস্যাটা হলো আধুনিক CPU-র গতি আর মেমরির গতির বিশাল ব্যবধান। CPU মেমরি থেকে ডেটা আনে ছোট ছোট টুকরোয় (cache line); দরকারি ডেটা পাশাপাশি থাকলে এক টুকরোতেই অনেকটা কাজ হয়, ছড়িয়ে থাকলে প্রতিবার অপেক্ষা। Data-oriented design তাই প্রশ্ন বদলে দেয়: “কোন কাজে কোন ডেটা একসাথে লাগে?” উত্তর অনুযায়ী সব সৈন্যের অবস্থান এক সারিতে, সব গতি আরেক সারিতে রাখা হয় (একে বলে “struct of arrays”, object-এর সারি বা “array of structs”-এর বদলে)।
২০১৪ সালের CppCon-এ Mike Acton-এর বক্তৃতা “Data-Oriented Design and C++” এই চিন্তাকে ব্যাপকভাবে পরিচিত করে। Unity-র DOTS এবং অনেক ইঞ্জিনের Entity Component System (ECS) এই ধারায় তৈরি — entity শুধু একটি নম্বর, ডেটা থাকে component-এর সারিতে, আর “system” নামের ফাংশন পুরো সারির ওপর কাজ করে।
তবে এর মানে এই নয় যে object খারাপ। গেমের মেনু, সেটিংস, নেটওয়ার্ক সংযোগ — যেখানে সংখ্যায় কম কিন্তু নিয়ম জটিল — সেখানে object-ই সুবিধাজনক। C++-এর শক্তি হলো এক প্রোগ্রামেই দুই ধারা মেশানো যায়। পর্ব ৪৯-এ পারফরম্যান্স মাপার সময় এই ধারণা আবার আসবে।
মূল উপলব্ধি: নকশা হয় সমস্যার আকার অনুযায়ী। নিয়ম জটিল হলে object, ডেটা বিশাল আর কাজ একঘেয়ে হলে ডেটা-কেন্দ্রিক নকশা। আর মেশিন কীভাবে মেমরি পড়ে, সেটা জানা থাকলে এই সিদ্ধান্ত অনেক সহজ হয়।
প্র ০৪Class-এর নাম ঠিক করা কেন এত কঠিন, আর “domain-driven design” এই সমস্যার কী সমাধান দেয়?
একটি পুরোনো রসিকতা আছে: কম্পিউটার বিজ্ঞানে দুটি কঠিন কাজ — cache বাতিল করা আর জিনিসের নাম দেওয়া। নাম কঠিন, কারণ নাম আসলে নকশার সিদ্ধান্ত। “Wallet” আর “Account” — দুটো একই জিনিস? ব্যাংকের লোকের কাছে account মানে হয়তো আইনি একটি চুক্তি, আর wallet মানে অ্যাপের ভেতরের একটি জিনিস। প্রোগ্রামার একটা বোঝেন, ব্যবসার মানুষ আরেকটা — আর এই ভুল-বোঝাবুঝিই বড় প্রজেক্টের অনেক বাগের উৎস।
২০০৩ সালে Eric Evans তাঁর বই Domain-Driven Design-এ এর একটি পদ্ধতিগত সমাধান দেন। কয়েকটি মূল ধারণা:
- Ubiquitous language (সবার এক ভাষা): ব্যবসার মানুষ যে শব্দ বলেন, কোডে ঠিক সেই নাম। এজেন্টরা “cash in” বললে কোডে
cashIn(), “topUp” বা “credit” নয়। - Bounded context: একই শব্দ ভিন্ন বিভাগে ভিন্ন অর্থ বহন করতে পারে — হিসাব বিভাগের “Customer” আর মার্কেটিং বিভাগের “Customer” আলাদা মডেল হতে পারে। সবকিছুর জন্য একটি বিশাল “Customer” class বানানো ভুল।
- Entity বনাম value object: রিনার ওয়ালেট একটি entity — তার পরিচয় আছে, সময়ের সাথে বদলায়। কিন্তু “৳৫০০” একটি value — দুটো ৳৫০০ সম্পূর্ণ সমান, আলাদা পরিচয় নেই।
- Aggregate: কয়েকটি object-এর একটি দল, যার invariant একটি “মূল” object পাহারা দেয় — যেমন একটি অর্ডার আর তার লাইন-আইটেম।
অংশ ৩-এ আমরা প্রোডাক্ট ম্যানেজারের বিবরণ থেকে নাম তুলেছিলাম — সেটাই DDD-র সরল রূপ।
মূল উপলব্ধি: ভালো class-এর নাম কোডের চেয়ে ব্যবসাকে বেশি প্রতিফলিত করে। কোড পড়ে একজন ব্যবসার মানুষ যদি বলতে পারেন “হ্যাঁ, আমরা এটাই করি” — নকশা সঠিক পথে আছে। Domain-driven design বড় প্রজেক্টের জন্য, কিন্তু “ব্যবসার ভাষায় নাম দিন” অভ্যাসটা ছোট প্রজেক্টেও কাজের।
প্র ০৫কখন object-এর চেয়ে functional ধারা (অপরিবর্তনীয় ডেটা আর বিশুদ্ধ ফাংশন) ভালো কাজ করে?
Object-এর মূল চিন্তা হলো পরিবর্তনশীল অবস্থা — ওয়ালেটের ব্যালান্স বদলায়, আর object সেই বদল নিয়ন্ত্রণ করে। Functional ধারার চিন্তা উল্টো: ডেটা বদলাবেন না, পুরোনো ডেটা থেকে নতুন ডেটা তৈরি করুন; আর ফাংশন হবে “বিশুদ্ধ” — একই ইনপুটে সবসময় একই আউটপুট, বাইরের কিছু বদলায় না।
কোথায় এটা জেতে? হিসাব আর রূপান্তরে। মাসের স্টেটমেন্ট বানানো ভাবুন: লেনদেনের তালিকা নাও → এই মাসেরগুলো ছাঁকো → ধরন অনুযায়ী ভাগ করো → যোগফল বের করো। এখানে কোনো object-এর অবস্থা বদলানোর দরকার নেই; একটি পাইপলাইন যথেষ্ট, আর প্রতিটি ধাপ আলাদাভাবে পরীক্ষা করা যায়। একসাথে অনেক thread চললে সুবিধা আরও বড়: যে ডেটা বদলায় না, তাকে দুই thread একসাথে পড়লেও কোনো গোলমাল হয় না — data race-এর সম্ভাবনাই থাকে না (পর্ব ৪৫)।
একটি বিখ্যাত তাত্ত্বিক দিক আছে, যাকে Philip Wadler নাম দিয়েছিলেন “expression problem”: object-এর ধারায় নতুন ধরন যোগ করা সহজ (নতুন একটি ওয়ালেট class), কিন্তু নতুন কাজ যোগ করতে সব class বদলাতে হয়। Functional ধারায় উল্টো — নতুন কাজ (নতুন একটি ফাংশন) সহজ, নতুন ধরন যোগ করলে সব ফাংশন বদলাতে হয়। কোন দিকে পরিবর্তন বেশি আসবে, সেটা দেখে ধারা বাছাই করা যায়।
আধুনিক C++ দুটোই দেয়: lambda (C++11 থেকে, পর্ব ৩৪), ranges পাইপলাইন (C++20 থেকে, পর্ব ৩৫), const দিয়ে অপরিবর্তনীয় ডেটা। STL-এর স্থপতি Alexander Stepanov-এর চিন্তাও ছিল algorithm-কে ডেটার ধরন থেকে আলাদা রাখা — তাই std::sort কোনো class-এর member নয়, একটি সাধারণ ফাংশন।
মূল উপলব্ধি: নিয়ম রক্ষা করতে হয় এমন দীর্ঘজীবী অবস্থায় object; ডেটার রূপান্তর আর হিসাবে বিশুদ্ধ ফাংশন। ওয়ালেট object হবে, স্টেটমেন্ট হবে ফাংশনের পাইপলাইন — একই প্রোগ্রামে, একসাথে।
অনুশীলন
খাতা-কলম নিন। এগুলোর বেশিরভাগে কোড লিখতে হবে না — ভাবা আর নকশা করাই আসল কাজ।
-
বাস কাউন্টারের নকশা: বিবরণটি পড়ুন — “যাত্রী কাউন্টারে গিয়ে একটি ট্রিপের জন্য সিট বুক করেন। প্রতিটি বাসে ৪০টি সিট। একটি সিট একটি ট্রিপে একবারই বিক্রি হবে। কাউন্টার মাস্টার টিকিট প্রিন্ট করেন।” বিশেষ্য ও ক্রিয়া দাগিয়ে class, দায়িত্ব আর অন্তত দুটি invariant বের করুন।
বিশেষ্য: যাত্রী, কাউন্টার, ট্রিপ, সিট, বাস, কাউন্টার মাস্টার, টিকিট। ক্রিয়া: বুক করা, বিক্রি করা, প্রিন্ট করা।
সম্ভাব্য class:
Trip(কোন বাস, কবে, কোথায়; কোন সিট বিক্রি হয়েছে জানে;book(seat, passenger)),Ticket(যাত্রী, ট্রিপ, সিট নম্বর, দাম),Passenger(নাম, ফোন),Counter(বুকিং করায়, টিকিট প্রিন্ট করে)।Busহয়তো শুধু নম্বর আর সিট সংখ্যা — সাধারণ struct-ই যথেষ্ট।লক্ষ করুন: সিটের অবস্থা Bus-এর নয়, Trip-এর — একই বাস সকালে ঢাকা→রাজশাহী, রাতে উল্টো পথে যায়, আর প্রতিটি ট্রিপে সিট নতুন করে বিক্রি হয়।
Invariant: (১) একটি ট্রিপে একটি সিট সর্বোচ্চ একজনের নামে; (২) সিট নম্বর ১ থেকে ৪০-এর মধ্যে; (৩) বিক্রি হওয়া সিটের সংখ্যা কখনো ৪০-এর বেশি নয়। তিনটিই পাহারা দেবে
Trip::book(). -
লাইব্রেরির সম্পর্ক: নিচের প্রতিটি জোড়ার সম্পর্ক has-a, is-a নাকি uses, বলুন — (ক) Library ও Book (খ) EBook ও Book (গ) Member ও Book (বই ধার নেওয়ার সময়) (ঘ) Loan ও Member
(ক) has-a — লাইব্রেরির অনেক বই আছে। (খ) is-a — ই-বুক এক ধরনের বই (তবে সাবধান: ই-বুক “ধার” দিলে কপি কমে না — নিয়ম আলাদা হলে is-a ঠিক কি না আবার ভাবুন)। (গ) uses — সদস্য বই ব্যবহার করেন, বই সদস্যের অংশ নয়। (ঘ) has-a — একটি ধারের (Loan) রেকর্ডে একজন সদস্য, একটি বই আর ফেরতের তারিখ থাকে। খেয়াল করুন, “Loan” বিবরণে লুকানো একটি class — “ধার নেওয়া” ক্রিয়াটাই একটি বিশেষ্য হয়ে উঠেছে, কারণ তার নিজের তথ্য (তারিখ, জরিমানা) আছে।
-
ফি যোগ করুন: উপরের
wallet_object.cppকোডটি পাতাতেই বদলে চালান —withdraw-এ প্রতিটি তোলার সাথে ৫ টাকা ফি কাটুন (মোট কাটা = taka + 5), আর নিশ্চিত করুন ফিসহ টাকা না থাকলে তোলা প্রত্যাখ্যাত হয়। কয়টি লাইন বদলাতে হলো?mainকি বদলাতে হলো?শুধু
withdraw-এর ভেতরে হাত দিতে হয় — একটি লাইন যোগ, দুটি লাইন বদল:bool withdraw(long taka) { const long total = taka + 5; // amount + fee if (taka <= 0 || total > balance_) return false; // invariant still safe balance_ -= total; return true; }এখন ৫০০ জমা, ২০০ তোলা হলে ব্যালান্স হবে ২৯৫.
mainএকটুও বদলাতে হয়নি — Wallet ব্যবহারকারী কোড জানেই না ভেতরে ফি এসেছে। এটাই encapsulation-এর লাভ: পরিবর্তন এক জায়গায় আটকে থাকে। আসল ব্যবস্থায় ফি-টাও আলাদা হিসাবে (Ledger-এ) লিখতে হতো। -
AI অডিট: যেকোনো AI-কে বলুন, “একটি চায়ের দোকানের হিসাব রাখার সফটওয়্যারের class ডিজাইন দাও।” তারপর অংশ ৯-এর চেকলিস্ট দিয়ে যাচাই করুন। কয়টি class পেলেন, কয়টি সত্যিই দরকার?
সাধারণত দেখা যায়:
Shop,Product,Tea,Biscuit(Product-এর subclass),Order,OrderItem,Customer,Inventory,InventoryManager,PaymentService— আর প্রতিটিতে getter-setter.একটি ছোট চায়ের দোকানের আসল দরকার হয়তো:
Item(নাম, দাম — সাধারণ struct; চা আর বিস্কুটের জন্য আলাদা class নয়, কারণ আচরণ একই),Sale(কোন জিনিস কয়টা, মোট দাম; invariant: পরিমাণ ≥ ১), আরDailyBook(দিনের বিক্রি লিখে রাখে, মোট হিসাব দেয়)। তিনটি।Tea is-a Productinheritance অপ্রয়োজনীয় — পার্থক্য শুধু ডেটায় (নাম ও দাম), আচরণে নয়। ডেটার পার্থক্যের জন্য inheritance নয়, আলাদা মান যথেষ্ট।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমসমস্যার বিবরণ থেকে object নকশায় যাওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📄 সাধারণ ভাষায় সমস্যার বিবরণ সংগ্রহ করা
- 🖍️ বিশেষ্য দাগিয়ে সম্ভাব্য class-এর তালিকা বানানো
- ✂️ তালিকা ছেঁকে আসল class আর অন্যদের ডেটা আলাদা করা
- 🎯 ক্রিয়া থেকে প্রতিটি class-এর দায়িত্ব ও invariant ঠিক করা
- 🃏 CRC কার্ড হাতে দৃশ্য অভিনয় করে নকশা যাচাই করা
Object-ওরিয়েন্টেড চিন্তায় একটি object-কে সবচেয়ে ভালোভাবে বর্ণনা করে কোনটি?
- শুধু কিছু ডেটার একটি বাক্স
- অবস্থা (ডেটা) আর আচরণ (কাজ) একসাথে, যে নিজের ডেটার নিয়ম নিজেই রক্ষা করে
- কয়েকটি ফাংশনের একটি তালিকা
- একটি class-এর আরেকটি নাম
একটি ওয়ালেট সিস্টেমে “ব্যালান্স কখনো শূন্যের নিচে নামবে না” — নকশার ভাষায় এই নিয়মকে কী বলে?
- Interface
- Inheritance
- Invariant
- Collaborator
“একটি গাড়ির একটি ইঞ্জিন আছে” — Car আর Engine-এর সম্পর্ক কোনটি?
- has-a (composition)
- is-a (inheritance)
- uses (association), কোনো অংশ নয়
- কোনো সম্পর্ক নেই
একটি প্রোগ্রামে ব্যালান্স সবার জন্য খোলা, আর ২০টি ফাংশন নিজে নিজে ব্যালান্স বদলায়। নতুন নিয়ম এলে সবচেয়ে বড় ঝুঁকি কী?
- প্রোগ্রাম অনেক ধীর হয়ে যাবে
- Compiler error দেবে
- ফাংশন ব্যবহার করাই ভুল, সব মুছতে হবে
- কোনো একটি জায়গা বদলাতে ভুলে গেলে নীরবে ভুল হিসাব হবে
CRC কার্ডের তিনটি অংশ কী কী?
- Code, Run, Compile
- Class, Responsibility, Collaborator
- Constructor, Return, Copy
- Class, Range, Count
একটি গ্রাফে বিন্দু রাখতে শুধু দুটি সংখ্যা x আর y লাগে, কোনো বিশেষ নিয়ম নেই। C++-এ সবচেয়ে উপযুক্ত নকশা কোনটি?
- private x, y আর প্রতিটির জন্য getter ও setter সহ একটি class
- AbstractShape থেকে inheritance করা Point class
- x ও y খোলা রাখা একটি সাধারণ struct
- দুটি global variable
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Object = অবস্থা + আচরণ; class হলো নকশা, object আসল জিনিস।
- বিবরণের বিশেষ্য → class, ক্রিয়া → দায়িত্ব, “কখনো না” লাইন → invariant; সম্পর্ক তিন রকম: has-a, is-a, uses.
- Object-রা ডেটায় হাত দেয় না, বার্তা পাঠায়; যার ডেটা, নিয়ম রক্ষার দায়ও তার (encapsulation).
- Interface = প্রতিশ্রুতি, implementation = কৌশল; প্রতিশ্রুতি স্থির রেখে কৌশল যত খুশি বদলানো যায়।
- নিয়ম থাকলে class, সাধারণ ডেটায় struct, হিসাবে সাধারণ ফাংশন; AI-এর নকশা দায়িত্ব, invariant আর পরিবর্তনের খরচ দিয়ে যাচাই করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৩ · টুলচেইন — g++, compile আর linkপরবর্তী পর্বC++ কোড থেকে প্রোগ্রাম তৈরির ধাপগুলো আর error চেনার কৌশল।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C কোর্স · কম্পিউটেশনাল থিংকিংভিত্তিDecomposition, অ্যালগরিদম, edge case আর loop invariant — বিস্তারিত।
- Software Engineering & Gitপাশের ট্র্যাকদলে কাজ, কোড রিভিউ আর বড় প্রজেক্ট সামলানোর পেশাদার অভ্যাস।
- System Designগভীরে যানএকটি ওয়ালেট বা টিকিটিং সিস্টেম যখন লাখো মানুষ একসাথে ব্যবহার করেন — তখনকার নকশা।