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

মাঝপথের প্রজেক্ট — দোকানের হিসাবের খাতা, একটি কমান্ড-লাইন টুল

Halfway Project — A Shop Sales Ledger CLI, Built Like an Engineer

Taught in Bangla, this halfway project builds a shop sales ledger as a command-line tool: writing requirements and user stories, designing the data model with structs and enums, splitting work into functions, a menu loop, milestones, and a test plan.

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

  • ব্যবহারকারীর কথা শুনে স্পষ্ট চাহিদা (user story) লেখা, আর প্রথম সংস্করণ থেকে কী বাদ রাখবেন তা ঠিক করা
  • পর্ব ১৬–২২-এর struct, enum, অ্যারে, স্ট্রিং আর ডায়নামিক মেমরি দিয়ে একটি পূর্ণ ডেটা মডেল সাজানো
  • প্রোগ্রামকে তিন স্তরে ভাগ করা — ব্যবহারকারীর সাথে কথা, হিসাবের যুক্তি, আর ডেটা
  • পাঁচটি মাইলস্টোনে প্রজেক্ট বানানো এবং প্রতিটি ধাপে কী টেস্ট করবেন তা আগেই ঠিক করা
  • AI দিয়ে একটি একটি ফাংশন লিখিয়ে টেস্ট দিয়ে বাগ ধরা — একটি বাস্তব উদাহরণসহ

১ · রহিম চাচার খাতা — প্রজেক্টের শুরু যেখান থেকে

মিরপুরে রহিম চাচার একটি ছোট মুদি দোকান। প্রতিদিনের সব বিক্রি লেখা থাকে একটি লাল মলাটের খাতায় — কেউ নগদ দেন, কেউ bKash-এ পাঠান, কয়েকজন “বাকি” রাখেন। রাতে ক্যালকুলেটর নিয়ে বসলে প্রায়ই যোগফল মেলে না।

তাঁর ভাগ্নি নাদিয়া CSE-তে পড়েন। তিনি একটি প্রোগ্রাম বানিয়ে দেবেন বলেছেন, আর এই পর্বে আমরা তাঁর সাথে সেটি বানাব। কিন্তু খেয়াল করুন — এই পর্বের আসল বিষয় কোড নয়। আসল বিষয় হলো একজন ইঞ্জিনিয়ার কীভাবে একটি ফাঁকা পাতা থেকে কাজ করা সফটওয়্যারে পৌঁছান।

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

=== Rahim Store : Sales Ledger ===
1) Add sale   2) List   3) Search   4) Totals   0) Exit
> 1
Item name : Rice 1kg
Quantity  : 2
Unit price: 72.50
Paid by (1=cash 2=bKash 3=due): 2
Saved. (sale #14)
> 4
Cash: 2310.00   bKash: 1845.00   Due: 420.00
Today total: 4575.00 Tk  (14 sales)
মূল ধারণা

CLI দেখতে সাধারণ, কিন্তু এর ভেতরের অংশ — ডেটা রাখা, হিসাব, খোঁজা, ভুল ইনপুট সামলানো — যেকোনো অ্যাপের মতোই। ভালো নকশা হলে একই ভেতরের অংশের ওপর পরে মোবাইল অ্যাপও বসানো যায়। তাই সব মনোযোগ ভেতরের কাঠামোতে।

নাদিয়া সরাসরি কোড লিখতে বসেননি। পাঁচটি ধাপের প্রতিটিতে একটি লিখিত জিনিস তৈরি হয়েছে — “পরে” চেপে দেখুন:

  1. চাহিদাstories.txtকী চাই, কেন চাই
  2. ডেটা মডেলstruct Saleকী তথ্য রাখব
  3. ফাংশনledger.hকে কোন কাজ করবে
  4. বানানোM1 → M5ছোট ধাপে
  5. টেস্ট ও রিভিউtests.txtপ্রমাণ করা
stories.txtচাচার সাথে কথা বলে লেখা চাহিদা
US-1  দোকানদার হিসেবে আমি প্রতিটি বিক্রি লিখে রাখতে চাই,
      যাতে দিনের শেষে কিছু বাদ না পড়ে।
US-2… US-5  তালিকা, মোট (নগদ/bKash/বাকি আলাদা),
            নাম দিয়ে খোঁজা, ভুল ইনপুটে না ভাঙা
বাদ (এখন নয়): ফাইলে সংরক্ষণ, মুছে ফেলা, স্টকের হিসাব
model.txtগল্পের বিশেষ্যগুলো থেকে ডেটা
বিক্রি (Sale)  : পণ্যের নাম, পরিমাণ, একক দাম, কীভাবে টাকা পেলাম
টাকার ধরন      : নগদ | bKash | বাকি          → enum
খাতা (Ledger)  : বিক্রির তালিকা, কয়টি আছে, কয়টির জায়গা আছে
সিদ্ধান্ত: দাম রাখব পয়সায় (72.50 টাকা = 7250), float নয়
ledger.hকোড নয় — শুধু ফাংশনের “চুক্তি”
void ledger_init (Ledger *lg);
int  ledger_add  (Ledger *lg, Sale s);          // 0 = ok, -1 = no memory
long ledger_total(const Ledger *lg, int pay);   // pay = -1 means all
long ledger_find (const Ledger *lg, const char *word, size_t from);
void ledger_free (Ledger *lg);
plan.txtপ্রতিটি মাইলস্টোনের শেষে প্রোগ্রাম চলে
[x] M1  মেনু ঘোরে, 0 চাপলে বের হয়
[x] M2  বিক্রি যোগ ও তালিকা (স্থির অ্যারে)
[ ] M3  খোঁজা ও মোট হিসাব
[ ] M4  দরকারমতো বাড়ে এমন জায়গা (realloc)
[ ] M5  ভুল ইনপুটে না ভাঙা
tests.txtআগে লেখা টেস্ট, পরে চালানো
T1  খালি খাতায় মোট             → 0.00          PASS
T2  পরিমাণে "abc"               → আবার চাইবে    PASS
T3  ৫০ অক্ষরের নাম             → কেটে ৩১ অক্ষর  PASS
T4  ১০০০টি বিক্রি যোগ           → সব আছে, leak নেই PASS
T5  Ctrl+D (ইনপুট শেষ)          → শান্তভাবে বের হয় PASS
  1. ধাপ ১ — চাহিদা। নাদিয়া চাচার আসল সমস্যা শুনে লিখলেন ব্যবহারকারীর ভাষায়, কোডের ভাষায় নয়। সাথে লিখলেন কী এখন বানাবেন না।
  2. ধাপ ২ — ডেটা মডেল। গল্পের বিশেষ্য (বিক্রি, দাম, খাতা) থেকে ঠিক হলো কোন তথ্য রাখতে হবে।
  3. ধাপ ৩ — ফাংশনে ভাগ। প্রতিটি কাজের জন্য একটি ফাংশন, যার ইনপুট আর আউটপুট আগেই লেখা। এটি একটি interface — ভেতরে কী আছে না জেনেও যে কেউ (আপনি, বন্ধু, বা AI) এটি দেখে কাজ করতে পারে।
  4. ধাপ ৪ — ছোট ধাপে বানানো। পুরো প্রোগ্রাম একবারে নয়। পাঁচটি milestone, আর প্রতিটির শেষে প্রোগ্রাম compile হয়ে চলে। এখানে দুটি শেষ, তিনটি বাকি।
  5. ধাপ ৫ — টেস্ট ও রিভিউ। কোন পরিস্থিতিতে কী হওয়া উচিত, তা আগেই লেখা ছিল; এখন চালিয়ে মিলিয়ে দেখা। “চলছে মনে হয়” নয় — “প্রমাণসহ চলে।”

২ · চাহিদা লেখা — user story আর “কী বানাব না”

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

চাহিদা লেখার একটি জনপ্রিয় ও কাজের ছাঁচ হলো user story:

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

“কে” মনে করিয়ে দেয় প্রোগ্রামটি কার জন্য — চাচা প্রোগ্রামার নন, তাই মেনু হবে সহজ। “কেন” বলে আসল উদ্দেশ্য — চাচা মোট দেখতে চান ক্যাশবাক্স মেলাতে, তাই নগদ আর bKash আলাদা দেখানো জরুরি।

প্রতিটি story-র সাথে লিখুন কিছু acceptance criteria — কখন বলবেন কাজটি “হয়েছে”। এগুলো যাচাই করা যায় এমন হতে হবে। “সহজে ব্যবহার করা যাবে” যাচাই করা যায় না; “ভুল সংখ্যা দিলে প্রোগ্রাম বন্ধ না হয়ে আবার চাইবে” যাচাই করা যায়।

নংUser story (দোকানদার হিসেবে আমি…)কখন “হয়েছে” বলব
US-1প্রতিটি বিক্রি লিখে রাখতে চাই, যাতে দিনের শেষে কিছু বাদ না পড়েনাম, পরিমাণ, দাম, পেমেন্টের ধরন নেয়; পরিমাণ বা দাম ০ বা ঋণাত্মক হলে নেয় না
US-2আজকের সব বিক্রির তালিকা দেখতে চাই, যাতে খাতার সাথে মেলাতে পারিযোগ করার ক্রমে দেখায়; খালি হলে “No sales yet” লেখে
US-3আজকের মোট দেখতে চাই — নগদ, bKash, বাকি আলাদা — যাতে ক্যাশবাক্স মেলাতে পারিহাতে হিসাবের সাথে পয়সা পর্যন্ত মেলে; খালি খাতায় ০
US-4পণ্যের নামের অংশ লিখে খুঁজতে চাই, যাতে কোনো বিক্রি দ্রুত পাই“rice” লিখলে “Rice 1kg”-ও পায় (ছোট-বড় হাতের অক্ষর মানে না); না পেলে জানায়
US-5ভুল চাপ দিলেও প্রোগ্রাম যেন বন্ধ না হয়, যাতে আজকের হিসাব হারিয়ে না যায়অক্ষর, খালি লাইন, খুব লম্বা লেখা — কোনোটিতেই crash নেই

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

চাচা আরও চাইলেন স্টকের হিসাব, ক্রেতাভিত্তিক বাকি, মাসিক রিপোর্ট, এমনকি SMS! সবই যৌক্তিক, কিন্তু সব একসাথে বানাতে গেলে কয়েক সপ্তাহেও শেষ হবে না।

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

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

বিক্রি যোগ, তালিকা, নাম দিয়ে খোঁজা, পেমেন্ট অনুযায়ী মোট, ভুল ইনপুট সামলানো।

US-1 থেকে US-5

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

ফাইলে সংরক্ষণ (পর্ব ২৪-এ), বিক্রি মুছে ফেলা বা বদলানো, স্টক, ক্রেতাভিত্তিক বাকি, মাসিক রিপোর্ট।

পরের সংস্করণ

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

৩ · ডেটা মডেল — খাতার একটি লাইন দেখতে কেমন

চাচার লাল খাতার যেকোনো একটি লাইন দেখুন: “চাল ১ কেজি — ২টি — ৭২.৫০ — bKash”. একটি লাইনে চারটি তথ্য। এটাই আমাদের data model-এর শুরু। পর্ব ২২-এ শেখা struct ঠিক এই কাজের জন্যই — কয়েকটি সম্পর্কিত তথ্যকে এক নামে বাঁধা।

typedef enum { PAY_CASH, PAY_BKASH, PAY_DUE } PayMethod;

typedef struct {
    char      item[32];   /* product name, at most 31 chars + '\0' */
    int       qty;        /* how many, always > 0                   */
    long      price;      /* unit price in paisa: 72.50 Tk = 7250    */
    PayMethod pay;        /* cash, bKash or due                      */
} Sale;

typedef struct {
    Sale  *items;         /* heap array, grows when full             */
    size_t count;         /* sales stored now                        */
    size_t capacity;      /* sales that fit before we must grow      */
} Ledger;

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

সিদ্ধান্তকেনদাম কী দিলাম
টাকা পয়সায়, long পূর্ণসংখ্যায়পর্ব ০৫-এ দেখেছেন, 0.1-এর মতো দশমিক floating-point-এ হুবহু রাখা যায় না; হাজারটি যোগে পয়সার গরমিল জমে। পূর্ণসংখ্যার যোগ নির্ভুল।দেখানোর সময় ১০০ দিয়ে ভাগ করে টাকা ও পয়সা আলাদা ছাপতে হয়
পেমেন্ট enum, স্ট্রিং নয়স্ট্রিং হলে “bkash” আর “Bkash” আলাদা হিসাব হবে; enum-এ মাত্র তিনটি বৈধ মান।নতুন ধরন (যেমন Nagad) যোগ করতে কোড বদলাতে হবে
নাম স্থির char[32]আলাদা malloc লাগে না, struct কপি করলেই নামসহ কপি হয়, free ভোলার ভয় নেই।লম্বা নাম কেটে যায়। বাংলা নাম UTF-8-এ প্রতি অক্ষরে ৩ byte নেয়, তাই মাত্র ১০টির মতো বাংলা অক্ষর ধরে
count আর capacity আলাদাপর্ব ২০-এর বাড়তে পারা অ্যারে: কতটি আছে আর কতটির জায়গা আছে, দুটি ভিন্ন প্রশ্ন।দুটিকে সবসময় সঠিক রাখার দায়িত্ব ফাংশনগুলোর

long কত বড়? কমপক্ষে ৩২ bit; Windows-এ ঠিক ৩২ bit, তাই সর্বোচ্চ প্রায় ২১৪ কোটি পয়সা (প্রায় ২ কোটি ১৪ লাখ টাকা)। এক দিনের হিসাবে যথেষ্ট, কিন্তু বছরের হিসাবে long long (কমপক্ষে ৬৪ bit) নিরাপদ। “সংখ্যা সবচেয়ে বড় কত হতে পারে?” — এই প্রশ্নটিও ডেটা মডেলিং।

ডেটা মডেল হলো বাড়ির ভিত। দেয়ালের রং পরে বদলানো সহজ, কিন্তু ভিত বদলাতে পুরো বাড়ি ভাঙতে হয়। টাকা float-এ রেখে ৫০টি ফাংশন লেখার পর ভুল বুঝলে ৫০টিই বদলাতে হবে।

৪ · কাজ ভাগ — ফাংশনের মানচিত্র আর তিন স্তরের নকশা

পর্ব ০২-এ শিখেছেন decomposition — বড় সমস্যাকে ছোট টুকরোয় ভাঙা। আর পর্ব ১৩-এ শিখেছেন single responsibility — একটি ফাংশন, একটি কাজ। এবার দুটোকে একসাথে প্রয়োগ করার পালা।

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

দোকানের খাতা — তিন স্তরের নকশা

UI layer → logic layer → data layer

ui_menu()মেনু দেখায়, পছন্দ পড়ে
ui_add()নাম-দাম জিজ্ঞেস করে
ui_list() / ui_totals()সুন্দর করে ছাপে
read_line() / read_long()নিরাপদ ইনপুট
ledger_add()যোগ করে, দরকারে বড় করে
ledger_total()মোট হিসাব করে
ledger_find()নাম দিয়ে খোঁজে
ledger_init() / ledger_free()শুরু ও শেষ
Ledgeritems, count, capacity
Sale[ ]heap-এ রাখা অ্যারে
পরে: ফাইল (পর্ব ২৪)শুধু এই স্তর বদলাবে
ছবি ২৩.১ — বাঁয়ে যে স্তর ব্যবহারকারীর সাথে কথা বলে, মাঝে হিসাবের যুক্তি, ডানে ডেটা। তীর দেখায় কে কাকে ডাকে — উল্টো দিকে কখনো নয়।

সবচেয়ে গুরুত্বপূর্ণ নিয়ম: মাঝের স্তরের (ledger_) কোনো ফাংশন printf বা scanf করবে না। সে শুধু ডেটা নেয়, হিসাব করে, ফল ফেরত দেয়। ফলে এগুলো কীবোর্ড ছাড়াই টেস্ট করা যায়, আর মোবাইল অ্যাপেও হুবহু আবার ব্যবহার করা যায় — ওয়েটার বদলালে রাঁধুনি বদলাতে হয় না।

এবার প্রতিটি ফাংশনের “চুক্তি” লিখে ফেলা — কী নেয়, কী ফেরত দেয়, কোন চাহিদা পূরণ করে। পর্ব ০২-এর specification-এর মতোই, শুধু ফাংশন পর্যায়ে:

ফাংশনইনপুটআউটপুট / প্রভাবচাহিদা
ledger_initখালি খাতার ঠিকানাcount = 0, capacity = 0, items = NULLসব
ledger_addখাতা, একটি বৈধ Sale০ = সফল, −১ = মেমরি নেই (খাতা অক্ষত)US-1
ledger_totalখাতা (শুধু পড়বে), পেমেন্টের ধরন বা −১পয়সায় মোট; খালি খাতায় ০US-3
ledger_findখাতা, খোঁজার শব্দ, কোথা থেকে খুঁজবেপ্রথম মিলের index, না পেলে −১US-4
ledger_freeখাতামেমরি ফেরত, আবার খালি অবস্থাসব
read_longপ্রশ্ন, সর্বনিম্ন, সর্বোচ্চবৈধ সংখ্যা না পাওয়া পর্যন্ত আবার চায়; ইনপুট শেষ হলে ব্যর্থতা জানায়US-5

খেয়াল করুন, ledger_total নেয় const Ledger * — প্রতিশ্রুতি দেয় খাতা বদলাবে না, ভুল করে বদলাতে গেলে compiler আটকাবে। আর ledger_add ব্যর্থতা জানায় ফেরত মান দিয়ে; ব্যবহারকারীকে কী বলা হবে, সেই সিদ্ধান্ত UI স্তরের।

৫ · ইউজার ফ্লো — মেনু লুপ কীভাবে ঘোরে

প্রতিটি CLI টুলের হৃদয় একটি লুপ: মেনু দেখানো → পছন্দ পড়া → সেই কাজ করা → আবার মেনু। পর্ব ১২-এ এর নাম শুনেছেন — event loop. মোবাইল অ্যাপ বা গেমের ভেতরেও এমন লুপ ঘোরে, শুধু “পছন্দ” আসে টাচ হয়ে।

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

1int main(void) {2    Ledger lg; ledger_init(&lg);3    int choice;4    while ((choice = ui_menu()) != 0) {5        switch (choice) {6        case 1: ui_add(&lg);    break;7        case 2: ui_list(&lg);   break;8        case 4: ui_totals(&lg); break;9        default: puts("No such option.");10        }11    }12    ledger_free(&lg);  puts("Bye!");13    return 0;14}
খাতার অবস্থা
choice
count
capacity
items
  1. শুরু। ledger_init খাতাকে একটি পরিচিত খালি অবস্থায় আনল: কোনো বিক্রি নেই, কোনো জায়গাও নেওয়া হয়নি (items = NULL).
  2. মেনু দেখানো হলো, চাচা চাপলেন 1. ui_menu একটি সংখ্যা ফেরত দিল। 0 নয়, তাই লুপের ভেতরে ঢোকা হলো, আর switch পাঠাল case 1-এ।
  3. প্রথম বিক্রি — “Rice 1kg”, ২টি, ৭২.৫০, নগদ। ui_add প্রশ্ন করে তথ্য নিল, যাচাই করল, তারপর ledger_add-কে দিল। খাতা ভর্তি (০ = ০) ছিল, তাই প্রথমে ২টির জায়গা নেওয়া হলো। UI প্রশ্ন করে, logic জমা রাখে।
  4. দ্বিতীয় বিক্রি — “Eggs”, ১২টি, ১৩.০০, bKash. জায়গা ছিল, তাই বড় করার দরকার হয়নি। এখন খাতা ঠিক ভর্তি (২ = ২)।
  5. চাচা চাপলেন 2 — তালিকা। ui_list খাতা শুধু পড়ল, তাই ডান পাশে কিছু বদলায়নি। দাম রাখা পয়সায় (7250), দেখানো হলো ৭২.৫০.
  6. চাচা চাপলেন 4 — মোট। নগদ: ২ × ৭২.৫০ = ১৪৫.০০, bKash: ১২ × ১৩.০০ = ১৫৬.০০, মোট ৩০১.০০. টেস্ট করার সময় ঠিক এভাবেই হাতে হিসাব করে মেলাতে হয়।
  7. চাচা চাপলেন 0. while-এর শর্ত মিথ্যা হলো, লুপ শেষ। একই জায়গায় পৌঁছানোর আরেকটি পথ হলো EOF — তখনও ui_menu 0 ফেরত দেয়, যাতে প্রোগ্রাম আটকে না থাকে।
  8. পরিষ্কার করে বের হওয়া। ledger_free মেমরি ফেরত দিল। প্রোগ্রাম যে পথেই বের হোক, এই লাইনে পৌঁছাতেই হবে — না হলে memory leak.

এবার মেনু লুপের একটি ছোট, চালানো যায় এমন সংস্করণ দেখুন। এটি M3-এর আদলে ইচ্ছা করে সরল রাখা — স্থির অ্যারে, পূর্ণ টাকা। ইনপুট আগে থেকেই দেওয়া, তাতে কিছু ভুল ইনপুটও আছে। “Run” চাপুন:

#include <stdio.h>
typedef struct { char item[24]; int qty; long taka; } Sale;
int main(void) {
    Sale v[10]; int n = 0, choice = -1; char line[100];
    while (choice != 0) {
        printf("\n1) Add  2) Total  0) Exit\n> ");
        if (fgets(line, sizeof line, stdin) == NULL) break;           /* EOF: stop */
        if (sscanf(line, "%d", &choice) != 1) { puts("Type a number."); continue; }
        if (choice == 1) {
            printf("item qty price: ");
            if (n < 10 && fgets(line, sizeof line, stdin) != NULL &&
                sscanf(line, "%23s %d %ld", v[n].item, &v[n].qty, &v[n].taka) == 3 &&
                v[n].qty > 0 && v[n].taka > 0) printf("saved #%d\n", ++n);
            else puts("Not saved (list full or bad input).");
        } else if (choice == 2) {
            long sum = 0;
            for (int i = 0; i < n; i++) sum += v[i].qty * v[i].taka;
            printf("%d sales, total %ld Tk\n", n, sum);
        } else if (choice != 0) puts("No such option.");
    }
    return 0;
}

ইনপুটে দুটি বৈধ বিক্রির পর ছিল “abc”, ঋণাত্মক পরিমাণ আর অস্তিত্বহীন অপশন 5 — প্রোগ্রাম কোনোটিতেই ভাঙেনি। মূল কৌশল পর্ব ১০-এর: আগে fgets দিয়ে পুরো লাইন পড়া, তারপর sscanf-এর ফেরত মান যাচাই করা। %23s নামকে ২৩ অক্ষরে সীমিত রাখে, যাতে ২৪ ঘরের অ্যারে উপচে না পড়ে। n < 10 শর্তটি খাতা ভর্তি হলে আর লিখতে দেয় না। দুর্বলতাও আছে: নামে space চলে না, ১০টির বেশি বিক্রি রাখা যায় না, দাম শুধু পূর্ণ টাকায় — সমাধান M4 আর M5-এ।

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

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

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

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

  1. M1menuকঙ্কাল
  2. M2add/listপ্রথম আসল কাজ
  3. M3find/totalMVP পূর্ণ
  4. M4reallocসীমা নেই
  5. M5hardeningভাঙে না
M1 · ~৩০ লাইনমেনু ঘোরে, কিন্তু কোনো কাজ করে না
> 1
[add: coming soon]
> 9
No such option.
> 0
Bye!
টেস্ট: 0 চাপলে বের হয় · ভুল সংখ্যায় বার্তা · EOF-এ আটকে থাকে না
M2 · ~৯০ লাইনstruct Sale, স্থির অ্যারে [100]
> 2
No sales yet.
> 1   (Rice, 2, 7250, cash)
Saved. (sale #1)
> 2
1. Rice   2 x 72.50  cash
টেস্ট: খালি তালিকা · ৩টি বিক্রি যোগ করে তালিকা মেলানো
M3 · ~১৫০ লাইনএখানেই MVP সম্পূর্ণ — চাচাকে দেখানো যায়
> 3   search: egg
2. Eggs  12 x 13.00  bKash
> 4
Cash: 145.00  bKash: 156.00  Due: 0.00
টেস্ট: না পেলে "Not found" · খালি খাতায় মোট 0.00 · হাতে হিসাব মেলানো
M4 · ~১৮০ লাইনস্থির [100] বদলে বাড়তে পারা অ্যারে
count=1  capacity=2
count=3  capacity=4      ← ভর্তি হলে দ্বিগুণ
count=5  capacity=8
...
count=1000 capacity=1024
টেস্ট: ১০০০টি বিক্রি · sanitizer-এ leak বা সীমা পেরোনো নেই
M5 · ~২২০ লাইনসব ইনপুট এক জায়গা দিয়ে, যাচাই করে
Quantity  : abc
Please enter a whole number from 1 to 9999.
Quantity  : 99999999999999
Please enter a whole number from 1 to 9999.
Item name : (৫০ অক্ষরের নাম)
Name too long, kept first 31 characters.
টেস্ট: পুরো টেস্ট প্ল্যান (অংশ ৭)
  1. M1 — কঙ্কাল। শুধু মেনু আর লুপ, অপশনগুলো “coming soon” বলে। তবু নিশ্চিত হলো মৌলিক জিনিস: প্রোগ্রাম চলে, লুপ ঘোরে, বের হওয়ার দুই পথই কাজ করে।
  2. M2 — যোগ ও তালিকা। struct Sale আর ১০০ ঘরের স্থির অ্যারে — একটি সচেতন শর্টকাট: আগে যোগ-তালিকার যুক্তি ঠিক করা, মেমরি পরে।
  3. M3 — খোঁজা ও মোট। US-1 থেকে US-4 পূর্ণ — MVP তৈরি। কাজ এখানে থামলেও চাচার হাতে ব্যবহারযোগ্য টুল থাকত। প্রতিটি মাইলস্টোনে ভাবুন: “আজই থামতে হলে হাতে কী থাকবে?”
  4. M4 — বাড়তে পারা জায়গা। শর্টকাটটি এবার শোধ: ভর্তি হলে realloc দিয়ে দ্বিগুণ। ভেতরটা বদলাল, কিন্তু ledger_add-এর চুক্তি একই — তাই আগের টেস্টগুলো আবার চালিয়েই বোঝা গেল কিছু ভাঙেনি।
  5. M5 — ইনপুট শক্ত করা। সব ইনপুট আসে দুটি ফাংশন (read_line, read_long) দিয়ে, যারা পরিসীমা যাচাই করে, লম্বা লেখা সামলায়, EOF বোঝে — পর্ব ১০-এর input validation এক জায়গায়।

M4-এর মূল অংশ চালিয়ে দেখুন — পাঁচবার যোগ, প্রতিবার কয়টি আছে (n) আর কয়টির জায়গা (cap):

#include <stdio.h>
#include <stdlib.h>
typedef struct { char item[16]; int qty; long paisa; } Sale;
typedef struct { Sale *v; size_t n, cap; } Ledger;
int ledger_add(Ledger *L, Sale s) {
    if (L->n == L->cap) {                            /* full: double the room */
        size_t ncap = L->cap ? 2 * L->cap : 2;
        Sale *t = realloc(L->v, ncap * sizeof *t);
        if (t == NULL) return -1;                    /* old list is still safe */
        L->v = t; L->cap = ncap;
    }
    L->v[L->n++] = s;                                /* struct copy, name included */
    return 0;
}
int main(void) {
    Ledger L = { NULL, 0, 0 };
    Sale rice = { "Rice 1kg", 2, 7250 };
    for (int i = 0; i < 5; i++)
        if (ledger_add(&L, rice) == 0) printf("n=%zu cap=%zu\n", L.n, L.cap);
    free(L.v);                                       /* one block, one free */
    return 0;
}

আউটপুটে capacity ২ → ৪ → ৮ হয়, আর realloc ডাকা হয় মাত্র তিনবার — ১ম, ৩য় আর ৫ম যোগের সময়। একটি একটি করে বাড়ালে ১০,০০০ বিক্রিতে ১০,০০০ বার ডাকতে হতো; দ্বিগুণ করলে মাত্র ১৪ বার। সবচেয়ে গুরুত্বপূর্ণ লাইনটি হলো realloc-এর ফল প্রথমে আলাদা পয়েন্টার t-তে রাখা। সরাসরি L->v = realloc(L->v, …) লিখলে মেমরি না পেলে L->v হতো NULL — আগের সব বিক্রির ঠিকানা হারাত (memory leak), চাচার পুরো দিনের হিসাবসহ। এখানে ব্যর্থ হলে খাতা অক্ষত থাকে, শুধু −১ ফেরত আসে। আর যতবারই realloc হোক, ব্লক একটিই — তাই শেষে একটি free.

প্রতিটি মাইলস্টোন শেষে একটি git commit করুন (যেমন git commit -m "M2: add and list sales"). পরে কিছু ভাঙলে আগের চলা অবস্থায় ফিরে যাওয়া যাবে, আর কোন পরিবর্তনে ভেঙেছে তাও দেখা যাবে। Git-এর বিস্তারিত পাবেন ABCL TECH-এর Software Engineering & Git কোর্সে।

৭ · টেস্ট প্ল্যান — প্রোগ্রাম কোথায় কোথায় ভাঙতে পারে

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

নংপরিস্থিতিইনপুটপ্রত্যাশিত ফল
T1খালি খাতাশুরুতেই তালিকা, মোট, খোঁজা“No sales yet”, মোট 0.00, “Not found” — crash নয়
T2সংখ্যার জায়গায় অক্ষরপরিমাণ: abc, 12abcদুটিই বাতিল, আবার চাইবে (12abc-কে ১২ ধরে নেবে না)
T3সীমার বাইরের সংখ্যাপরিমাণ 0, -3, 99999999999999বাতিল; বিশাল সংখ্যায় overflow হয়ে ভুল মান বসবে না
T4খুব লম্বা নাম৫০ অক্ষরের নামপ্রথম ৩১ অক্ষর রাখে; বাকিটা পরের প্রশ্নের উত্তর হয়ে যায় না
T5বাংলা নামচালঠিকভাবে দেখায়; কাটলে অক্ষরের মাঝখানে কাটে না
T6জায়গা বাড়ানোর সীমানাঠিক ২, ৩, তারপর ১০০০টি বিক্রিসব বিক্রি অক্ষত; sanitizer-এ কোনো error বা leak নেই
T7খোঁজার খুঁটিনাটিRICE, ice, খালি লেখাদুটিই “Rice 1kg” পায়; খালি লেখায় আবার চায়
T8ইনপুট হঠাৎ শেষনাম চাওয়ার সময় Ctrl+D / Ctrl+Zশান্তভাবে বের হয়, মেমরি ফেরত দেয়, অসীম লুপ নয়

T5 নিয়ে একটু ভাবুন। UTF-8-এ “চ” একটিমাত্র অক্ষর, কিন্তু মেমরিতে ৩ byte. যদি ৩১ byte-এ কাটতে গিয়ে কোনো অক্ষরের ৩ byte-এর মাঝখানে কাটা পড়ে, তাহলে শেষে একটি ভাঙা অক্ষর দেখাবে — পর্ব ০৫-এর সেই mojibake. এমন বাগ ধরা পড়ে কেবল আগে থেকে ভাবলে: “বাংলা নাম দিলে কী হবে?”

যুক্তির ফাংশন টেস্ট করা — কীবোর্ড ছাড়াই

অংশ ৪-এর নিয়মটি এবার ফল দিচ্ছে: ledger_ ফাংশনগুলো একটি ছোট টেস্ট প্রোগ্রাম থেকে সরাসরি ডাকা যায়, হাতে টাইপ ছাড়াই। একে বলে unit test:

#include <assert.h>
/* ... include ledger.h ... */
void test_total(void) {
    Ledger lg; ledger_init(&lg);
    assert(ledger_total(&lg, -1) == 0);                 /* T1: empty ledger   */
    Sale a = { "Rice 1kg", 2, 7250, PAY_CASH  };
    Sale b = { "Eggs",    12, 1300, PAY_BKASH };
    assert(ledger_add(&lg, a) == 0 && ledger_add(&lg, b) == 0);
    assert(ledger_total(&lg, -1)        == 30100);      /* 301.00 Tk by hand  */
    assert(ledger_total(&lg, PAY_BKASH) == 15600);
    ledger_free(&lg);
}

assert-এর শর্ত মিথ্যা হলে প্রোগ্রাম থেমে লাইন নম্বর জানায়। টেস্ট চালান sanitizer চালু রেখে, যাতে সীমা পেরোনো বা leak চোখের আড়ালে না থাকে (Linux, Mac বা WSL-এ):

gcc -Wall -Wextra -g -fsanitize=address,undefined test_ledger.c ledger.c -o test_ledger && ./test_ledger
মূল ধারণা

বাগ ধরা পড়লে ঠিক করার আগে তার জন্য একটি টেস্ট লিখুন, যা এখন ব্যর্থ হয়। ঠিক করার পর সেটি পাস করবে — আর টেস্টটি চিরকাল রেখে দিন। এর নাম regression test — ছয় মাস পর কেউ (বা AI) ভুল করে পুরোনো বাগ ফিরিয়ে আনলে প্রথম দিনেই ধরা পড়বে।

৮ · AI-এর যুগে — একবারে একটি ফাংশন, আপনার স্পেক দিয়ে

AI-কে পুরো সফটওয়্যার লিখতে বললে কয়েকশো লাইনের চমৎকার-দেখতে কোড পাবেন — কিন্তু কোন সিদ্ধান্ত কেন, কোন edge case বাদ পড়েছে, জানবেন না। অথচ সেই কোডের দায় আপনার।

পেশাদার পথ: নকশা, চুক্তি আর টেস্ট আপনার; একটি একটি ফাংশনের খসড়া AI-এর; যাচাই আবার আপনার। ছোট টুকরোয় AI-এর ভুল কম হয়, আর প্রতিটি লাইন পড়ে বোঝা যায়।

একটি ফাংশন চাওয়ার কাঠামো — খেয়াল করুন, ডেটা মডেল, চুক্তি, সীমাবদ্ধতা আর টেস্ট সবই আপনি দিচ্ছেন:

আমি C-তে (C11, gcc -Wall -Wextra) একটি দোকানের হিসাবের CLI বানাচ্ছি। আমার ডেটা মডেল: typedef enum { PAY_CASH, PAY_BKASH, PAY_DUE } PayMethod; typedef struct { char item[32]; int qty; long price; PayMethod pay; } Sale; typedef struct { Sale *items; size_t count; size_t capacity; } Ledger; শুধু এই একটি ফাংশন লেখো: long ledger_find(const Ledger *lg, const char *word, size_t from); চুক্তি: - items[from] থেকে শুরু করে প্রথম যে বিক্রির নামে word আছে, তার index ফেরত দাও; না পেলে -1. - ছোট-বড় হাতের অক্ষর মানবে না ("RICE" দিয়ে "Rice 1kg" পাবে)। - lg বা word NULL হলে, word খালি হলে, বা from >= count হলে -1. - printf/scanf করবে না, malloc করবে না, খাতা বদলাবে না। কোডের পরে এই টেস্টগুলোর প্রত্যাশিত ফল বলো: খালি খাতা; "ice"; "RICE"; from = count. প্রতিটি লাইন কেন লিখেছ, সহজ বাংলায় ছোট করে ব্যাখ্যা দাও।

AI কোড দিলেও কাজ শেষ নয়। নিচে AI-এর লেখা একটি ledger_add — দেখতে নির্ভুল, warning-ও নেই, কিন্তু সীমানার একটি টেস্টে ধরা পড়ল:

1/* AI: ledger_init() starts with capacity 4 */2int ledger_add(Ledger *lg, Sale s) {3    if (lg->count > lg->capacity) {   /* BUG */4        size_t nc = lg->capacity * 2;5        Sale *t = realloc(lg->items, nc * sizeof *t);6        if (t == NULL) return -1;7        lg->items = t; lg->capacity = nc;8    }9    lg->items[lg->count++] = s;10    return 0;11}
টেস্ট চলার সময়
count
capacity
শর্ত
লেখা হলো
  1. AI-এর খসড়া এলো। realloc আলাদা পয়েন্টারে, NULL যাচাই — চোখে পড়ার মতো ভুল নেই। AI-এর ledger_init শুরুতেই ৪টির জায়গা নেয়।
  2. Compile — শূন্য warning. কিন্তু মনে রাখুন: compiler ব্যাকরণ দেখে, যুক্তি নয়। ভুল তুলনা চিহ্ন ব্যাকরণগতভাবে একদম বৈধ।
  3. T6a: একটি বিক্রি যোগ। শর্ত 0 > 4 মিথ্যা, লেখা হলো items[0]-এ। পাস — সহজ টেস্ট প্রায় সবসময় পাস করে।
  4. T6b: ৪টি বিক্রি। শেষটি লেখা হলো items[3]-এ — শেষ বৈধ ঘর। এখনো পাস। খাতা এখন ঠিক ভর্তি: count = capacity = 4.
  5. T6c: পঞ্চম বিক্রি — সীমানা পেরোনো। শর্ত 4 > 4 মিথ্যা! তাই বড় না করেই লেখা হলো items[4]-এ, যা বরাদ্দ করা জায়গার বাইরে — buffer overflow. Sanitizer ছাড়া হয়তো প্রোগ্রাম চুপচাপ অন্য ডেটা নষ্ট করে চলতেই থাকত।
  6. ঠিক করা ও পাহারা বসানো। শর্ত হবে count == capacity (“ভর্তি হলে বড় করো”)। এক অক্ষরের off-by-one ভুল। এখন সব টেস্ট পাস, আর T6c চিরকালের জন্য regression test হয়ে থাকল। সীমানার আগে-পরে (৩, ৪, ৫) টেস্ট না থাকলে বাগটি ধরা পড়ত চাচার দোকানে।

এই ধরনের প্রজেক্টে AI সাধারণত যে ভুলগুলো করে

AI-এর ভুলফলকীভাবে ধরবেন
p = realloc(p, …) সরাসরি একই পয়েন্টারেমেমরি না পেলে পুরো খাতা হারায়রিভিউতে realloc-এর প্রতিটি লাইন দেখুন
টাকা float/double-এমোটে পয়সার গরমিলঅনেকগুলো 0.10 যোগ করে টেস্ট
scanf("%d")-এর ফেরত মান উপেক্ষাঅক্ষর দিলে অসীম লুপT2 টেস্ট
strcpy দিয়ে নাম কপিলম্বা নামে buffer overflowT4 টেস্ট + sanitizer
logic ফাংশনের ভেতরে printfটেস্ট করা ও আবার ব্যবহার কঠিনঅংশ ৪-এর নিয়ম ধরে রিভিউ
বের হওয়ার কোনো পথে free নেইmemory leakT8 টেস্ট + sanitizer

AI-এর দেওয়া প্রতিটি ফাংশন গ্রহণের আগে — চেকলিস্ট

৯ · রিভিউ, refactoring আর “শেষ” মানে কী

সব টেস্ট পাস করলেও একজন ইঞ্জিনিয়ার আরেকবার থামেন। Code review মানে কোড অন্য চোখে পড়া — বন্ধুকে দিয়ে, অথবা এক দিন পর নিজেই নতুন চোখে। প্রশ্ন: নামগুলো কি অর্থ বলে দেয়? একই কোড দুবার আছে কি? কোনো ফাংশন দুটি কাজ করছে কি?

নাদিয়ার রিভিউতে ধরা পড়ল, পরিমাণ, দাম আর পেমেন্টের ধরন — তিন জায়গায় প্রায় একই “সংখ্যা চাওয়া, যাচাই করা, ভুল হলে আবার চাওয়া” কোড লেখা। তিনি তিনটিকে একটি read_long(prompt, min, max, &out) ফাংশনে আনলেন। আচরণ একটুও বদলাল না, কিন্তু কোড ছোট ও পরিষ্কার হলো। এর নাম refactoring — আচরণ এক, গঠন উন্নত। টেস্টগুলো আগে থেকে ছিল বলেই এটি নিরাপদে করা গেল।

Technical debt অনেকটা দোকানের বাকির মতো। M2-এর স্থির অ্যারে ছিল দ্রুত এগোনোর জন্য নেওয়া ঋণ — লিখে রাখা, M4-এ শোধ। লিখে রাখা বাকি সমস্যা নয়; ভুলে যাওয়া বাকিই একদিন বিশাল অঙ্ক হয়ে দাঁড়ায়।

প্রজেক্ট “শেষ” কখন? অনুভূতি দিয়ে নয়, লিখিত definition of done দিয়ে:

শর্তকীভাবে প্রমাণ করবেনগুরুত্ব
US-1 থেকে US-5 সব কাজ করেএকটি demo সেশনের লেখা (terminal-এর কপি)৩০%
শূন্য warninggcc -Wall -Wextra-এর আউটপুট১৫%
টেস্ট প্ল্যানের সব টেস্ট পাসT1–T8-এর ফলের তালিকা১৫%
মেমরি নিরাপদ — leak বা সীমা পেরোনো নেইsanitizer চালু রেখে টেস্টের আউটপুট১৫%
পরিষ্কার নকশা — তিন স্তর, ছোট ফাংশনফাংশনের মানচিত্র (অংশ ৪-এর মতো)১৫%
README — কীভাবে build ও ব্যবহার করবেন, সীমাবদ্ধতা কীREADME ফাইল১০%
চাহিদা লিখুন → ডেটা সাজান → কাজ ভাগ করুন → ছোট ধাপে বানান → প্রমাণ করুন
কোড এই পথের একটি অংশ মাত্র — পুরো পথটি আপনার।

প্রথম আপগ্রেড ঠিক করাই আছে: পর্ব ২৪-এ খাতা ফাইলে জমা থাকবে — আর তিন স্তরের নকশার কারণে বদলাতে হবে শুধু ডেটার স্তর।

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

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

প্র ০১পুরো প্রোগ্রাম একবারে লিখে শেষে টেস্ট করলেই তো সময় বাঁচে — তাহলে মাইলস্টোনে ভাগ করে বানানো কেন?

সময় বাঁচে মনে হয়, কিন্তু বাস্তবে উল্টো। তিনটি কারণ আছে।

  • ভুল খোঁজার জায়গা ছোট থাকে: ৪০০ লাইন একসাথে লিখলে ভুল যেকোনো জায়গায় থাকতে পারে, আর কয়েকটি ভুল একে অপরকে ঢেকে রাখে। প্রতি ৩০–৫০ লাইন পরপর চালালে নতুন ভুল প্রায় নিশ্চিতভাবে শেষ যোগ করা অংশেই।
  • সবসময় হাতে একটি কাজ করা জিনিস থাকে: “৯০% শেষ” কিন্তু একটুও চলে না এমন কোডের মূল্য ব্যবহারকারীর কাছে শূন্য।
  • শেখা আগে আসে: M2 দেখে চাচা হয়তো বলতেন, “নামের বদলে পণ্যের কোড দিয়ে খুঁজতে চাই।” তখন জানলে দিক বদলানো সহজ; পুরোটা বানানোর পর জানলে অনেক কিছু ভাঙতে হতো।

শিল্পজগতেও এই শিক্ষা দামি অভিজ্ঞতায় এসেছে। ১৯৭৫ সালে Fred Brooks তাঁর “The Mythical Man-Month” বইয়ে IBM-এর বিশাল OS/360 প্রজেক্টের অভিজ্ঞতা লেখেন — বড় সিস্টেম একবারে বানাতে গেলে সময় ও খরচ কীভাবে নিয়ন্ত্রণের বাইরে যায়। পরের দশকগুলোতে “অল্প অল্প করে বানাও, প্রতিবার দেখাও” পদ্ধতি — iterative development, পরে Agile — মূলধারায় আসে; আজ প্রতিটি ছোট পরিবর্তনের আগে স্বয়ংক্রিয় টেস্ট চলে (পর্ব ০৪-এ দেখা CI).

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

মূল উপলব্ধি: সফটওয়্যারকে সবসময় চলা অবস্থায় রাখুন। ছোট ধাপ মানে ছোট ঝুঁকি, দ্রুত ভুল ধরা, আর যেকোনো মুহূর্তে দেখানোর মতো কিছু।

প্র ০২চাচার সব চাহিদাই যৌক্তিক। তাহলে কোনটি বানাব না, সেটা কীভাবে ঠিক করবেন?

এটি কঠিন সিদ্ধান্ত, কারণ প্রতিটি “না” কাউকে হতাশ করে। কয়েকটি কার্যকর প্রশ্ন:

  • সবচেয়ে বড় কষ্ট কোনটি? চাচার ঘুম কাড়ছে যোগফল না মেলা; SMS না থাকলে তাঁর দিন আটকায় না। প্রথম সংস্করণ সবচেয়ে বড় কষ্টটি দূর করবে — এটাই MVP-র মূল কথা। Eric Ries-এর “The Lean Startup” (২০১১) বইয়ের মাধ্যমে ধারণাটি ব্যাপক পরিচিতি পায়: সবচেয়ে কম খরচে আসল ব্যবহারকারীর কাছ থেকে শেখা।
  • খরচ কত, আর কী কী জটিল করবে? “বিক্রি মুছে ফেলা” শুনতে ছোট, কিন্তু মুছলে index বদলায়, খোঁজার ফল বদলায়। প্রতিটি সুবিধা বাকি সবকিছুর জটিলতাও বাড়ায়।
  • পরে যোগ করা কি কঠিন হবে? কিছু সিদ্ধান্ত পরে বদলানো খুব কঠিন (যেমন টাকা কীভাবে রাখবেন) — সেগুলো আগেই ঠিক করুন। কিছু সুবিধা পরে সহজেই যোগ হয় (যেমন নতুন মেনু অপশন) — সেগুলো অপেক্ষা করতে পারে।

Extreme Programming আন্দোলনের একটি নীতি আছে — YAGNI: “You Aren’t Gonna Need It”. অর্থাৎ ভবিষ্যতে হয়তো লাগবে ভেবে আজ কিছু বানাবেন না। আগাম বানানো সুবিধা প্রায়ই কেউ ব্যবহার করে না, অথচ তার রক্ষণাবেক্ষণের খরচ চলতেই থাকে।

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

মূল উপলব্ধি: ভালো প্রকৌশল শুধু কী বানাবেন তা নয়, কী বানাবেন না তা জানাও। লিখিত MVP আর লিখিত “পরে” তালিকা — এ দুটিই scope creep-এর সবচেয়ে কার্যকর প্রতিরোধ।

প্র ০৩যে কোড আপনি নিজে লেখেননি (AI লিখেছে), তা রিভিউ করবেন কীভাবে — যখন সেটা দেখতে নিখুঁত লাগে?

AI-এর কোডের বিশেষ বিপদ হলো, এটি আত্মবিশ্বাসী দেখায় — সুন্দর নাম, পরিষ্কার মন্তব্য। অংশ ৮-এর বাগটি ঠিক এমনই ছিল: সব নিরাপত্তা-কৌশল আছে, শুধু একটি তুলনা চিহ্ন ভুল। তাই রিভিউ হতে হবে কাঠামোবদ্ধ, অনুভূতিনির্ভর নয়।

  • কোড দেখার আগে চুক্তি আর টেস্ট আপনার হাতে থাকুক। তাহলে যাচাই করবেন “চুক্তি মানছে কি না”, শুধু “দেখতে ঠিক লাগছে কি না” নয়।
  • সীমানার দিকে আগে তাকান। লুপের শর্ত, অ্যারের index, < বনাম <=, খালি ইনপুট, NULL — বেশিরভাগ বাগ লুকিয়ে থাকে এখানে। প্রতিটির জন্য মাথায় একটি ছোট উদাহরণ চালান: “count = 4, capacity = 4 হলে কী হবে?”
  • সম্পদের হিসাব মেলান। প্রতিটি malloc/realloc থেকে প্রতিটি সম্ভাব্য বের হওয়ার পথ পর্যন্ত আঙুল দিয়ে অনুসরণ করুন — সব পথে কি free আছে?
  • যন্ত্রকে কাজে লাগান। -Wall -Wextra, sanitizer, আর টেস্ট — এরা ক্লান্ত হয় না, আত্মবিশ্বাসী চেহারায় বিভ্রান্তও হয় না।
  • ব্যাখ্যা চান, তারপর যাচাই করুন। AI-কে জিজ্ঞেস করুন “এই লাইনটি না থাকলে কী হতো?” তবে সেই ব্যাখ্যাও যাচাই করুন — AI ভুল কোডেরও বিশ্বাসযোগ্য ব্যাখ্যা দিতে পারে।

বড় সফটওয়্যার কোম্পানিগুলোতে সাধারণত অন্য কেউ রিভিউ না করা পর্যন্ত কোনো কোড মূল প্রজেক্টে ঢোকে না। AI একজন দ্রুত কিন্তু মাঝে মাঝে অসাবধান “লেখক” — রিভিউয়ের দায়িত্ব তাই আরও বেড়েছে।

মূল উপলব্ধি: যে কোড আপনি ব্যাখ্যা করতে পারেন না, তা আপনার প্রজেক্টে রাখবেন না। AI কোড লেখার খরচ কমিয়েছে, কিন্তু কোড বোঝা ও যাচাইয়ের দায়িত্ব কমায়নি — বরং সেটাই এখন আসল দক্ষতা।

প্র ০৪ডেটা, হিসাবের যুক্তি আর ব্যবহারকারীর সাথে কথা — এই তিনটিকে আলাদা রাখার এত জোর কেন? ছোট প্রোগ্রামে তো সব main()-এ রাখলেই চলে।

২০০ লাইনের প্রোগ্রামে সব main()-এ রাখলে আজ চলবে ঠিকই। কিন্তু সফটওয়্যার কখনো একই জায়গায় থেমে থাকে না। দেখুন চাচার খাতার সামনে কী কী পরিবর্তন অপেক্ষা করছে:

  • পর্ব ২৪ — ফাইলে সংরক্ষণ: শুধু ডেটার স্তর বদলাবে। মেনু জানবেই না যে ডেটা এখন ফাইল থেকে আসছে।
  • মোবাইল অ্যাপ বা ওয়েবসাইট: UI পুরোপুরি বদলাবে, কিন্তু ledger_total হুবহু একই থাকবে — হিসাব তো একই!
  • নতুন পেমেন্ট মাধ্যম (Nagad): enum-এ একটি মান আর মেনুতে একটি অপশন — বাকি সব আগের মতো।

সব মিশিয়ে লিখলে প্রতিটি পরিবর্তনে পুরো প্রোগ্রাম নাড়াতে হতো। আরও দুটি সুবিধা: টেস্ট: যে ফাংশন scanf করে না, তাকে কীবোর্ড ছাড়াই হাজারবার টেস্ট করা যায় (অংশ ৭)। দলগত কাজ: চুক্তি ঠিক থাকলে একজন UI, আরেকজন logic একসাথে লিখতে পারেন, কেউ কারও কাজ আটকে রাখেন না।

ধারণাটি পুরোনো ও সুপ্রতিষ্ঠিত। ১৯৭০-এর দশকের শেষে Xerox PARC-এ Trygve Reenskaug Smalltalk-এর জন্য Model–View–Controller (MVC) নকশার কথা লেখেন — ডেটা (Model), দেখানো (View) আর ব্যবহারকারীর ইনপুট সামলানো (Controller) আলাদা। আজকের বহু ওয়েব ও মোবাইল ফ্রেমওয়ার্ক এই চিন্তার উত্তরসূরি। আমাদের তিন স্তর তারই একটি সরল রূপ।

তবে ২০ লাইনের এককালীন স্ক্রিপ্টে তিন স্তর বাড়াবাড়ি। নীতিটি মূল্যবান তখনই, যখন প্রোগ্রাম বাঁচবে ও বদলাবে — দোকানের খাতার মতো।

মূল উপলব্ধি: ভালো নকশা আজকের কোড ছোট করার জন্য নয়, আগামীর পরিবর্তন সস্তা করার জন্য। যে অংশগুলো আলাদা কারণে বদলায়, সেগুলোকে আলাদা রাখুন।

প্র ০৫চাচার দোকান বড় হয়ে সুপারশপ হলো — ১০,০০০ পণ্য, বছরে লক্ষাধিক বিক্রি। আমাদের নকশার কোথায় চাপ পড়বে?

আগে একটি ভুল ধারণা ভাঙি: ১০,০০০ কম্পিউটারের কাছে খুব ছোট সংখ্যা। একটি Sale প্রায় ৫০ byte, তাই ১০,০০০টি মাত্র আধা মেগাবাইটের মতো, আর তাতে linear search চোখের পলকেরও কম সময় নেয়। চাপ আসে আরও বড় সংখ্যায়, বা একই কাজ বারবার করলে।

  • খোঁজা: বছরে কয়েক লক্ষ বিক্রি, আর প্রতিটি নতুন বিক্রির সময় পুরো তালিকা খোঁজা হলে মোট কাজ দ্রুত বাড়ে। পর্ব ০২-এর time complexity এখানে বাস্তব হয়ে ওঠে: নাম অনুযায়ী সাজানো থাকলে binary search-এ ১০,০০০-এর মধ্যে খুঁজতে লাগে মাত্র ১৪টির মতো তুলনা। আরও দ্রুত চাইলে hash table — DSA কোর্সের বিষয়।
  • বাড়ানোর খরচ: realloc-এ পুরো অ্যারে নতুন জায়গায় কপি হতে পারে, আর পুরোনো ঠিকানা অচল হয়ে যেতে পারে। কোথাও &lg.items[5] জমিয়ে রাখলে সেটি তখন ঝুলন্ত পয়েন্টার।
  • স্থায়িত্ব: লক্ষাধিক বিক্রি প্রোগ্রাম বন্ধ হলেই হারানো চলে না। প্রথমে ফাইল (পর্ব ২৪), পরে ডেটাবেস — যা লোডশেডিংয়েও ডেটা অক্ষত রাখার কাজ ভেতরেই করে।
  • একাধিক কাউন্টার: তিনটি কাউন্টার একই মুহূর্তে একই খাতায় লিখলে লাগে concurrency-র নিয়ম, যা আমাদের নকশায় নেই।

লক্ষ করুন, তিন স্তরের নকশার কারণে এর বেশিরভাগ পরিবর্তন ডেটা স্তরেই সীমাবদ্ধ থাকবে — মেনু প্রায় একই থাকবে।

মূল উপলব্ধি: আগে মাপুন, তারপর অপ্টিমাইজ করুন। আজকের সমস্যার জন্য সরল নকশা বানান, কিন্তু এমনভাবে যাতে কালকের চাপের জায়গাটি (খোঁজা, সংরক্ষণ) আলাদা করে বদলানো যায়।

অনুশীলন — পুরো প্রজেক্ট, চারটি ধাপে

এটিই আপনার মাঝপথের প্রজেক্ট — এ পর্যন্ত যা শিখেছেন, সব একসাথে। প্রতিটি ধাপের শেষে প্রোগ্রাম যেন চলে — উত্তরে পুরো কোড নয়, পদ্ধতি দেওয়া আছে। আগে নিজে চেষ্টা করুন।

  1. M1 + M2 — কঙ্কাল, যোগ আর তালিকা: অংশ ৩-এর ডেটা মডেল দিয়ে শুরু করুন। মেনু লুপ বানান (0 আর EOF — দুই পথেই বের হবে), তারপর ১০০ ঘরের স্থির অ্যারেতে বিক্রি যোগ ও তালিকা দেখানো। দাম ব্যবহারকারী লিখবেন টাকায় (যেমন 72.50), কিন্তু রাখবেন পয়সায়।

    পদ্ধতি: প্রথমে শুধু main আর ui_menu লিখে M1 চালিয়ে দেখুন। তারপর ledger_add (এখনো স্থির অ্যারে, ভর্তি হলে −১) আর ui_add, ui_list. দাম পড়ার সহজ ও নির্ভুল উপায়: লাইনটি fgets দিয়ে পড়ে sscanf(line, "%ld.%2ld", &taka, &ps)-এর মতো করে টাকা আর পয়সা আলাদা পড়া (তবে 72.5 মানে ৫০ পয়সা আর 72.05 মানে ৫ পয়সা — দুটিতেই ps হয় একই সংখ্যা, তাই পয়সার অংশে কয়টি অঙ্ক পড়া হলো তা %n দিয়ে গুনে নিন; এক অঙ্ক হলে ১০ দিয়ে গুণ করতে হবে)। double-এ পড়ে ১০০ দিয়ে গুণ করে পূর্ণসংখ্যায় নেবেন না — যেমন 0.29 * 100 হিসাব হয় 28.999999…, ফলে পূর্ণসংখ্যায় নিলে ২৯-এর বদলে ২৮ পয়সা আসে।

    যাচাই: খালি তালিকা, ৩টি বিক্রি, আর ১০১তম বিক্রি (ভর্তি হলে স্পষ্ট বার্তা)।

  2. M3 — খোঁজা আর পেমেন্টভিত্তিক মোট: ledger_find (ছোট-বড় হাতের অক্ষর না মেনে নামের অংশ দিয়ে) আর ledger_total (সব, বা নির্দিষ্ট পেমেন্ট) লিখুন। একই শব্দের একাধিক মিল থাকলে সবগুলো দেখাতে হবে।

    পদ্ধতি: দুটি স্ট্রিংকে ছোট হাতের অক্ষরে তুলনা করার একটি সাহায্যকারী ফাংশন বানান (tolower দিয়ে, অক্ষরে অক্ষরে), যা বলবে একটি স্ট্রিং আরেকটির ভেতরে আছে কি না। ledger_find from থেকে লুপ চালিয়ে প্রথম মিলের index দেবে। সব মিল দেখাতে UI স্তরে লুপ: i = ledger_find(lg, w, 0), তারপর i = ledger_find(lg, w, i + 1), যতক্ষণ না −১.

    যাচাই: চাচার সেশনের দুটি বিক্রি দিয়ে হাতে হিসাব — মোট ৩০১০০ পয়সা, bKash ১৫৬০০. খালি খাতায় ০, না পেলে “Not found”. ledger_total-এর জন্য অংশ ৭-এর মতো assert টেস্ট লিখুন।

  3. M4 + M5 — বাড়তে পারা খাতা আর শক্ত ইনপুট: স্থির অ্যারে বদলে realloc দিয়ে দ্বিগুণ হওয়া খাতা বানান, সাথে ledger_free. তারপর সব ইনপুট আনুন দুটি ফাংশন দিয়ে: read_line আর read_long(prompt, min, max, &out).

    পদ্ধতি: ledger_add-এর চুক্তি একই রেখে ভেতরে ledger_grow.c-এর কৌশল বসান, তারপর আগের সব টেস্ট আবার চালান।

    read_line: fgets-এর পর শেষে '\n' না থাকলে লাইনটি বেশি লম্বা — বাকি অক্ষর getchar দিয়ে ফেলে দিন। read_long: strtol ব্যবহার করুন, errno দিয়ে অতিরিক্ত বড় সংখ্যা ধরুন, আর শেষ পয়েন্টার দেখে নিশ্চিত হোন সংখ্যার পরে আর কিছু নেই (তাহলে 12abc বাতিল হবে)। EOF পেলে ব্যর্থতা ফেরত দিন, যাতে প্রোগ্রাম পরিষ্কারভাবে বের হয়।

  4. টেস্ট, AI অডিট আর README: অংশ ৭-এর T1–T8 চালিয়ে ফলের তালিকা বানান। তারপর একটি ফাংশন (যেমন ledger_find) অংশ ৮-এর প্রম্পট দিয়ে AI-কে লিখতে দিন, চেকলিস্ট ধরে রিভিউ করুন, আর আপনার নিজের সংস্করণের সাথে তুলনা করুন। শেষে README লিখুন।

    পদ্ধতি: টেস্ট চালান sanitizer চালু রেখে (Linux, Mac বা WSL-এ -fsanitize=address,undefined দিয়ে)। প্রতিটি ব্যর্থ টেস্টের জন্য লিখুন: কী হলো, কারণ কী, কীভাবে ঠিক করলেন — আর সেটিকে regression test হিসেবে রেখে দিন।

    AI অডিটে প্রায়ই পাবেন: NULL বা from >= count যাচাই নেই, ছোট হাতের অক্ষরে বদলাতে নেওয়া মেমরির free নেই, বা নিজে থেকে printf — যা চুক্তি ভাঙে। README-তে রাখুন: কীভাবে build করবেন (পুরো gcc কমান্ড), একটি উদাহরণ সেশন, আর সীমাবদ্ধতা (“প্রোগ্রাম বন্ধ করলে ডেটা মুছে যায় — পর্ব ২৪-এ সমাধান”)।

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

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

ক্রমএকটি প্রজেক্টের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 ব্যবহারকারীর সাথে কথা বলে user story লেখা
  2. 🧱 struct আর enum দিয়ে ডেটা মডেল ঠিক করা
  3. 🧩 ফাংশনে কাজ ভাগ করে প্রতিটির চুক্তি লেখা
  4. 🏗️ মাইলস্টোন ধরে ধরে বানানো, প্রতিটির শেষে চালানো
  5. ✅ টেস্ট প্ল্যান চালানো, রিভিউ আর refactoring

নিচের কোনটি সবচেয়ে কার্যকর user story?

  • “প্রোগ্রামে realloc দিয়ে বাড়তে পারা অ্যারে থাকবে।”
  • “দোকানদার হিসেবে আমি আজকের মোট নগদ ও bKash আলাদা দেখতে চাই, যাতে ক্যাশবাক্স মেলাতে পারি।”
  • “প্রোগ্রামটি সহজ আর সুন্দর হবে।”
  • “মোট বিক্রি দেখানো।”

দোকানের খাতায় টাকা পয়সায়, long পূর্ণসংখ্যায় রাখার মূল কারণ কী?

  • float-এর চেয়ে কম মেমরি লাগে
  • float ছাপানো যায় না বা খুব ধীর
  • দশমিক টাকা float-এ হুবহু রাখা যায় না, অনেক যোগে গরমিল জমে
  • C-তে টাকার জন্য long ব্যবহার করা বাধ্যতামূলক

AI লিখেছে: lg->items = realloc(lg->items, n * sizeof *lg->items); এতে সমস্যা কী?

  • মেমরি না পেলে items NULL হয়ে পুরোনো সব বিক্রির ঠিকানা হারিয়ে যায়
  • realloc পুরোনো ডেটা কপি করে না
  • sizeof ভুল জায়গায় ব্যবহার হয়েছে
  • এটি compile-ই হবে না

মাইলস্টোন M2 “শেষ” বলার আগে কোনটি অবশ্যই সত্য হতে হবে?

  • প্রোগ্রামের সব সুবিধা কাজ করে
  • কোড অন্তত ২০০ লাইন হয়েছে
  • সব কোড refactoring করা হয়েছে
  • প্রোগ্রাম warning ছাড়া compile হয়ে চলে, আর যোগ-তালিকার টেস্ট পাস করে

ledger_total-এর ভেতরে printf না রেখে মোট ফেরত দেওয়াই ভালো কেন?

  • ফাংশনের ভেতরে printf কাজ করে না
  • তাহলে কীবোর্ড ছাড়াই টেস্ট করা যায় আর অন্য UI-তেও আবার ব্যবহার করা যায়
  • printf থাকলে প্রোগ্রাম অনেক ধীর হয়
  • printf থাকলে compiler warning দেয়

AI-এর ledger_add ১ ও ৪টি বিক্রির টেস্টে পাস করল, কিন্তু ৫ম বিক্রিতে sanitizer error দিল। এ থেকে সবচেয়ে বড় শিক্ষা কী?

  • AI কখনো ব্যবহার করা উচিত নয়
  • sanitizer বন্ধ রাখলেই প্রোগ্রাম ঠিকঠাক চলবে
  • সীমানার ঠিক আগে-পরে (৩, ৪, ৫) টেস্ট করা জরুরি, আর সেই টেস্ট regression test হিসেবে রাখতে হয়
  • শূন্য warning থাকলে কোড নির্ভুল ধরে নেওয়া যায়

শব্দকোষ

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

মনে রাখুন

  1. কোডের আগে চাহিদা: user story-তে কে, কী, কেন — আর লিখিত MVP দিয়ে কী বানাবেন না।
  2. ডেটা মডেল ভিত্তি: struct Sale, পেমেন্টের enum, টাকা পয়সায় পূর্ণসংখ্যায়।
  3. তিন স্তর — UI, logic, data; logic ফাংশনে কোনো printf/scanf নয়, তাই টেস্ট সহজ।
  4. মাইলস্টোনে বানান — প্রতিটির শেষে প্রোগ্রাম চলে; realloc আলাদা পয়েন্টারে, ভর্তি হলে দ্বিগুণ।
  5. টেস্ট প্ল্যান আগে, সীমানায় বিশেষ নজর; AI-কে একবারে একটি ফাংশন, যাচাই চেকলিস্ট আর sanitizer দিয়ে।

পরের পর্বে: চাচার একটাই অভিযোগ বাকি — “প্রোগ্রাম বন্ধ করলেই সব হিসাব মুছে যায়!” পর্ব ২৪-এ ফাইল I/O শিখে খাতাকে ফাইলে জমিয়ে রাখব, আর দেখবেন তিন স্তরের নকশার জন্য সেই আপগ্রেড কত সহজ হয়।

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

  • পর্ব ২৪ · ফাইল I/O — ডেটা জমিয়ে রাখাপরবর্তী পর্বদোকানের খাতাকে ফাইলে সংরক্ষণ — প্রজেক্টের প্রথম আপগ্রেড।
  • C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
  • Software Engineering & Gitপাশের ট্র্যাকRequirement, মাইলস্টোন, code review আর git দিয়ে দলগত কাজ — বিস্তারিত।
  • Software Testing & QAগভীরে যানটেস্ট প্ল্যান, unit test, regression test আর স্বয়ংক্রিয় টেস্টিং।
  • System Designপরবর্তী ধাপছোট টুল থেকে লক্ষ ব্যবহারকারীর সিস্টেম — স্তর ভাগ, ডেটাবেস আর স্কেল।
  • Data Structures & Algorithmsপাশের ট্র্যাকবাড়তে পারা অ্যারে, binary search আর hash table — বড় খাতায় দ্রুত খোঁজা।
এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান