সরাসরি পর্বে যান
পর্ব ১৯ · ৫৫-এর মধ্যে · মডিউল ৩

মাঝপথের প্রজেক্ট — দোকানের হিসাবের একটি CLI টুল

Halfway Project — A Shop Inventory & Sales CLI Tool in Modern C++

Taught in Bangla, this project lesson builds a command-line inventory tool in C++: defining requirements and scope, designing the data model, parsing and validating each input line, a single-file MVP, splitting into headers and layers, milestones, and scripted testing.

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

  • একটি ফাঁকা পাতা থেকে চাহিদা, কমান্ডের ভাষা আর MVP-র সীমারেখা লেখা
  • struct Item, enum class Command আর std::vector দিয়ে ডেটা মডেল, আর প্রতিটি সিদ্ধান্তের কারণ
  • একটি কমান্ড-লাইনকে ধাপে ধাপে প্রক্রিয়া করা, আর এক ফাইলের চালানো যায় এমন MVP
  • item.h, ledger.h/.cpp, main.cpp — namespace আর header দিয়ে আসল প্রজেক্টের ফাইল-কাঠামো
  • মাইলস্টোনে বানানো, স্ক্রিপ্ট করা ইনপুটে টেস্ট, একটি বাগ শিকার, আর AI-কে pair-programmer হিসেবে ব্যবহার

১ · কেন একটি প্রজেক্ট — আর ইঞ্জিনিয়ার কীভাবে শুরু করেন

আঠারোটি পর্বে আপনি অনেক যন্ত্র হাতে নিয়েছেন: std::vector, std::string, struct, enum class, ফাংশন, reference, namespace, header, আর মেমরির মালিকানা ও lifetime. কিন্তু যন্ত্র চেনা আর মিলিয়ে কিছু বানানো দুটি আলাদা দক্ষতা — হাতুড়ি আর করাত চিনলেই কেউ আলমারি বানাতে পারেন না।

এই পর্বে আমরা একটি সত্যিকারের ছোট টুল বানাব। মোহাম্মদপুরের টাউন হল বাজারে শাহীন ভাইয়ের একটি মুদি দোকান। প্রতিদিন সকালে মাল আসে, সারাদিন বিক্রি হয়, আর রাতে তিনি জানতে চান — কোন মাল কতটা বাকি, আজ মোট কত টাকা বিক্রি হলো। খাতায় লিখতে লিখতে প্রায়ই হিসাব মেলে না: কখনো লেখা হয় ২০ কেজি চাল বিক্রি, অথচ দোকানে ছিল ১৫ কেজি!

আমাদের প্রোগ্রামটি হবে একটি CLI টুল — terminal-এ চলে, প্রতিটি লাইনে একটি কমান্ড পড়ে। শেষ পর্যন্ত এর সাথে কথোপকথন দেখতে হবে এমন:

> add rice 50 72.50
ok: rice +50
> sell rice 3
ok: rice -3
> sell rice 80
error: only 47 rice left
> report
ITEM      QTY      PRICE
rice       47      72.50
sales today: 217.50 Tk
> quit
মূল ধারণা

এই পর্বের আসল বিষয় দোকানের প্রোগ্রাম নয়। আসল বিষয় হলো একজন ইঞ্জিনিয়ার কীভাবে একটি অস্পষ্ট ইচ্ছা (“হিসাব মেলে না”) থেকে প্রমাণসহ কাজ করা সফটওয়্যারে পৌঁছান। পদ্ধতিটি একই থাকে — আপনি দোকানের হিসাব বানান, ব্যাংকের লেনদেন বা হাসপাতালের রোগীর তালিকা।

অভিজ্ঞ ইঞ্জিনিয়ার সরাসরি int main() লিখতে বসেন না। পাঁচটি ধাপের প্রতিটিতে একটি লিখিত জিনিস তৈরি হয়, আর পরের ধাপ দাঁড়ায় আগেরটির ওপর। “পরে” চেপে দেখুন:

  1. চাহিদাspec.txtকী চাই, কেন চাই
  2. ডেটা মডেলitem.hকী তথ্য রাখব
  3. অংশে ভাগledger.hকে কোন কাজ করবে
  4. মাইলস্টোনM1 → M5ছোট ধাপে বানানো
  5. টেস্টtests/প্রমাণ করা
spec.txtশাহীন ভাইয়ের সাথে কথা বলে লেখা
add  <item> <qty> <price>   মাল এল — স্টকে যোগ
sell <item> <qty>           বিক্রি — স্টক কমবে, বিক্রির মোট বাড়বে
report                     সব মালের স্টক আর আজকের মোট বিক্রি
quit                       বের হওয়া
নিয়ম: যতটা আছে তার বেশি বিক্রি করা যাবে না
এখন নয়: ফাইলে সংরক্ষণ, বাকির খাতা, মাসিক রিপোর্ট
item.hspec-এর বিশেষ্যগুলো থেকে টাইপ
struct Item {
    std::string name;          // "rice"
    int         qty{};         // স্টকে কতটা, কখনো ঋণাত্মক নয়
    long long   price_paisa{}; // 72.50 Tk = 7250
};
enum class Command { Add, Sell, Report, Quit, Unknown };
সিদ্ধান্ত: টাকা পয়সায় পূর্ণসংখ্যা, double নয়
ledger.hকোড নয় — শুধু ফাংশনের “চুক্তি”
namespace shop {
  Result add (Ledger& lg, const std::string& name,
              int qty, long long price_paisa);
  Result sell(Ledger& lg, const std::string& name, int qty);
}
// Result = Ok | BadInput | NotFound | NotEnough
নিয়ম: shop:: এর কোনো ফাংশন কিছু ছাপে না
plan.txtপ্রতিটি মাইলস্টোনের শেষে প্রোগ্রাম চলে
[x] M1  লুপ ঘোরে, quit আর অজানা কমান্ড বোঝে
[x] M2  add আর report — প্রথম আসল কাজ
[ ] M3  sell আর সব ইনপুট যাচাই   ← এখানে MVP পূর্ণ
[ ] M4  ফাইলে ভাগ, std::format-এ সুন্দর রিপোর্ট
[ ] M5  ফাইলে save/load (পর্ব ৪৪-এর পরে)
tests/আগে লেখা ইনপুট আর প্রত্যাশিত আউটপুট
T1  খালি দোকানে report          → sales today: 0.00 Tk   PASS
T2  sell rice abc                → usage বার্তা           PASS
T3  স্টকের বেশি বিক্রি           → only 47 rice left      PASS
T4  অজানা কমান্ড "dance"         → unknown command        PASS
T5  ইনপুট হঠাৎ শেষ (EOF)        → শান্তভাবে বের হয়       PASS
  1. ধাপ ১ — চাহিদা। শাহীন ভাইয়ের আসল সমস্যা শুনে লেখা হলো ব্যবহারকারীর ভাষায়, কোডের ভাষায় নয়। সাথে লেখা হলো কী এখন বানানো হবে না — এটাও চাহিদার অংশ।
  2. ধাপ ২ — ডেটা মডেল। spec-এর বিশেষ্য (মাল, পরিমাণ, দাম, কমান্ড) থেকে টাইপ ঠিক হলো। পর্ব ১২-এর struct আর enum class এখানেই কাজে লাগল।
  3. ধাপ ৩ — অংশে ভাগ। প্রতিটি কাজের জন্য একটি ফাংশন, যার ইনপুট আর ফল আগেই লেখা — একটি interface. ভেতরে কী আছে না জেনেও যে কেউ (আপনি, বন্ধু বা AI) এটা দেখে কাজ করতে পারে।
  4. ধাপ ৪ — ছোট ধাপে বানানো। পুরো প্রোগ্রাম একবারে নয়। পাঁচটি milestone, আর প্রতিটির শেষে প্রোগ্রাম compile হয়ে চলে। এখানে দুটি শেষ, তিনটি বাকি।
  5. ধাপ ৫ — টেস্ট। কোন ইনপুটে কী ছাপা উচিত, তা আগেই লেখা ছিল; এখন চালিয়ে মিলিয়ে দেখা। “চলছে মনে হয়” নয় — “প্রমাণসহ চলে।”

২ · চাহিদা — কমান্ডের ভাষা আর “কী বানাব না”

নতুনদের সবচেয়ে সাধারণ ভুল হলো সমস্যা না বুঝেই কোড লিখতে বসা। দর্জির কথা ভাবুন — মাপ না নিয়ে জামা সেলাই করলে যত সুন্দর সেলাই-ই হোক, গায়ে লাগবে না। সফটওয়্যারে “মাপ নেওয়া”-র নাম requirement বা চাহিদা লেখা। চাহিদা লেখার একটি জনপ্রিয় ছাঁচ হলো user story:

“(কে) হিসেবে আমি (কী) চাই, যাতে (কেন)।”

“কে” মনে করিয়ে দেয় প্রোগ্রামটি কার জন্য (শাহীন ভাই প্রোগ্রামার নন, তাই বার্তা হবে পরিষ্কার), আর “কেন” বলে আসল উদ্দেশ্য। প্রতিটি story-র সাথে লিখুন কিছু acceptance criteria — কখন বলবেন কাজটি “হয়েছে”। এগুলো যাচাই করা যায় এমন হতে হবে: “সহজে ব্যবহার করা যাবে” যাচাই করা যায় না; “স্টকের বেশি বিক্রি চাইলে কিছু না বদলে বাকি পরিমাণ জানাবে” যাচাই করা যায়।

কমান্ডUser story (দোকানদার হিসেবে আমি…)কখন “হয়েছে” বলব
add rice 50 72.50নতুন মাল এলে স্টকে যোগ করতে চাই, যাতে কতটা আছে জানিনতুন মাল হলে তালিকায় আসে; আগে থাকলে পরিমাণ বাড়ে; পরিমাণ বা দাম ০, ঋণাত্মক বা অক্ষর হলে কিছুই বদলায় না, বার্তা দেখায়
sell rice 3বিক্রি লিখতে চাই, যাতে স্টক আর আজকের বিক্রি নিজে থেকেই মেলেস্টক কমে, বিক্রির মোট বাড়ে; অজানা মাল বা স্টকের বেশি হলে কিছুই বদলায় না, কারণসহ বার্তা
reportরাতে সব মালের স্টক আর আজকের মোট বিক্রি দেখতে চাইযোগ করার ক্রমে সারি; টাকা দুই ঘর দশমিকে; খালি দোকানে 0.00, crash নয়
quit / ইনপুট শেষকাজ শেষে বের হতে চাইquit বা EOF — দুই পথেই শান্তভাবে শেষ
অন্য যেকোনো লেখাভুল টাইপ করলেও প্রোগ্রাম যেন বন্ধ না হয়error: unknown command, তারপর পরের লাইন পড়ে

লক্ষ করুন, এই টেবিলটিই একটি ছোট specification. এতে কীভাবে বানানো হবে তার একটি শব্দও নেই — vector না map, কোন ফাংশন — শুধু কী করবে। এই আলাদা রাখাটাই spec-এর শক্তি: পরে পুরো ভেতরটা বদলে গেলেও টেবিল একই থাকে, আর টেস্টগুলোও।

যা বানাব না — MVP-র সীমারেখা

শাহীন ভাই আরও চাইলেন: কোন ক্রেতার কত বাকি, মাসিক লাভ-ক্ষতি, স্টক কমলে মোবাইলে SMS! সবই যৌক্তিক, কিন্তু সব একসাথে বানাতে গেলে কয়েক সপ্তাহেও শেষ হবে না। তাই প্রথমে একটি MVP — সবচেয়ে ছোট সংস্করণ, যা তাঁর সবচেয়ে বড় কষ্টটি দূর করে: স্টকের বেশি বিক্রি লেখা আর রাতের যোগফলের গরমিল।

✅ প্রথম সংস্করণে আছে

add, sell, report, quit; প্রতিটি ভুল ইনপুটে পরিষ্কার বার্তা; টাকার নির্ভুল হিসাব।

MVP

⏳ পরে (লিখে রাখা আছে)

ফাইলে সংরক্ষণ (পর্ব ৪৪-এর ফাইল I/O-র পরে), নামে space (“chinigura rice”), দাম বদলানো, বাকির খাতা, মাসিক রিপোর্ট।

পরের সংস্করণ

প্রথম সংস্করণে ফাইলে সংরক্ষণ নেই — প্রোগ্রাম বন্ধ করলেই সব হিসাব মুছে যাবে। এটি জেনেশুনে নেওয়া সিদ্ধান্ত, আর spec-এ লেখা আছে। কাজের মাঝে নতুন চাহিদা ঢুকে প্রজেক্ট শেষই না হওয়ার রোগের নাম scope creep. লিখিত MVP-ই তার ওষুধ: নতুন ইচ্ছা এলে “না” নয়, বলুন “হ্যাঁ, পরের তালিকায়।”

৩ · ডেটা মডেল — একটি মাল, একটি খাতা, একটি কমান্ড

শাহীন ভাইয়ের খাতার যেকোনো লাইন দেখুন: “চাল — ৪৭ কেজি — ৭২.৫০”. তিনটি তথ্য, একটি মাল। এটাই আমাদের data model-এর শুরু। পর্ব ১২-এর struct ঠিক এই কাজের জন্য, আর পর্ব ১৩-এর std::vector অনেকগুলো মাল পাশাপাশি রাখার জন্য:

struct Item {
    std::string name;                 // "rice" — std::string, তাই দৈর্ঘ্যের সীমা নেই
    int         qty{};                // স্টকে কতটা; নিয়ম: কখনো ঋণাত্মক নয়
    long long   price_paisa{};        // একক দাম পয়সায়: 72.50 Tk = 7250
};

struct Ledger {
    std::vector<Item> items;          // দোকানের সব মাল, যোগ করার ক্রমে
    long long sold_paisa{};           // আজকের মোট বিক্রি, পয়সায়
};

enum class Command { Add, Sell, Report, Quit, Unknown };
enum class Result  { Ok, BadInput, NotFound, NotEnough };

মাত্র কয়েকটি লাইন, কিন্তু প্রতিটির পেছনে একটি সিদ্ধান্ত আছে। একজন ইঞ্জিনিয়ার সিদ্ধান্তগুলো লিখে রাখেন — কী বেছে নিলাম, কেন, আর তার দাম কী — যাতে ছয় মাস পরে কেউ (বা নিজেই) “এটা এমন কেন?” প্রশ্নের উত্তর পান:

সিদ্ধান্তকেনদাম কী দিলাম
টাকা পয়সায়, long long পূর্ণসংখ্যায়পর্ব ০৫-এ দেখেছেন, 0.1-এর মতো দশমিক floating point-এ হুবহু রাখা যায় না; হাজারটি যোগে পয়সার গরমিল জমে। পূর্ণসংখ্যার যোগ নির্ভুল। long long কমপক্ষে ৬৪ bit, তাই কয়েক লক্ষ কোটি টাকাও ধরে।পড়ার সময় “72.50” থেকে 7250 বানাতে আর ছাপার সময় ১০০ দিয়ে ভাগ করে টাকা-পয়সা আলাদা করতে হয়
সব মাল একটি std::vector<Item>-এসরল, যোগ করার ক্রম রাখে, নিজেই বড় হয়, নিজেই মেমরি ফেরত দেয়। একটি দোকানে কয়েকশো মাল — প্রতিবার শুরু থেকে খোঁজাও যথেষ্ট দ্রুত।নাম দিয়ে খুঁজতে পুরো তালিকা হাঁটতে হয়; লাখো মাল হলে পর্ব ৩১-এর std::unordered_map দিয়ে সূচি লাগবে
কমান্ড আর ফলাফল enum classলেখা "sell" একবারই পরীক্ষা হয়, বাকি কোড কাজ করে Command::Sell দিয়ে। switch-এ কোনো case বাদ পড়লে -Wswitch ধরিয়ে দেয় (পর্ব ১২)।নতুন কমান্ড যোগ করতে enum, parse আর switch — তিন জায়গায় বদলাতে হয়
new/delete নেই, std::unique_ptr-ও নেইপর্ব ১৬-১৭-এর শিক্ষা: মালিকানা কার, আগে ঠিক করুন। এখানে মালিক একজনই — Ledger-এর vector — আর সে নিজেই RAII মেনে মেমরি ছাড়ে। সবচেয়ে ভালো smart pointer প্রায়ই “কোনো pointer-ই না”।কিছুই না; বরং leak আর double free-র সম্ভাবনাই নেই
খোঁজার ফল Item*, কিন্তু শুধু তখনই ব্যবহারপর্ব ১৫-এর pointer “না পেলে nullptr” বোঝানোর সহজ উপায়।পর্ব ১৮-এর বিপদ: vector-এ push_back হলে আগের pointer অচল হতে পারে (invalidation). তাই নিয়ম — pointer জমিয়ে রাখা যাবে না
নামে space নেই (rice, egg)শব্দে ভাগ করা সহজ থাকে; MVP দ্রুত শেষ হয়।“chinigura rice” লেখা যায় না — পরের তালিকায় লেখা আছে

শেষ কথা qty-এর comment নিয়ে: “নিয়ম: কখনো ঋণাত্মক নয়।” এমন সবসময়-সত্য নিয়মকে বলে invariant. খেয়াল করুন, এখন এটা শুধু একটি comment — struct-এর সব member খোলা, তাই যেকোনো জায়গা থেকে কেউ lg.items[0].qty = -5; লিখলে কেউ আটকাবে না। এই দুর্বলতাটি মনে রাখুন; মডিউল ৪ ঠিক এর সমাধান দিয়ে শুরু হবে।

৪ · একটি লাইনের যাত্রা — পড়া, ভাগ, যাচাই, বদল, ছাপা

প্রতিটি CLI টুলের হৃদয় একটি লুপ: একটি লাইন পড়া → বুঝে নেওয়া → কাজ করা → ফল জানানো → আবার পড়া। লুপ থামে দুই পথে: ব্যবহারকারী quit লিখলেন, অথবা ইনপুটই শেষ (EOF — Linux-এ Ctrl+D, Windows-এ Ctrl+Z তারপর Enter). দ্বিতীয় পথটি ভুলে গেলে প্রোগ্রাম অসীম লুপে আটকে যেতে পারে।

কেন cin >> নয়, পুরো লাইন

পর্ব ০৭-এ std::cin >> x দিয়ে ইনপুট নিয়েছিলেন। কিন্তু কমান্ড-লাইনের জন্য এটা বিপজ্জনক। ধরুন কেউ লিখলেন sell rice — পরিমাণ দিতে ভুলে গেছেন। cin >> cmd >> name >> qty তখন পরের লাইনের প্রথম শব্দটিকেই (হয়তো report) পরিমাণ হিসেবে পড়তে চাইবে, ব্যর্থ হবে, আর stream ব্যর্থ অবস্থায় আটকে যাবে। এক লাইনের ভুল গড়িয়ে পরের লাইন নষ্ট করল।

সমাধান দুই ধাপের, যা প্রায় প্রতিটি পেশাদার CLI-তে দেখবেন। এক, std::getline দিয়ে পুরো একটি লাইন একটি std::string-এ পড়া — লাইনের সীমানা এখানেই চূড়ান্ত। দুই, সেই লাইন থেকে একটি std::istringstream বানানো — <sstream>-এর এই stream একটি string-কে ইনপুটের উৎস বানায়, ফলে ঠিক cin-এর মতো >> দিয়ে শব্দ আর সংখ্যা আলাদা করা যায়। এই লাইনের পড়া ব্যর্থ হলে শুধু এই ছোট stream-টাই নষ্ট হয়; পরের লাইনের জন্য নতুন একটি তৈরি হবে।

এবার দোকানের লুপের ভেতরে একটি লাইন — sell rice 3 — ঢুকলে ঠিক কী কী ঘটে, ধাপে ধাপে দেখুন। শুরুতে স্টকে আছে ১০ কেজি চাল, প্রতি কেজি ৭২ টাকা:

1for (std::string line; std::getline(std::cin, line); ) {2    std::istringstream in{line};3    in >> word;4    switch (parse(word)) { case Command::Sell:5    if (!(in >> name >> qty) || qty <= 0) usage();6    Item* it = find(stock, name);7    if (qty > it->qty) not_enough();8    it->qty -= qty; sold += qty * it->price_paisa;9    std::println("ok: {} -{}", name, qty);
এই মুহূর্তের অবস্থা
line
in-এ বাকি
word / name / qty
it
rice.qty
sold (পয়সা)
  1. পড়া। std::getline Enter পর্যন্ত পুরো লাইনটি line-এ তুলল (শেষের newline বাদ দিয়ে)। ইনপুট শেষ হলে getline ব্যর্থ হয়, for-এর শর্ত মিথ্যা হয় — EOF-এ লুপ নিজেই থামে।
  2. লাইনটিকে একটি ছোট stream বানানো। in এখন শুধু এই লাইনটুকু জানে। এর ভেতরে যা-ই ঘটুক, std::cin অক্ষত থাকে।
  3. প্রথম শব্দ। >> শুরুর space এড়িয়ে প্রথম শব্দ “sell” পড়ল, আর পরের space-এ থামল। বাকি থাকল “ rice 3”.
  4. লেখা থেকে টাইপে। parse("sell") ফেরত দিল Command::Sell. লেখার তুলনা পুরো প্রোগ্রামে শুধু এই এক জায়গায়; এরপর সব সিদ্ধান্ত enum দিয়ে।
  5. যাচাই (validate). নাম আর পরিমাণ পড়া গেল কি না, আর পরিমাণ ধনাত্মক কি না — একসাথে। sell rice abc হলে in >> qty ব্যর্থ হতো, stream-এর failbit উঠত, আর শর্ত সত্য হয়ে usage বার্তা যেত। এটাই input validation: বাইরের ডেটা বিশ্বাস করার আগে পরীক্ষা।
  6. খোঁজা। find vector হেঁটে “rice” পেল, তার ঠিকানা দিল। না পেলে nullptr দিত, আর আমরা “no item” বার্তা দিতাম (আসল কোডে সেই পরীক্ষাও আছে, এখানে জায়গার জন্য বাদ)।
  7. ব্যবসার নিয়ম। ৩ ≤ ১০ — স্টক যথেষ্ট। এই একটি লাইনই qty-এর invariant রক্ষা করছে। এটা না থাকলে কী হয়, অংশ ৮-এর বাগ শিকারে দেখবেন।
  8. বদল (update). স্টক ১০ থেকে ৭, বিক্রির মোট ৩ × 7200 = 21600 পয়সা (২১৬ টাকা)। লক্ষ করুন, সব যাচাই আগে, বদল শেষে — তাই মাঝপথে ভুল ধরা পড়লে অর্ধেক-বদলানো হিসাব থাকে না।
  9. ছাপা। সফল হলে ছোট একটি নিশ্চিতকরণ। লাইনের কাজ শেষ, in ধ্বংস হলো (scope শেষ — RAII), আর লুপ পরের লাইনের জন্য আবার getline-এ ফিরল।
মূল ধারণা

আগে সব যাচাই, তারপর একবারে বদল। প্রতিটি কমান্ড হয় পুরোপুরি সফল, নয়তো কিছুই বদলায় না। ব্যাংকের লেনদেনে এই নীতির নাম “all or nothing” — bKash-এ টাকা পাঠানোর মাঝপথে নেটওয়ার্ক গেলে আপনার টাকা কাটা যাবে আর প্রাপক পাবেন না, এমন অবস্থা যেন কখনো না হয়।

৫ · এক ফাইলের MVP — চালিয়ে দেখুন

এবার পুরো MVP একটি ফাইলে। অনলাইনে চালানো সহজ করতে একটি ছোট ছাড় নেওয়া হয়েছে: দাম পূর্ণ টাকায় (72), আর ভেতরে পয়সায় রাখা হয় (7200). “72.50”-এর মতো দশমিক দাম পড়া পরের মাইলস্টোনের কাজ, সেটা অংশ ৮-এ টেস্টসহ দেখবেন। ইনপুট আগে থেকেই দেওয়া — একটি ছোট সেশন, যাতে ইচ্ছা করে কয়েকটি ভুলও রাখা আছে। “Run” চাপুন:

#include <iostream>
#include <print>
#include <sstream>
#include <string>
#include <vector>

enum class Command { Add, Sell, Report, Quit, Unknown };
struct Item { std::string name; int qty{}; long long price_paisa{}; };

Command parse(const std::string& w) {
    if (w == "add")    return Command::Add;
    if (w == "sell")   return Command::Sell;
    if (w == "report") return Command::Report;
    return w == "quit" ? Command::Quit : Command::Unknown;
}
Item* find(std::vector<Item>& stock, const std::string& name) {
    for (Item& it : stock) if (it.name == name) return &it;
    return nullptr;                                  // not in stock
}

int main() {
    std::vector<Item> stock;
    long long sold = 0;                              // today's sales, in paisa
    for (std::string line; std::getline(std::cin, line); ) {
        std::istringstream in{line};
        std::string word, name;
        int qty = 0, taka = 0;
        in >> word;
        switch (parse(word)) {
        case Command::Add:
            if (!(in >> name >> qty >> taka) || qty <= 0 || taka <= 0) {
                std::println("error: usage: add <item> <qty> <taka>"); break; }
            if (Item* it = find(stock, name)) it->qty += qty;
            else stock.push_back({name, qty, taka * 100LL});
            std::println("ok: {} +{}", name, qty); break;
        case Command::Sell: {
            if (!(in >> name >> qty) || qty <= 0) { std::println("error: usage: sell <item> <qty>"); break; }
            Item* it = find(stock, name);
            if (!it) { std::println("error: no item '{}'", name); break; }
            if (qty > it->qty) { std::println("error: only {} {} left", it->qty, name); break; }
            it->qty -= qty; sold += qty * it->price_paisa;
            std::println("ok: {} -{}", name, qty); break; }
        case Command::Report:
            for (const Item& it : stock) std::println("{:<6} qty {:>3}", it.name, it.qty);
            std::println("sales: {}.{:02} Tk", sold / 100, sold % 100); break;
        case Command::Quit: return 0;
        case Command::Unknown: std::println("error: unknown command '{}'", word);
        }
    }
}

quit ছাড়া প্রতিটি ইনপুট-লাইনের জন্য একটি আউটপুট-লাইন আসে (report-এর জন্য তিনটি): তিনটি ok, তারপর error: only 30 egg left, error: no item 'fish', ঋণাত্মক পরিমাণে usage বার্তা, error: unknown command 'dance', আর শেষে স্টক (rice ৭, egg ৩০) ও sales: 216.00 Tk — হাতে হিসাব করে মিলিয়ে নিন: ৩ × ৭২ = ২১৬. খেয়াল করুন: (১) switch-এ পাঁচটি Command-এর প্রতিটির case আছে, কোনো default নেই — নতুন কমান্ড যোগ করে case ভুলে গেলে -Wswitch জানাবে। (২) find-এর ফেরত দেওয়া pointer সাথে সাথে ব্যবহার করে ফেলে দেওয়া হয়; কোথাও জমিয়ে রাখা হয়নি, কারণ পরের push_back vector-কে নতুন জায়গায় সরাতে পারে (পর্ব ১৮)। (৩) taka * 100LL — LL গুণটিকে long long-এ করায়, যাতে বড় দামে int উপচে না পড়ে। দুর্বলতা: সব কাজ main-এ, যুক্তি আর ছাপা মেশানো, তাই sell আলাদা করে টেস্ট করা যায় না — পরের অংশে এটাই ঠিক করব।

C জানলে

C কোর্সের মাঝপথের প্রজেক্টেও একই পদ্ধতি — চাহিদা, পয়সায় টাকা, তিন স্তরের নকশা, মাইলস্টোন, টেস্ট প্ল্যান। পার্থক্য যন্ত্রে: C-তে fgets + sscanf, স্থির char item[32], আর realloc দিয়ে নিজে হাতে বাড়ানো অ্যারে ও শেষে free; C++-এ getline + istringstream, std::string, আর নিজে থেকে বাড়া std::vector — মেমরির হিসাব রাখার একটি লাইনও লিখতে হয়নি। একই সমস্যা দুই ভাষায় পাশাপাশি দেখতে C কোর্সের মাঝপথের প্রজেক্ট পর্বটি দেখুন।

৬ · ফাইলে ভাগ — namespace, header আর তিন স্তর

পঞ্চাশ লাইনের প্রোগ্রাম এক ফাইলে চলে, কিন্তু টুল বড় হলে সব main-এ রাখলে একটি জিনিস বদলাতে গিয়ে আরেকটি ভাঙবে। পর্ব ০২-এ শিখেছেন decomposition, আর পর্ব ১১-এ শিখেছেন namespace ও header file. এবার দুটিকে একসাথে প্রয়োগ করার পালা।

প্রোগ্রামটিকে তিনটি দায়িত্বে ভাগ করা হলো। এই নীতির নাম separation of concerns — আলাদা দায়িত্ব আলাদা জায়গায়। রেস্টুরেন্টের মতো: ওয়েটার অর্ডার নেন আর খাবার দেন, রাঁধুনি রান্না করেন, ভাঁড়ারে মাল থাকে। রাঁধুনি কখনো টেবিলে গিয়ে অর্ডার নেন না।

দোকানের টুল — তিন স্তরের নকশা

main.cpp (কথা বলা) → ledger.cpp (হিসাবের যুক্তি) → item.h / ledger.h (ডেটা)

getline + istringstreamলাইন পড়ে, শব্দে ভাগ করে
parse_command()লেখা → Command
message(Result)Result → মানুষের ভাষায় বার্তা
print_report()std::format দিয়ে টেবিল
shop::add()যোগ বা পরিমাণ বাড়ানো
shop::sell()যাচাই, তারপর বদল
shop::stock_value()শুধু পড়ে, হিসাব দেয়
shop::Ledgeritems, sold_paisa
shop::Itemname, qty, price_paisa
পরে: storage.cppফাইলে save/load — শুধু এই স্তর বাড়বে
ছবি ১৯.১ — বাঁয়ের স্তর ব্যবহারকারীর সাথে কথা বলে, মাঝে হিসাবের নিয়ম, ডানে ডেটার টাইপ। তীর দেখায় কে কাকে চেনে — ledger.cpp কখনো main.cpp-কে চেনে না।

সবচেয়ে গুরুত্বপূর্ণ নিয়ম: shop:: namespace-এর কোনো ফাংশন std::cin পড়বে না, std::println করবে না। সে শুধু ডেটা নেয়, নিয়ম মেনে বদলায়, আর একটি Result ফেরত দেয়। কী বার্তা দেখানো হবে — ইংরেজিতে, বাংলায়, বা মোবাইল অ্যাপের পপ-আপে — সেই সিদ্ধান্ত ওপরের স্তরের। ফলে যুক্তির অংশ কীবোর্ড ছাড়াই টেস্ট করা যায়, আর ভবিষ্যতে একটি GUI বা ওয়েব সার্ভার এই একই ledger.cpp হুবহু ব্যবহার করতে পারে। নিচে চারটি ফাইল দেখুন (এগুলো একসাথে compile করতে হয়, তাই এখানে Run নেই):

#pragma once
#include <string>

namespace shop {
    struct Item {
        std::string name;
        int         qty{};            // invariant: qty >= 0 (এখনো শুধু কথায়)
        long long   price_paisa{};    // 72.50 Tk = 7250
    };
}
#pragma once
#include <string>
#include <vector>
#include "item.h"

namespace shop {
    enum class Result { Ok, BadInput, NotFound, NotEnough };

    struct Ledger {
        std::vector<Item> items;
        long long sold_paisa{};
    };

    // চুক্তি: কোনোটিই কিছু ছাপে না। ব্যর্থ হলে Ledger একটুও বদলায় না।
    [[nodiscard]] Result add (Ledger& lg, const std::string& name, int qty, long long price_paisa);
    [[nodiscard]] Result sell(Ledger& lg, const std::string& name, int qty);
    long long stock_value(const Ledger& lg);      // const&: শুধু পড়বে
}
#include "ledger.h"

namespace shop {
namespace {                                       // এই ফাইলের ভেতরের সাহায্যকারী
    Item* find(Ledger& lg, const std::string& name) {
        for (Item& it : lg.items) if (it.name == name) return &it;
        return nullptr;
    }
}

Result add(Ledger& lg, const std::string& name, int qty, long long price_paisa) {
    if (name.empty() || qty <= 0 || price_paisa <= 0) return Result::BadInput;
    if (Item* it = find(lg, name)) it->qty += qty;
    else lg.items.push_back({name, qty, price_paisa});
    return Result::Ok;
}

Result sell(Ledger& lg, const std::string& name, int qty) {
    if (qty <= 0) return Result::BadInput;        // guard clause: আগে সব যাচাই
    Item* it = find(lg, name);
    if (it == nullptr) return Result::NotFound;
    if (qty > it->qty) return Result::NotEnough;
    it->qty -= qty;                               // তারপর একসাথে বদল
    lg.sold_paisa += qty * it->price_paisa;
    return Result::Ok;
}

long long stock_value(const Ledger& lg) {
    long long total = 0;
    for (const Item& it : lg.items) total += it.qty * it.price_paisa;
    return total;
}
}
#include <iostream>
#include <print>
#include <sstream>
#include "ledger.h"

enum class Command { Add, Sell, Report, Quit, Unknown };
Command parse_command(const std::string& w);     // সংজ্ঞা নিচে, এখানে বাদ
bool parse_money(const std::string& s, long long& paisa);   // অংশ ৮-এ টেস্টসহ
void print_report(const shop::Ledger& lg);       // std::format-এর টেবিল

const char* message(shop::Result r) {
    switch (r) {
        case shop::Result::Ok:        return "ok";
        case shop::Result::BadInput:  return "error: bad input";
        case shop::Result::NotFound:  return "error: no such item";
        case shop::Result::NotEnough: return "error: not enough stock";
    }
    return "error: ?";
}

int main() {
    shop::Ledger lg;                              // মালিক একজনই — RAII
    for (std::string line; std::getline(std::cin, line); ) {
        std::istringstream in{line};
        std::string word, name, price; int qty = 0;
        in >> word;
        switch (parse_command(word)) {
        case Command::Sell:
            in >> name >> qty;
            std::println("{}", message(shop::sell(lg, name, qty))); break;
        // ... Add, Report, Quit, Unknown — আগের মতোই
        }
    }
}

কয়েকটি বিষয় খেয়াল করুন, প্রতিটিই আগের কোনো পর্বের শিক্ষা। #pragma once header দুবার ঢোকা আটকায় (পর্ব ১১)। header-এ শুধু টাইপ আর ফাংশনের ঘোষণা, সংজ্ঞা .cpp-তে — তাই ODR ভাঙে না। find একটি anonymous namespace-এ, ফলে অন্য কোনো ফাইল একে দেখতেই পায় না — বাইরের দুনিয়ার কাছে প্রতিশ্রুতি শুধু ledger.h-এ যা লেখা। sell-এর শুরুর তিনটি guard clause “আগে যাচাই, পরে বদল” নিয়মটি কোডে লিখে দিয়েছে। আর [[nodiscard]] থাকায় কেউ shop::sell(lg, "rice", 3); লিখে ফলাফল ফেলে দিলে compiler সতর্ক করবে — ব্যর্থ বিক্রি নিঃশব্দে হারিয়ে যাবে না। তৈরি করার কমান্ড (পর্ব ০৩-এর মতো, প্রতিটি .cpp আলাদা translation unit, শেষে link):

# header কমান্ডে লাগে না — #include নিজেই টেনে আনে
g++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined main.cpp ledger.cpp -o shop
./shop

৭ · মাইলস্টোন — প্রতিটি ধাপের শেষে প্রোগ্রাম চলে

নতুনরা প্রায়ই ৪০০ লাইন লিখে প্রথমবার compile করেন, আর পান অসংখ্য error. অভিজ্ঞরা প্রথমে ছোট কিছু বানান যা চলে, তারপর একটু একটু যোগ করেন, আর প্রতিটি যোগের পর প্রোগ্রাম আবার চলে। কিছু ভাঙলে দোষী শেষ যোগ করা অংশটুকুই।

১৯৭৫ সালে প্রকাশিত “Systemantics” বইয়ে John Gall একটি পর্যবেক্ষণ লেখেন, যা এখন “Gall’s law” নামে পরিচিত: কাজ করে এমন প্রতিটি জটিল সিস্টেম দেখা যায় কাজ করা একটি সরল সিস্টেম থেকে ধীরে ধীরে বড় হয়েছে; শুরু থেকেই জটিল করে বানানো সিস্টেম কখনো কাজ করে না, আর জোড়াতালি দিয়েও তাকে চালানো যায় না।

দোকানের টুলের পাঁচটি মাইলস্টোন দেখুন — প্রতিটিতে নতুন কী কাজ করে, আর কোন টেস্টে তা প্রমাণিত:

  1. M1loopকঙ্কাল
  2. M2add/reportপ্রথম আসল কাজ
  3. M3sell + checksMVP পূর্ণ
  4. M4files + formatসাজানো ও সুন্দর
  5. M5save/loadপরে, পর্ব ৪৪
M1 · ~২০ লাইনলুপ ঘোরে, কিন্তু কোনো কাজ করে না
> add rice 10 72
[add: coming soon]
> dance
error: unknown command 'dance'
> quit
টেস্ট: quit-এ বের হয় · অজানা কমান্ডে বার্তা · EOF-এ আটকে থাকে না
M2 · ~৩৫ লাইনstruct Item, std::vector
> report
sales: 0.00 Tk
> add rice 10 72
ok: rice +10
> add rice 5 72
ok: rice +15     ← আগের মাল, তাই পরিমাণ বাড়ল
টেস্ট: খালি report · একই মাল দুবার add · তিনটি মাল যোগ করে report মেলানো
M3 · ~৫০ লাইনএখানেই MVP সম্পূর্ণ — শাহীন ভাইকে দেখানো যায়
> sell rice 3
ok: rice -3
> sell rice 99
error: only 12 rice left
> sell rice abc
error: usage: sell <item> <qty>
টেস্ট: অংশ ৮-এর পুরো টেস্ট প্ল্যান · হাতে হিসাবের সাথে sales মেলানো
M4 · চারটি ফাইলআচরণ একই, ভেতরটা সাজানো; দশমিক দাম
> report
ITEM      QTY      PRICE
rice       12      72.50
egg        30      13.00
sales today: 217.50 Tk
std::println("{:<8}{:>5}{:>11}", name, qty, taka_text(price));
টেস্ট: M3-এর সব টেস্ট হুবহু আবার পাস (refactoring-এর প্রমাণ)
M5 · পরেবন্ধ করলেও হিসাব থাকে
$ ./shop shop.txt
loaded 2 items from shop.txt
> sell egg 6
ok: egg -6
> quit
saved 2 items to shop.txt
std::ifstream / std::ofstream — নিজেই ফাইল বন্ধ করে (RAII)
টেস্ট: save → load → report আগের মতোই · ভাঙা ফাইলে crash নয়
  1. M1 — কঙ্কাল। শুধু getline-এর লুপ, Command enum আর parse. কমান্ডগুলো “coming soon” বলে। তবু নিশ্চিত হলো মৌলিক জিনিস: প্রোগ্রাম চলে, লুপ ঘোরে, বের হওয়ার দুই পথই (quit আর EOF) কাজ করে।
  2. M2 — add আর report. struct Item আর std::vector এল। একটি সচেতন শর্টকাট: দাম পূর্ণ টাকায়, সুন্দর টেবিল নেই — আগে মূল যুক্তি ঠিক করা।
  3. M3 — sell আর যাচাই। সব ভুল ইনপুটের বার্তা, আর “স্টকের বেশি নয়” নিয়ম — MVP তৈরি; উপরের চালানো কোডটি ঠিক এই অবস্থা। কাজ এখানে থামলেও শাহীন ভাইয়ের হাতে ব্যবহারযোগ্য টুল থাকত। প্রতিটি মাইলস্টোনে ভাবুন: “আজই থামতে হলে হাতে কী থাকবে?”
  4. M4 — ফাইলে ভাগ আর সুন্দর রিপোর্ট। অংশ ৬-এর চার ফাইল, Result enum, দশমিক দাম পড়া, আর std::format-এর {:<8}, {:>5} দিয়ে সারিবদ্ধ টেবিল (পর্ব ০৭)। আচরণ বদলায়নি, ভেতরটা বদলেছে — একে বলে refactoring. প্রমাণ? M3-এর সব টেস্ট হুবহু আবার পাস।
  5. M5 — ফাইলে সংরক্ষণ (stretch goal). প্রোগ্রাম বন্ধ করলেও হিসাব থাকবে। এর জন্য দরকার std::ifstream/std::ofstream — পর্ব ৪৪-এর বিষয়। স্তরে ভাগ করা নকশার লাভ এখানে চোখে পড়ে: নতুন একটি storage.cpp যোগ হবে, sell বা add-এর একটি লাইনও বদলাবে না। শুরুর কমান্ড-লাইন আর্গুমেন্ট (./shop shop.txt) পড়তে লাগবে main(int argc, char* argv[]).

প্রতিটি মাইলস্টোন শেষে একটি git commit করুন (যেমন git commit -m "M3: sell with stock checks"). পরে কিছু ভাঙলে আগের চলা অবস্থায় ফিরে যাওয়া যাবে, আর কোন পরিবর্তনে ভেঙেছে তাও দেখা যাবে। Git-এর বিস্তারিত পাবেন ABCL TECH-এর Software Engineering ও Git কোর্সে। ফাইল অনেক হলে প্রতিবার g++ ... লেখা ক্লান্তিকর হয় — তখন CMake (পর্ব ৪৭)।

৮ · টেস্ট — স্ক্রিপ্ট করা ইনপুট আর একটি বাগ শিকার

“চালিয়ে দেখেছি, চলছে” — এটি টেস্ট নয়; আপনি চালিয়েছেন শুধু নিজের মাথার ইনপুট দিয়ে। দোকানের কর্মচারী টাইপ করবেন এমন জিনিস, যা আপনি কল্পনাও করেননি। Test plan হলো আগে থেকে লিখে রাখা তালিকা: কোন পরিস্থিতি, কী ইনপুট, কী ফল আশা করছি। সবচেয়ে বেশি বাগ লুকিয়ে থাকে edge case-এ — সীমানায়, শূন্যে, খালি জায়গায়:

নংপরিস্থিতিইনপুটপ্রত্যাশিত ফল
T1খালি দোকানশুরুতেই report, sell rice 1sales: 0.00 Tk; no item 'rice' — crash নয়
T2সংখ্যার জায়গায় অক্ষরsell rice abc, add tea x 10usage বার্তা, কিছুই বদলায় না
T3শূন্য আর ঋণাত্মকsell rice 0, add oil -2 190দুটিই বাতিল — ঋণাত্মক বিক্রি মানে লুকিয়ে স্টক বাড়ানো!
T4স্টকের সীমানাস্টক ১০: sell rice 10, তারপর sell rice 1প্রথমটি সফল (ঠিক ০ বাকি), দ্বিতীয়টি only 0 rice left
T5বিশাল সংখ্যাsell rice 99999999999int-এ ধরে না, তাই পড়া ব্যর্থ — usage বার্তা; কোনো overflow নয়
T6ইনপুট হঠাৎ শেষquit ছাড়াই EOFশান্তভাবে বের হয়, অসীম লুপ নয়

স্ক্রিপ্ট করা ইনপুট — হাতে টাইপ ছাড়া পুরো সেশন

CLI-এর একটি বড় সুবিধা: পুরো সেশন একটি টেক্সট ফাইলে লেখা যায়। পর্ব ০৭-এর redirection দিয়ে সেই ফাইল প্রোগ্রামের ইনপুট বানিয়ে, আউটপুট আরেক ফাইলে নিয়ে, আগে থেকে যাচাই করে রাখা “সঠিক” আউটপুটের সাথে মেলানো যায়। একে বলে golden-file test. অংশ ৫-এর “Run” বোতামও ঠিক এটাই করেছে — আগে লেখা ইনপুট দিয়ে প্রোগ্রাম চালানো:

# tests/t3_negative.txt-এ আছে: add oil -2 190 / sell rice 0 / report
./shop < tests/t3_negative.txt > got.txt
diff tests/t3_negative.expected got.txt && echo PASS    # এক অক্ষর আলাদা হলেও ধরা পড়ে

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

tests/t4_stock.txtআগে লেখা ইনপুট
add egg 30 13
sell egg 40
report
diff expected got.txtটেস্ট ব্যর্থ
  ok: egg +30
- error: only 30 egg left
+ ok: egg -40
- egg    qty  30
+ egg    qty -10
- sales: 0.00 Tk
+ sales: 520.00 Tk
FAIL — ৩টি লাইন আলাদা
ledger.cpp · sell()কারণ খোঁজা
Result sell(Ledger& lg, const std::string& name, int qty) {
    if (qty <= 0) return Result::BadInput;
    Item* it = find(lg, name);
    if (it == nullptr) return Result::NotFound;
    // স্টকের পরীক্ষা নেই!
    it->qty -= qty;                       // 30 - 40 = -10
    lg.sold_paisa += qty * it->price_paisa;   // 40 x 1300
    return Result::Ok;
}
test_ledger.cppঠিক করার আগে: ব্যর্থ হয় এমন unit test
shop::Ledger lg;
assert(shop::add(lg, "egg", 30, 1300) == shop::Result::Ok);
assert(shop::sell(lg, "egg", 40) == shop::Result::NotEnough);  // FAIL
assert(lg.items[0].qty == 30 && lg.sold_paisa == 0);
ঠিক করার পরএক লাইন যোগ
    if (it == nullptr) return Result::NotFound;
    if (qty > it->qty) return Result::NotEnough;
    it->qty -= qty;
t4_stock: PASS · test_ledger: PASS · বাকি সব: PASS
  1. টেস্টটি বাগের আগেই লেখা ছিল। test plan-এর T4 থেকে বানানো ইনপুট-ফাইল: ৩০টি ডিম আছে, ৪০টি বিক্রির চেষ্টা। প্রত্যাশিত ফলও আগে লেখা — একটি error, আর কিছুই বদলাবে না।
  2. diff লাল দেখাল। প্রোগ্রাম আনন্দে “ok” বলেছে, স্টক হয়েছে −১০, আর বিক্রি দেখাচ্ছে ৫২০ টাকা — যে ডিম দোকানে ছিলই না তার দাম! নিঃশব্দে ভুল হিসাব crash-এর চেয়েও বিপজ্জনক।
  3. কারণ খোঁজা। আউটপুট বলছে সমস্যা বিক্রিতে, তাই সরাসরি sell(). স্তরে ভাগ করা নকশার লাভ: সন্দেহভাজন জায়গা মাত্র কয়েক লাইন। invariant (qty ≥ 0) রক্ষার লাইনটিই নেই।
  4. ঠিক করার আগে একটি ছোট টেস্ট। sell() কিছু ছাপে না, তাই একে সরাসরি একটি unit test থেকে ডাকা যায় — কোনো ইনপুট-ফাইল ছাড়াই। এই মুহূর্তে টেস্টটি ব্যর্থ হয়; এতে প্রমাণ হলো টেস্টটি সত্যিই বাগটি ধরতে পারে।
  5. এক লাইনের সমাধান, সব টেস্ট সবুজ। পরীক্ষা যোগ হলো বদলের আগে. এখন দুটি টেস্টই চিরকাল থেকে যাবে — একে বলে regression test. ছয় মাস পরে কেউ (বা AI) “পরিষ্কার” করতে গিয়ে লাইনটি মুছে ফেললে প্রথম দিনেই ধরা পড়বে।

একটি ছোট ফাংশনের টেস্ট — দশমিক দাম পড়া

M4-এ দাম আসবে “72.50” রূপে, আর আমাদের দরকার 7250 পয়সা। std::stod("72.50") * 100 লেখা লোভনীয়, কিন্তু তাতে আবার floating point ঢুকে পড়ে — 0.29 * 100 হুবহু ২৯ হয় না, ২৮.৯৯৯৯… হয়, আর পূর্ণসংখ্যায় কাটলে ২৮! তাই লেখাটিকে অক্ষর ধরে ধরে নিজেই পড়া নিরাপদ। এমন ফাংশন লেখার সাথে সাথেই তার টেস্ট লিখুন। assert শর্ত মিথ্যা হলে লাইন নম্বরসহ প্রোগ্রাম থামায়; সব পাস করলে শেষ লাইনে PASS ছাপা হয়:

#include <cassert>
#include <print>
#include <string>

// "72.50" -> 7250 paisa; returns false for bad text
bool parse_money(const std::string& s, long long& paisa) {
    long long taka = 0; int frac = 0, decimals = 0;
    bool dot = false, digit = false;
    for (char c : s) {
        if (c == '.' && !dot) { dot = true; continue; }
        if (c < '0' || c > '9') return false;        // "12abc", "-5"
        digit = true;
        if (!dot) taka = taka * 10 + (c - '0');
        else if (++decimals > 2) return false;       // "1.234"
        else frac = frac * 10 + (c - '0');
    }
    if (!digit) return false;                        // "" or "."
    paisa = taka * 100 + (decimals == 1 ? frac * 10 : frac);
    return true;
}

int main() {
    long long p = 0;
    assert(parse_money("72.50", p) && p == 7250);
    assert(parse_money("72.5", p) && p == 7250);
    assert(parse_money("13", p) && p == 1300);
    assert(!parse_money("12abc", p) && !parse_money("1.234", p));
    assert(!parse_money("", p) && !parse_money(".", p));
    std::println("PASS: all parse_money tests");
}

আউটপুটে আসে একটিই লাইন: PASS: all parse_money tests. ফাংশনটি পর্ব ১০-এর out parameter (long long& paisa) দিয়ে ফল দেয় আর bool দিয়ে জানায় সফল কি না। টেস্টগুলো সাজানো তিন দলে: স্বাভাবিক মান (72.50, 13), একটু অস্বাভাবিক কিন্তু বৈধ (72.5 মানে ৭২.৫০, ৭২.০৫ নয়!), আর ভুল লেখা (12abc, তিন ঘর দশমিক, খালি, শুধু বিন্দু)। একটি সতর্কতা: assert-এর ভেতরে আসল কাজ (এখানে parse_money ডাকা) রাখা শুধু টেস্ট প্রোগ্রামে চলে; -DNDEBUG দিয়ে compile করলে পুরো assert-ই মুছে যায়, ভেতরের ডাকসহ। আসল প্রোগ্রামের যাচাই তাই সবসময় সাধারণ if দিয়ে। আরেকটি ফাঁক রইল: ২০ অঙ্কের টাকা দিলে taka উপচে পড়বে।

পরীক্ষা ও ইন্টারভিউতে
  • টাকা double-এ রাখা উচিত নয় কেন? — অনেক দশমিক মান (যেমন 0.1) binary floating point-এ হুবহু রাখা যায় না; যোগে ক্ষুদ্র ত্রুটি জমে। সমাধান: ছোট এককে (পয়সা) পূর্ণসংখ্যা, বা decimal টাইপ।
  • একটি লাইন পড়ে শব্দে ভাগ করবেন কীভাবে? — std::getline(std::cin, line), তারপর std::istringstream in{line}; in >> a >> b; এবং প্রতিটি পড়া সফল কি না যাচাই।
  • std::cin >> n;-এর পরে getline খালি লাইন দেয় কেন? — >> Enter-এর newline stream-এ রেখে দেয়, getline সেটাই পড়ে ফেলে; std::ws বা ignore দিয়ে সরাতে হয়, অথবা সবসময় পুরো লাইন পড়ুন।
  • header-এ কী রাখবেন, .cpp-তে কী? — header-এ টাইপ, ঘোষণা, inline/template; সাধারণ ফাংশন ও global ভ্যারিয়েবলের সংজ্ঞা .cpp-তে, নইলে ODR ভেঙে linker error.
  • vector-এর উপাদানের pointer কখন অচল হয়? — যখন push_back/insert-এ capacity বাড়াতে vector নতুন জায়গায় সরে; তখন আগের সব pointer, reference, iterator dangling.

৯ · AI-এর যুগে — AI আপনার pair-programmer, আপনি নকশাকার

এই পুরো প্রজেক্ট একটি প্রম্পটেই AI দিয়ে লেখানো যায় — কিন্তু এক প্রম্পটের ৩০০ লাইনে কোথায় টাকা double, কোথায় স্টকের পরীক্ষা নেই, খুঁজে পাওয়া কঠিন। পেশাদারেরা AI-কে ব্যবহার করেন pair programming-এর সঙ্গীর মতো: AI লেখে, আপনি দিক ঠিক করেন আর প্রতিটি লাইন যাচাই করেন। এর তিনটি নিয়ম:

📝 আগে spec, পরে প্রম্পট

অংশ ২-এর কমান্ড-টেবিল আর অংশ ৩-এর ডেটা মডেল আপনার লেখা। AI-কে সেগুলো দিন — নকশার সিদ্ধান্ত AI-এর হাতে ছাড়বেন না।

নকশা আপনার

🧩 একবারে একটি ফাংশন

ledger.h-এর একটি ঘোষণা দিন, শুধু সেটার সংজ্ঞা চান। ২০ লাইন যাচাই করা যায়; ৩০০ লাইন যায় না।

ছোট টুকরো

✅ টেস্ট আগে, কোড পরে

টেস্ট-কেস আপনি লিখুন (বা AI-কে দিয়ে লিখিয়ে নিজে যাচাই করুন), তারপর কোড। AI-এর কোড টেস্ট পাস না করলে কোনো তর্ক নেই।

প্রমাণ চাই

নিচের প্রম্পটে লক্ষ করুন — AI-কে কী বানাতে হবে তার চেয়ে বেশি লেখা আছে কী করা যাবে না:

আমি C++23 শিখছি (g++ -std=c++23 -Wall -Wextra). একটি দোকানের স্টক ও বিক্রির CLI টুল বানাচ্ছি। নিচে আমার নকশা — এটা বদলাবে না। ডেটা: struct Item { std::string name; int qty{}; long long price_paisa{}; }; struct Ledger { std::vector<Item> items; long long sold_paisa{}; }; enum class Result { Ok, BadInput, NotFound, NotEnough }; এখন শুধু এই একটি ফাংশনের সংজ্ঞা লেখো, namespace shop-এর ভেতরে: [[nodiscard]] Result sell(Ledger& lg, const std::string& name, int qty); নিয়ম: ১. ফাংশন কিছু ছাপবে না, cin পড়বে না। ২. আগে সব যাচাই (qty <= 0, নাম না পাওয়া, স্টকের বেশি), তারপর বদল; ব্যর্থ হলে Ledger একটুও বদলাবে না। ৩. টাকা শুধু পূর্ণসংখ্যায় (পয়সা); কোনো float/double নয়। ৪. new/delete, raw array বা using namespace std নয়; Item-এর pointer বা reference কোথাও জমিয়ে রাখবে না। ৫. কোডের পরে assert দিয়ে ৫টি টেস্ট দাও: সফল বিক্রি, ঠিক পুরো স্টক বিক্রি, স্টকের বেশি, অজানা নাম, শূন্য পরিমাণ — প্রতিটিতে Ledger-এর অবস্থাও যাচাই করবে।

AI-এর লেখা প্রজেক্ট-কোড যাচাইয়ের চেকলিস্ট

AI কোড লিখতে পারে, কিন্তু “কী বানাব” আর “কখন হয়েছে” —
এই দুটি প্রশ্নের উত্তর spec আর টেস্টে, যা আপনার লেখা।

১০ · মডিউল ৩ শেষ — মেমরি, স্ট্রিং আর রিসোর্স এখন আপনার হাতে

এই পর্বের সাথে মডিউল ৩ “মেমরি, স্ট্রিং আর রিসোর্স” শেষ হলো। সাতটি পর্বে আপনি শিখেছেন ডেটা মেমরিতে কোথায় থাকে, কে তার মালিক, আর কতক্ষণ বাঁচে — আর আজ সব একসাথে একটি টুলে ব্যবহার করলেন। একবার ফিরে দেখুন:

পর্ববিষয়এখন আপনি যা পারেনদোকানের টুলে কোথায়
১৩vector ও arrayঅনেক মান এক সারিতে রাখতে, নিজে থেকে বাড়াতে, লুপে হাঁটতে পারেনstd::vector<Item> items
১৪string ও string_viewসীমাহীন দৈর্ঘ্যের লেখা নিরাপদে রাখতে, তুলনা ও ভাগ করতে পারেনমালের নাম, কমান্ড-লাইন
১৫pointerঠিকানা দিয়ে কাজ করতে, আর “কিছুই নেই” বোঝাতে nullptr ব্যবহার করতে পারেনfind()-এর Item*
১৬new/deleteheap-এর মেমরি বোঝেন, আর জানেন কেন হাতে delete লেখা এড়াতে হয়একটিও new নেই — সচেতন সিদ্ধান্ত
১৭RAII ও smart pointerমালিকানা ঠিক করে রিসোর্সের দায়িত্ব object-কে দিতে পারেনLedger সব মালের একমাত্র মালিক
১৮lifetime ও danglingকোন object কতক্ষণ বাঁচে বলতে পারেন, dangling pointer/reference চিনতে পারেনpointer জমিয়ে না রাখার নিয়ম
১৯মাঝপথের প্রজেক্টচাহিদা থেকে টেস্টসহ কাজ করা একটি টুল পর্যন্ত পৌঁছাতে পারেনপুরো টুল

মডিউল ৪ শুরু হবে ঠিক সেই দুর্বলতা থেকে, যা এই পর্বে বারবার চোখে পড়েছে: qty ≥ 0 নিয়মটি রক্ষা করছে শুধু comment আর ভদ্রলোকের চুক্তি। Ledger-এর ডেটা খোলা, তাই যে কেউ পাহারা পাশ কাটাতে পারে। পর্ব ২০-এ class দিয়ে ডেটা লুকানো হবে — items হবে private, আর বদলানো যাবে শুধু add()/sell() দিয়ে। এর নাম encapsulation.

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

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

প্র ০১কোড লিখতে পারলে আগে spec লেখার ঝামেলা কেন? সরাসরি লিখে, চালিয়ে, ভুল ঠিক করলেই তো হয়।

ছোট প্রোগ্রামে সরাসরি লেখা সত্যিই চলে — পাঁচ মিনিটের হিসাবের জন্য কেউ spec লেখেন না। সমস্যা শুরু হয় যখন প্রোগ্রাম অন্য কারও জন্য, বা কয়েক সপ্তাহের কাজ। তখন “কী বানাব” প্রশ্নের উত্তর মাথায় থাকলে তা প্রতিদিন একটু একটু বদলায়, আর বদলানোটা কেউ টের পায় না। দোকানের উদাহরণেই ভাবুন: “বিক্রি” মানে কি স্টক কমানো, নাকি শুধু টাকা লেখা? স্টকের বেশি বিক্রি চাইলে কী হবে — আটকাবে, নাকি সতর্ক করে মেনে নেবে (কারণ খাতায় হয়তো ভুল আছে, দোকানে আসলে মাল আছে)? এগুলো কোডের প্রশ্ন নয়, ব্যবসার প্রশ্ন। কোড লেখার মাঝখানে এগুলোর উত্তর আন্দাজে দিলে পরে পুরো অংশ নতুন করে লিখতে হয়।

সফটওয়্যার ইঞ্জিনিয়ারিংয়ের ইতিহাসে এটা বহুবার মাপা হয়েছে। Barry Boehm ১৯৮১ সালের “Software Engineering Economics” বইয়ে দেখান, চাহিদার ভুল যত দেরিতে ধরা পড়ে, ঠিক করার খরচ তত বহুগুণ বাড়ে — নকশার কাগজে যে ভুল এক ঘণ্টায় শুধরানো যায়, ব্যবহারকারীর হাতে যাওয়ার পর সেটাই হতে পারে সপ্তাহের কাজ। সঠিক অনুপাত নিয়ে আজও বিতর্ক আছে, কিন্তু প্রবণতা নিয়ে নেই। পরে Agile আন্দোলন ভারী spec-এর বিরুদ্ধে দাঁড়ালেও spec বাদ দেয়নি — ছোট user story আর acceptance criteria দিয়ে তাকে হালকা করেছে।

spec টেস্টেরও উৎস: অংশ ২-এর প্রতিটি “কখন হয়েছে” সরাসরি একটি টেস্ট-ফাইল হয়েছে। spec না থাকলে প্রত্যাশিত আউটপুট ঠিক করতে হয় কোড দেখে — অর্থাৎ কোড যা করে তাকেই ঠিক ধরে নেওয়া।

AI-এর যুগে প্রশ্নটা আরও তীক্ষ্ণ। AI খুব দ্রুত কোড লেখে, কিন্তু যা বলবেন ঠিক তা-ই — আর যা বলবেন না, সেখানে আন্দাজ করে। লিখিত spec আসলে সবচেয়ে ভালো প্রম্পট। যিনি spec লিখতে পারেন, তিনি AI-কে দিয়ে কাজ করাতে পারেন; যিনি পারেন না, তিনি AI-এর আন্দাজের ওপর নির্ভর করেন।

মূল উপলব্ধি: spec কোড লেখার গতি কমায় না, ভুল দিকে দৌড়ানো থামায়। “কী” আর “কীভাবে” আলাদা রাখলে প্রথমটি স্থির থাকে, আর দ্বিতীয়টি নির্ভয়ে বদলানো যায়।

প্র ০২টাকা পয়সায় পূর্ণসংখ্যা করে রাখার নিয়ম কি শুধু শিক্ষার্থীদের জন্য, নাকি আসল ব্যাংক ও পেমেন্ট সিস্টেমও এমন করে? ভ্যাট বা ভাগের হিসাবে পয়সার ভগ্নাংশ এলে কী হয়?

আসল সিস্টেমও ঠিক এভাবেই ভাবে। সমস্যার মূল হলো double সংখ্যাকে রাখে দুইয়ের ঘাতে (binary). ১/৩ যেমন দশমিকে শেষ হয় না (০.৩৩৩…), তেমনি ০.১ binary-তে শেষ হয় না। ফলে 0.1 + 0.2 হুবহু 0.3 হয় না — ১৯৮৫ সালের IEEE 754 standard মেনে চলা প্রায় সব কম্পিউটারে এটা সত্য। একটি যোগে ত্রুটি অতি ক্ষুদ্র, কিন্তু একটি পেমেন্ট সিস্টেম দিনে লাখো লেনদেন যোগ করে, আর হিসাবের নিয়ম হলো পয়সা পর্যন্ত মিলতেই হবে।

ক্ষুদ্র ত্রুটি জমে কতটা বিপদ হতে পারে, তার একটি দুঃখজনক উদাহরণ ১৯৯১ সালের উপসাগরীয় যুদ্ধের Patriot ক্ষেপণাস্ত্র-প্রতিরক্ষা। এর কম্পিউটার সময় গুনত এক সেকেন্ডের দশ ভাগের এক ভাগ (০.১) এককে, আর ০.১ তার ২৪-bit রেজিস্টারে হুবহু ধরা যেত না। প্রায় ১০০ ঘণ্টা একটানা চলার পর জমা ত্রুটি দাঁড়ায় এক সেকেন্ডের প্রায় এক-তৃতীয়াংশে — দ্রুতগামী ক্ষেপণাস্ত্রের জন্য বিশাল দূরত্ব। মার্কিন সরকারের হিসাব-নিরীক্ষা অফিস (GAO) পরে এই ত্রুটিকে একটি ব্যর্থ প্রতিরোধের কারণ হিসেবে চিহ্নিত করে, যাতে ২৮ জন সৈন্য নিহত হন। শিক্ষা একই: হুবহু রাখা যায় না এমন মান বারবার যোগ করলে ত্রুটি জমে।

এ কারণেই ব্যবসার জগতের ভাষা ও টুল দশমিক হিসাব আলাদা করে দেয়। ১৯৫৯ সালে নকশা করা COBOL শুরু থেকেই দশমিক সংখ্যা সমর্থন করত, আর আজও অনেক ব্যাংকের মূল সিস্টেমে চলে। Java-র BigDecimal, Python-এর decimal, SQL ডেটাবেসের DECIMAL/NUMERIC টাইপ — সবই একই সমস্যার সমাধান।

ভগ্নাংশের প্রশ্নটি আসল। ১৫% ভ্যাট ধরলে ৭২.৫০ টাকার ভ্যাট ১০.৮৭৫ টাকা — পয়সার ভগ্নাংশ। পূর্ণসংখ্যা এখানে আপনাকে বাধ্য করে একটি লিখিত নিয়ম বেছে নিতে: কাছের পয়সায় গোল করা, সবসময় নিচে, নাকি “banker’s rounding” (ঠিক মাঝখানে হলে জোড় সংখ্যার দিকে)? পূর্ণসংখ্যায় এটা লেখা যায় নির্ভুলভাবে: (7250 * 15 + 50) / 100 = ১০৮৮ পয়সা। double-এ এই সিদ্ধান্ত লুকিয়ে থাকে, আর হার্ডওয়্যারের খেয়ালে ঠিক হয়।

মূল উপলব্ধি: পূর্ণসংখ্যার টাকা শুধু নির্ভুলতা দেয় না, প্রতিটি গোল করার সিদ্ধান্তকে চোখের সামনে আনে। হিসাবের সফটওয়্যারে “আনুমানিক ঠিক” বলে কিছু নেই।

প্র ০৩সুপারশপের আসল POS (Point of Sale) সিস্টেম কি আমাদের মতো একটি vector-এ স্টক রাখে? তারা ডেটা কীভাবে সংরক্ষণ করে, যাতে বিদ্যুৎ গেলেও কিছু না হারায়?

মূল ধারণা একই — প্রতিটি মালের নাম, পরিমাণ, দাম — কিন্তু বাস্তবের সিস্টেমে তিনটি বড় পার্থক্য আছে। প্রথমত, ডেটা মেমরিতে নয়, একটি ডেটাবেসে থাকে। ছোট দোকান বা একক কাউন্টারে প্রায়ই SQLite-এর মতো embedded ডেটাবেস — যা D. Richard Hipp ২০০০ সালে বানান, আর এখন মোবাইল ফোন থেকে ব্রাউজার পর্যন্ত অসংখ্য জায়গায় চলে। বড় সুপারশপ চেইনে কেন্দ্রীয় সার্ভারের ডেটাবেস, আর প্রতিটি কাউন্টারে একটি স্থানীয় কপি।

দ্বিতীয়ত, তারা transaction ব্যবহার করে। একটি বিক্রিতে কয়েকটি জিনিস একসাথে বদলায়: স্টক কমে, বিক্রির খাতায় লাইন যোগ হয়, ক্যাশবাক্সের হিসাব বাড়ে। ডেটাবেস নিশ্চিত করে — হয় সবগুলো ঘটবে, নয়তো একটিও না; মাঝপথে বিদ্যুৎ গেলে পরের বার চালু হলে অর্ধেক-করা কাজ বাতিল হয়। এই নিশ্চয়তার নাম ACID-এর “A” (atomicity). অংশ ৪-এর “আগে সব যাচাই, তারপর একবারে বদল” নিয়মটি এরই সবচেয়ে সরল রূপ।

তৃতীয় পার্থক্যটি সবচেয়ে শিক্ষণীয়: অনেক সিস্টেম “এখনকার স্টক” মূল সত্য হিসেবে রাখে না। তারা রাখে ঘটনার তালিকা — “সকাল ৯টায় ৫০ কেজি চাল এল”, “১০:১৫-এ ৩ কেজি বিক্রি” — আর এখনকার স্টক সেই তালিকা যোগ করে বের করা হয়। কোনো ঘটনা কখনো মোছা হয় না; ভুল হলে নতুন একটি সংশোধনী ঘটনা যোগ হয়। এই ধারণা নতুন নয়: ১৪৯৪ সালে ইতালির গণিতবিদ Luca Pacioli তাঁর বইয়ে double-entry bookkeeping-এর যে পদ্ধতি বর্ণনা করেন, তাতেও খাতার লেখা মোছা হয় না, শুধু উল্টো লেখা যোগ হয়। সফটওয়্যারের ভাষায় এর আধুনিক নাম event sourcing. লাভ: পুরো ইতিহাস থাকে (কে কখন কী বিক্রি করল — চুরি বা ভুল ধরতে অমূল্য), আর যেকোনো দিনের স্টক আবার হিসাব করা যায়।

মূল উপলব্ধি: আমাদের vector আর আসল POS-এর মাঝে দূরত্ব ধারণায় নয়, নিশ্চয়তায় — টিকে থাকা, সব-অথবা-কিছুই-না, আর ইতিহাস। M5-এ ফাইলে সংরক্ষণ সেই পথের প্রথম ধাপ, আর পর্ব ৫১-এর ক্যাপস্টোনে আরও এগোবেন।

প্র ০৪২০২৬ সালে এসে terminal-এর CLI টুল কেন বানাব? মানুষ তো বোতাম আর জানালা চায়। CLI কখন GUI-এর চেয়ে সত্যিই ভালো?

শাহীন ভাইয়ের মতো সাধারণ ব্যবহারকারীর জন্য শেষ পর্যন্ত GUI বা মোবাইল অ্যাপই ঠিক — বোতাম দেখে বোঝা যায় কী করা যায়। কিন্তু প্রোগ্রামার আর সিস্টেম অ্যাডমিনের কাছে CLI আজও সবচেয়ে শক্তিশালী যন্ত্রগুলোর একটি।

প্রথম কারণ স্বয়ংক্রিয়তা। এই পর্বেই দেখলেন, পুরো সেশন একটি টেক্সট ফাইলে লেখা যায়, আর এক কমান্ডে চালানো যায়। রাত ১২টায় নিজে থেকে রিপোর্ট বানানো, একশো দোকানের ফাইল একসাথে প্রক্রিয়া করা — CLI-তে এগুলো একটি ছোট script. GUI-তে একই কাজ মানে মানুষকে বসে একশোবার ক্লিক করা। দ্বিতীয় কারণ জোড়া লাগানো। ১৯৭৩ সালে Doug McIlroy-র প্রস্তাবে Unix-এ pipe (|) আসে — এক প্রোগ্রামের আউটপুট সরাসরি আরেকটির ইনপুট। Unix-এর দর্শন দাঁড়ায়: প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে, আর লেখা দিয়ে অন্যের সাথে কথা বলবে। ./shop < day.txt | grep error | wc -l — তিনটি ছোট টুল মিলে “আজ কতগুলো ভুল কমান্ড এসেছে” বলে দিল, কেউ এই কাজের জন্য আলাদা কোড লেখেননি।

তৃতীয় কারণ দূর থেকে চালানো ও কম সম্পদ। ডেটা সেন্টারের সার্ভারে সাধারণত কোনো পর্দাই থাকে না; SSH দিয়ে terminal-ই একমাত্র পথ। চতুর্থ কারণ টেস্ট — লেখা ঢোকে, লেখা বের হয়, তাই golden-file টেস্ট সহজ; GUI টেস্ট অনেক জটিল ও ভঙ্গুর।

সবচেয়ে বুদ্ধিমান উত্তর অবশ্য “দুটোই”। git এর ভালো উদাহরণ: Linus Torvalds ২০০৫ সালে এটি বানান একেবারে CLI হিসেবে, আর আজ এর ওপর অসংখ্য GUI বসানো — সবাই ভেতরে একই git চালায়। আমাদের অংশ ৬-এর নকশাও তাই ভবিষ্যতের জন্য তৈরি: ledger.cpp কিছু ছাপে না, তাই main.cpp-এর জায়গায় কাল একটি GUI বা ওয়েব সার্ভার বসালে যুক্তির একটি লাইনও বদলাতে হবে না।

মূল উপলব্ধি: CLI আর GUI প্রতিযোগী নয়, একই মূল যুক্তির ওপর দুটি আলাদা মুখ। প্রথমে যুক্তি আর CLI বানালে টেস্ট আর স্বয়ংক্রিয়তা বিনামূল্যে আসে; GUI পরে যোগ করা যায়।

প্র ০৫মডিউল ৪-এ class শেখার পর এই টুল কীভাবে বদলাবে? আজকের struct + আলাদা ফাংশনের নকশা কি তখন ফেলে দিতে হবে?

ফেলে দিতে হবে না — বরং আজকের নকশাই হবে ভিত্তি। ১৯৭২ সালে David Parnas তাঁর বিখ্যাত প্রবন্ধ “On the Criteria To Be Used in Decomposing Systems into Modules”-এ বলেন, একটি প্রোগ্রামকে অংশে ভাগ করার সেরা নিয়ম হলো — যে সিদ্ধান্তগুলো ভবিষ্যতে বদলাতে পারে, সেগুলো এক একটি অংশের ভেতরে লুকিয়ে রাখা (information hiding). আমরা আজ এর অর্ধেক করেছি: find লুকানো (anonymous namespace), যুক্তি ledger.cpp-এ আলাদা। কিন্তু ডেটা — items vector — এখনো সবার সামনে খোলা।

পর্ব ২০-এ প্রথম বদল: struct Ledger হবে class Inventory, যার items private, আর add/sell হবে তার member function. তখন lg.items[0].qty = -5; লেখা compile-ই হবে না — invariant রক্ষা করবে compiler, comment নয়। পর্ব ২১-এর constructor নিশ্চিত করবে কোনো Item শুরু থেকেই অবৈধ অবস্থায় জন্মায় না (খালি নাম বা ঋণাত্মক দাম)।

পর্ব ২৬-এর abstract class দিয়ে সংরক্ষণের অংশ আলাদা করা যাবে: একটি Storage interface, যার দুটি রূপ — আসল ফাইলে লেখা FileStorage আর টেস্টের জন্য মেমরিতে রাখা MemoryStorage. Inventory জানবে না কোনটা পেয়েছে। পর্ব ২৭-এ শিখবেন কেন এখানে inheritance-এর চেয়ে composition ভালো — Inventory একটি Storage ব্যবহার করে, নিজে Storage নয়।

সবচেয়ে মূল্যবান যা আজ থেকে অক্ষত থাকবে, তা হলো টেস্টগুলো। golden-file টেস্ট শুধু ইনপুট আর আউটপুট দেখে, ভেতরে struct না class তা জানে না। তাই পুরো ভেতরটা class-এ নতুন করে লেখার পরও একই টেস্ট-ফাইলগুলো চালিয়ে প্রমাণ করা যাবে যে আচরণ বদলায়নি — এটাই বড় মাপের refactoring-এর নিরাপদ পথ।

মূল উপলব্ধি: class কোনো নতুন জাদু নয়; আজ যে নিয়মগুলো আপনি নিজে মেনে চলছেন (যুক্তি এক জায়গায়, আগে যাচাই, ডেটা শুধু নির্দিষ্ট ফাংশন দিয়ে বদলানো), class সেগুলোকে compiler-এর পাহারায় তুলে দেয়।

অনুশীলন — প্রজেক্টকে এক ধাপ এগিয়ে নিন

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

  1. নতুন কমান্ডের spec: শাহীন ভাই চান দাম বদলাতে — price rice 75.00. অংশ ২-এর টেবিলের মতো এর user story আর অন্তত চারটি acceptance criteria লিখুন। একটি কঠিন প্রশ্নও ভাবুন: দাম বদলালে আগের বিক্রির হিসাব কি বদলাবে?

    Story: “দোকানদার হিসেবে আমি একটি মালের দাম বদলাতে চাই, যাতে বাজারদর বাড়লে-কমলে পরের বিক্রি নতুন দামে হয়।” Criteria: (১) জানা মাল আর বৈধ দাম হলে দাম বদলায়, ok: rice 75.00; (২) অজানা মালে no item, কিছুই বদলায় না; (৩) দাম ০, ঋণাত্মক, অক্ষর বা তিন ঘর দশমিক হলে usage বার্তা — এখানেই parse_money কাজে লাগবে; (৪) স্টকের পরিমাণ অপরিবর্তিত থাকে। কঠিন প্রশ্নের উত্তর: না — আগের বিক্রি হয়েছিল পুরোনো দামে, আর sold_paisa-তে সেই টাকাই যোগ হয়েছে; ইতিহাস বদলানো হিসাবের সবচেয়ে বড় পাপ। তাই প্রতিটি বিক্রির আলাদা রেকর্ড রাখা ভালো নকশা (ভাবনার প্রশ্ন ৩)।

  2. আউটপুট অনুমান: অংশ ৫-এর MVP-কে এই ইনপুট দিলে কী ছাপবে? add tea 5 10 / add tea 3 99 / sell tea 6 / sell Tea 1 / report. আউটপুট থেকে নকশার কোন দুটি প্রশ্ন বেরিয়ে আসে?

    আউটপুট: ok: tea +5, ok: tea +3, ok: tea -6, error: no item 'Tea', তারপর tea qty 2 আর sales: 60.00 Tk. দুটি নকশার প্রশ্ন: (১) দ্বিতীয় add-এ দাম ৯৯ লেখা হলেও কোড পুরোনো দাম ১০ রেখে শুধু পরিমাণ বাড়িয়েছে — তাই ৬ × ১০ = ৬০. এটা কি ঠিক? নাকি ভিন্ন দাম দেখলে সতর্ক করা উচিত? spec-এ এর উত্তর নেই, তাই কোড চুপচাপ একটি সিদ্ধান্ত নিয়ে ফেলেছে — ঠিক এমন ফাঁকই spec লেখার সময় ধরা উচিত। (২) “Tea” আর “tea” আলাদা মাল — ছোট-বড় হাতের অক্ষর মানা উচিত কি না? দোকানের জন্য সম্ভবত না; তখন নাম জমা ও খোঁজার আগে সব ছোট হাতের অক্ষরে বদলানো একটি সমাধান।

  3. টেস্ট প্ল্যান বাড়ান: report আর add-এর জন্য অন্তত চারটি নতুন edge case লিখুন — ইনপুট আর প্রত্যাশিত ফলসহ — যা অংশ ৮-এর টেবিলে নেই।

    কয়েকটি ভালো উদাহরণ: (১) add rice 10 72 extra — বাড়তি শব্দ; MVP চুপচাপ উপেক্ষা করে, কিন্তু spec হয়তো বলবে error দাও — in >> std::ws করে in.eof() যাচাই করলে ধরা যায়। (২) add rice 2147483647 1, তারপর add rice 1 1 — পরিমাণ int-এর সর্বোচ্চ ছাড়িয়ে যায়; প্রত্যাশা: overflow নয়, error. (৩) পণ্যের নাম ২০ অক্ষরের — report-এর {:<6} টেবিল ভাঙে কি না, সারি সোজা থাকে কি না। (৪) sell rice 3 — অতিরিক্ত space; >> space এড়িয়ে যায়, তাই সফল হওয়া উচিত। (৫) Windows-এ তৈরি টেস্ট-ফাইল, যার লাইনের শেষে \r থাকে — quit\r তখন “quit”-এর সাথে মেলে না! বাস্তবে এটা খুব সাধারণ বাগ।

  4. নিজে লিখুন (কোড): MVP-তে একটি নতুন কমান্ড value যোগ করুন, যা দোকানে থাকা সব মালের মোট দাম (প্রতিটির qty × price_paisa-এর যোগফল) টাকায় ছাপে। হিসাবটি আলাদা একটি ফাংশনে রাখুন, যা কিছু ছাপে না। enum class-এ নতুন মান যোগ করে switch-এ case দিতে ভুলে গেলে compiler কী বলে, তাও দেখুন।

    ফাংশন: long long stock_value(const std::vector<Item>& stock) { long long t = 0; for (const Item& it : stock) t += it.qty * it.price_paisa; return t; } — const&, কারণ শুধু পড়বে। তারপর enum class Command-এ Value যোগ, parse-এ if (w == "value") return Command::Value;, আর switch-এ case Command::Value: { long long v = stock_value(stock); std::println("stock value: {}.{:02} Tk", v / 100, v % 100); break; }. অংশ ৫-এর সেশনের শেষে চালালে rice ৭ × ৭২ + egg ৩০ × ১৩ = ৫০৪ + ৩৯০ = 894.00 Tk. case ভুলে গেলে GCC বলে enumeration value 'Value' not handled in switch [-Wswitch] — নতুন কমান্ডের জন্য কোথায় কোথায় কাজ বাকি, compiler-ই দেখিয়ে দিল।

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

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

ক্রমএকটি CLI টুল sell rice 3 লাইনটি যে ক্রমে প্রক্রিয়া করে, সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. std::getline দিয়ে পুরো লাইন পড়া
  2. লাইন থেকে std::istringstream বানিয়ে প্রথম শব্দ পড়া
  3. শব্দটিকে Command::Sell-এ রূপান্তর
  4. নাম ও পরিমাণ পড়া আর যাচাই করা, স্টক যথেষ্ট কি না দেখা
  5. স্টক কমানো আর বিক্রির মোট বাড়ানো
  6. ফলাফলের বার্তা ছাপা

একটি হিসাবের প্রোগ্রামে টাকার পরিমাণ double-এর বদলে পয়সায় long long পূর্ণসংখ্যা হিসেবে রাখার প্রধান কারণ কী?

  • পূর্ণসংখ্যার হিসাব double-এর চেয়ে অনেক দ্রুত
  • double-এ অনেক দশমিক মান হুবহু রাখা যায় না, তাই যোগফলে পয়সার গরমিল জমে
  • double যথেষ্ট বড় সংখ্যা ধরতে পারে না
  • std::println double ছাপতে পারে না

C++-এ ব্যবহারকারীর লেখা একটি পুরো লাইন পড়ে তারপর তাকে শব্দ ও সংখ্যায় ভাগ করার প্রচলিত ও নিরাপদ উপায় কোনটি?

  • শুধু std::cin >> a >> b >> c
  • শুধু std::getline(std::cin, line)
  • std::getline দিয়ে লাইন, তারপর std::istringstream থেকে >>
  • প্রতিটি অক্ষর getchar() দিয়ে পড়ে নিজে ভাগ করা

std::istringstream in{"sell rice abc"}; std::string w, n; int q = 0; in >> w >> n >> q; চালানোর পর if (!in)-এর শর্ত কী হবে?

  • সত্য — "abc" সংখ্যা নয়, তাই পড়া ব্যর্থ হয়ে stream ব্যর্থ অবস্থায় যায়
  • মিথ্যা — প্রথম দুটি শব্দ সফলভাবে পড়া গেছে
  • প্রোগ্রাম exception ছুড়ে থেমে যায়
  • মিথ্যা — q-তে "abc"-এর ASCII মান বসে

std::vector<Item> v{...}; Item* p = &v[0]; v.push_back(x); p->qty = 5; — এই কোডের সবচেয়ে বড় সমস্যা কী?

  • &v[0] লেখা অবৈধ, compile হয় না
  • push_back প্রথম উপাদানটি মুছে ফেলে
  • p->qty দিয়ে member বদলানো যায় না
  • capacity বাড়াতে vector সরে গেলে p অচল (dangling) হয়ে যেতে পারে — undefined behavior

একটি CLI প্রোগ্রামকে “ব্যবহারকারীর সাথে কথা বলার স্তর” আর “হিসাবের যুক্তির স্তর”-এ ভাগ করা হলো। কোন নিয়মটি এই নকশার সবচেয়ে বড় লাভ নিশ্চিত করে?

  • যুক্তির প্রতিটি ফাংশন নিজের ফলাফল নিজেই ছেপে দেবে
  • যুক্তির ফাংশন কিছু পড়বে বা ছাপবে না, শুধু ফলাফল ফেরত দেবে
  • প্রতিটি ফাংশন আলাদা একটি .cpp ফাইলে থাকবে
  • দুই স্তর একটি global ভ্যারিয়েবল দিয়ে তথ্য আদান-প্রদান করবে

সফটওয়্যার প্রজেক্টে “regression test” বলতে কী বোঝায়?

  • প্রোগ্রাম কত দ্রুত চলে তা মাপার টেস্ট
  • শুধু প্রজেক্ট শেষে একবার ব্যবহারকারীকে দিয়ে চালানো টেস্ট
  • আগে ধরা পড়া বাগ বা আগের আচরণের জন্য রেখে দেওয়া টেস্ট, যা প্রতিটি পরিবর্তনের পর আবার চালানো হয়
  • শুধু ভুল ইনপুট দিয়ে চালানো টেস্ট

শব্দকোষ

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

মনে রাখুন

  1. চাহিদা → ডেটা মডেল → অংশে ভাগ → মাইলস্টোন → টেস্ট। প্রতিটি ধাপে একটি লিখিত জিনিস; spec-এ “কী”, কোডে “কীভাবে”, আর MVP-র সীমারেখা লিখে রাখুন।
  2. প্রতিটি কমান্ড-লাইন: getline → istringstream → parse → যাচাই → বদল → ছাপা। সব যাচাই আগে, বদল একবারে শেষে।
  3. টাকা পয়সায় long long; দশমিক দাম অক্ষর ধরে পড়ুন, double দিয়ে নয়। প্রতিটি নকশার সিদ্ধান্ত কারণ আর দামসহ লিখে রাখুন।
  4. যুক্তির স্তর কিছু ছাপে না — Result ফেরত দেয়; header-এ ঘোষণা, .cpp-তে সংজ্ঞা, সাহায্যকারী ফাংশন anonymous namespace-এ; মালিক vector, কোনো new নেই।
  5. স্ক্রিপ্ট করা ইনপুট + প্রত্যাশিত আউটপুট = টেস্ট। প্রতিটি বাগের জন্য আগে একটি ব্যর্থ টেস্ট, তারপর সমাধান, তারপর টেস্টটি চিরকাল রেখে দেওয়া। AI লিখুক, কিন্তু spec আর টেস্ট আপনার।

পরের পর্বে: মডিউল ৪ শুরু! আজকের struct Ledger-এর সবচেয়ে বড় দুর্বলতা — যে কেউ qty-কে ঋণাত্মক করে দিতে পারে — সারাতে আসছে class: private ডেটা, public member function, আর compiler-এর পাহারায় রক্ষা করা invariant.

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

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