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

মাঝপথের প্রজেক্ট — “হিসাব খাতা” CLI টুল

Halfway Project — Designing and Building an Expense-Tracker Command-Line Tool in Java

Taught in Bangla, this project lesson builds a command-line expense tracker in Java: scoping an MVP, designing with noun-verb analysis, CRC cards and a UML diagram, an immutable validated Expense class, an array-backed ExpenseBook, polymorphic commands via a Command interface, Scanner input, printf tables, edge-case testing and a multi-file layout.

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

  • চাহিদা লেখা আর “কী বানাব না” ঠিক করা — একটি ছোট কিন্তু সম্পূর্ণ MVP-র পরিধি
  • বিশেষ্য-ক্রিয়া বিশ্লেষণ, CRC কার্ড আর লেখায় আঁকা UML class diagram দিয়ে নকশা
  • যাচাই করা immutable class, array-with-count খাতা, আর Command interface দিয়ে polymorphic dispatch
  • Scanner দিয়ে কমান্ড পড়া, printf দিয়ে সারিবদ্ধ টেবিল, আর ভুল ইনপুট সামলানো
  • Edge case দিয়ে হাতে টেস্ট, একাধিক ফাইলে আসল প্রজেক্ট সাজানো, আর AI-কে নকশা-রিভিউয়ার হিসেবে ব্যবহার

১ · কেন একটি প্রজেক্ট — “হিসাব খাতা”-র চাহিদা

গত বিশটি পর্বে আপনি অনেক টুকরো শিখেছেন: class আর object, constructor, private field, static আর final, inheritance, polymorphism, interface, array, Scanner, switch expression. প্রতিটি টুকরো আলাদাভাবে বোঝা এক জিনিস। কিন্তু একটি আসল প্রোগ্রামে এগুলো একসাথে কাজ করে, আর তখন নতুন প্রশ্ন আসে: কোন কাজ কোন class-এর? কোথায় ভুল ইনপুট আটকাব? নতুন feature যোগ করলে কতটা কোড বদলাতে হবে? এই প্রশ্নগুলোর উত্তর কোনো একক পর্বে শেখানো যায় না — বানাতে বানাতে শিখতে হয়।

মেসে থাকা ছাত্র বা চাকরিজীবী — অনেকেই মাসের শেষে ভাবেন, “বেতন তো এলো, টাকা গেল কোথায়?” কেউ একটি খাতায় লিখে রাখেন: চা-শিঙাড়া ৪০, রিকশা ৬০, বিদ্যুৎ বিল ১৫০০. মাস শেষে যোগ করেন, কোন খাতে বেশি গেছে দেখেন, ভুল লিখলে শেষ লাইনটা কেটে দেন। আমাদের প্রজেক্ট ঠিক এই খাতাটিকে একটি ছোট CLI (command-line interface) টুলে রূপ দেবে — terminal-এ কমান্ড লিখবেন, টুল উত্তর দেবে।

চাহিদা — কমান্ডের টেবিল

কোড লেখার আগে লিখে ফেলুন, টুলটি ঠিক কী করবে। আমাদের “হিসাব খাতা” পাঁচটি কমান্ড বোঝে:

কমান্ডকী করেউদাহরণ
add <খাত> <টাকা> [নোট]একটি খরচ যোগ করে; নোট ঐচ্ছিক, তাতে ফাঁকা জায়গা থাকতে পারেadd food 120 cha singara
listসব খরচ সারিবদ্ধ টেবিলে দেখায়list
total [খাত]মোট খরচ, অথবা একটি খাতের মোটtotal, total food
undoশেষ খরচটি বাতিল করেundo
quitসারাংশ দেখিয়ে বের হয়ে যায়quit

নিয়মগুলোও লিখে রাখুন, কারণ পরে এগুলোই টেস্টের তালিকা হবে: (১) টাকা হবে ধনাত্মক পূর্ণসংখ্যা — পয়সার হিসাব এই সংস্করণে নেই। (২) খাত চারটির একটি: food, transport, bill, other. (৩) খাতায় জায়গা নির্দিষ্ট; ভরে গেলে টুল পরিষ্কার জানাবে। (৪) অজানা কমান্ড বা ভুল টাকায় টুল ভেঙে পড়বে না, একটি বোধগম্য বার্তা দিয়ে পরের কমান্ডের জন্য অপেক্ষা করবে।

কী বানাব না — পরিধির সীমা

নতুনদের প্রজেক্ট সবচেয়ে বেশি ডোবে পরিধি বাড়তে বাড়তে। তাই সচেতনভাবে ঠিক করুন কী বাদ: ফাইলে সেভ করা নেই (পর্ব ৩৭), তারিখ নেই (পর্ব ৩৮), ArrayList নেই (পর্ব ৩০), পয়সা নেই, মাঝের কোনো খরচ মুছে ফেলা নেই। যা থাকবে, তা পুরোপুরি কাজ করবে। ছোট কিন্তু সম্পূর্ণ, চালিয়ে দেখা যায় এমন প্রথম সংস্করণকে বলে MVP (minimum viable product).

মূল ধারণা

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

২ · নকশা — বিশেষ্য, ক্রিয়া, CRC কার্ড আর একটি UML ছবি

পর্ব ০২-এ শিখেছিলেন বিশেষ্য-ক্রিয়া বিশ্লেষণ: চাহিদার বাক্যে বিশেষ্যগুলো প্রায়ই class হয়, ক্রিয়াগুলো method. আমাদের চাহিদা এক বাক্যে: “ব্যবহারকারী একটি কমান্ড লিখে খাতায় একটি খাতের খরচ যোগ করেন, খরচগুলো দেখেন, মোট বের করেন, আর শেষটা বাতিল করেন।”

  • বিশেষ্য → class: খরচ → Expense, খাত → Category, খাতা → ExpenseBook, কমান্ড → Command. “ব্যবহারকারী” class নয় — তিনি প্রোগ্রামের বাইরে বসে আছেন।
  • ক্রিয়া → method বা আলাদা কমান্ড: যোগ করা → add, দেখা → list, মোট → total, বাতিল → undo.

CRC কার্ড — কার দায়িত্ব কী

CRC কার্ড (Class–Responsibility–Collaborator) হলো প্রতিটি class-এর জন্য একটি ছোট কার্ড: নাম, তার দায়িত্ব, আর কাজ করতে সে কাদের সাহায্য নেয় (সহযোগী). ১৯৮৯ সালে Kent Beck আর Ward Cunningham এই পদ্ধতি প্রকাশ করেন, যাতে নকশা কোডের আগেই টেবিলে বসে আলোচনা করা যায়।

Classদায়িত্ব (জানে / করে)সহযোগী
Categoryবৈধ খাতের তালিকা জানে; একটি নাম বৈধ কি না বলে—
Expenseএকটি খরচের খাত, টাকা, নোট জানে; জন্মের সময়ই নিজেকে যাচাই করেCategory
ExpenseBookখরচগুলো ক্রমানুসারে রাখে; যোগ, শেষটা সরানো, মোট হিসাবExpense
Command ও তার class-গুলোনিজের নাম জানে; একটি কমান্ডের কাজ করে আর ফল ছাপেExpenseBook, Expense
Mainলাইন পড়ে, ভাগ করে, ঠিক কমান্ড খুঁজে তাকে কাজ দেয়Command, ExpenseBook, Scanner

লক্ষ করুন, কোনো কার্ডে “সব কিছু” লেখা নেই। প্রতিটি class একটি স্পষ্ট কাজ করে — একে বলে উঁচু cohesion. উল্টোটা হলো god class: একটি Main যে পড়ে, যাচাই করে, রাখে, হিসাব করে, ছাপে। ছোট প্রোগ্রামে god class চলে যায়, কিন্তু প্রতিটি নতুন feature তাকে আরও জট পাকায়।

UML class diagram — লেখায় আঁকা ছবি

CRC কার্ড বলে কে কী করে; UML class diagram দেখায় class-গুলোর মধ্যে সম্পর্ক। কাগজ-কলমেই আঁকা যায়, এখানে লেখা দিয়ে আঁকা হলো:

┌──────────────────────┐        ┌───────────────────────┐
│ ExpenseBook          │ has-a  │ Expense  (immutable)  │
│ - items : Expense[]  │◆──────▶│ - category : String   │
│ - count : int        │  0..n  │ - amount   : int      │
│ + add(e) : boolean   │        │ - note     : String   │
│ + removeLast()       │        │ + Expense(c, a, n)    │──▶ Category.isValid()
│ + total(cat) : int   │        └───────────────────────┘
└──────────▲───────────┘
           │ ব্যবহার করে
┌──────────┴───────────────────────┐
│ «interface» Command              │
│ + name() : String                │
│ + run(book, words) : void        │
└──────────▲───────────────────────┘
           │ implements (is-a)
   ┌───────┼───────────┬──────────────┐
AddCommand ListCommand TotalCommand UndoCommand

দুই ধরনের সম্পর্ক চোখে পড়ে। ExpenseBook-এর ভেতরে Expense-এর একটি array আছে — এটা has-a সম্পর্ক (composition). আর AddCommand একটি Command — এটা is-a সম্পর্ক, যা Java-তে implements দিয়ে লেখা হয়। - চিহ্ন মানে private, + মানে বাইরে থেকে ব্যবহারযোগ্য। এই ছবিটিই পুরো প্রজেক্টের মানচিত্র — কোডে হারিয়ে গেলে এখানে ফিরে আসবেন।

৩ · বড় সিদ্ধান্ত — switch দিয়ে বাছাই, নাকি Command interface?

লাইনটি পড়ার পর প্রোগ্রামকে ঠিক করতে হবে কোন কাজ চলবে। পর্ব ১০-এর switch expression দিয়ে এটা সবচেয়ে সরলভাবে করা যায়:

// বিকল্প ক — এক জায়গায় বাছাই, প্রতিটি কাজ একটি static method
String reply = switch (words[0]) {
    case "add"   -> add(book, words);
    case "list"  -> list(book);
    case "total" -> total(book, words);
    case "undo"  -> undo(book);
    default      -> "error: unknown command " + words[0];
};

দ্বিতীয় বিকল্প পর্ব ২০–২১-এর polymorphism কাজে লাগায়: প্রতিটি কমান্ড একটি আলাদা class, সবাই একটি interface মানে।

// বিকল্প খ — প্রতিটি কমান্ড নিজেই একটি object
interface Command {
    String name();
    void run(ExpenseBook book, String[] words);
}
// main loop শুধু বলে: "যে-ই হও, run করো"
Command cmd = find(words[0]);
cmd.run(book, words);
প্রশ্নক · switchখ · Command interface
নতুন কমান্ড যোগ করতেswitch-এ নতুন case + নতুন method; main-এর কোড বদলায়নতুন একটি class + তালিকায় একটি এন্ট্রি; main loop অপরিবর্তিত
কোডের পরিমাণকম — চারটি কমান্ডে এটিই সবচেয়ে ছোটবেশি — প্রতিটি কমান্ডে একটি class
একটি কমান্ড আলাদা করে টেস্টstatic method হিসেবে করা যায়সহজ — object বানিয়ে run ডাকলেই হয়
পরে undo/history, কমান্ড লগ করাকঠিন — কাজটা কোনো object নয়, রাখা যায় নাস্বাভাবিক — কমান্ড object array-তে রাখা যায়

সৎ উত্তর: চারটি কমান্ডের টুলে বিকল্প ক মোটেও ভুল নয়, বরং অনেক পেশাদার এটিই লিখতেন। আমরা বিকল্প খ বেছে নিচ্ছি দুটি কারণে। প্রথমত, এই পর্বের উদ্দেশ্য পর্ব ২০–২১-এর ধারণাগুলো একটি বাস্তব জায়গায় কাজ করতে দেখা। দ্বিতীয়ত, কমান্ডকে object বানানো একটি প্রতিষ্ঠিত নকশা — Command pattern — যার ওপর ভবিষ্যতে undo-history বা স্ক্রিপ্ট চালানো সহজে বসানো যায় (ভাবনার প্রশ্ন ২)। নতুন কমান্ড এলে পুরোনো কোড না ছুঁয়ে শুধু নতুন class যোগ করা — একে বলে open/closed principle: বাড়ানোর জন্য খোলা, বদলানোর জন্য বন্ধ।

এই নকশায় পৌঁছানো এক লাফে হয় না। একটি নতুন প্রোগ্রামার যেভাবে ধাপে ধাপে এখানে আসেন, নিচে দেখুন — প্রতিটি ধাপে কোন সমস্যা পরের ধাপকে বাধ্য করল, সেটা খেয়াল করুন:

সংস্করণ ১সব কিছু main-এ
String[] cats = new String[100];
int[] amounts = new int[100];       // দুটি সমান্তরাল array
int n = 0;
while (in.hasNextLine()) {
    String[] w = in.nextLine().split(" ");
    if (w[0].equals("add")) { cats[n] = w[1]; amounts[n] = ...; n++; }
    else if (w[0].equals("list")) { ... }
    else if (w[0].equals("total")) { ... }
    // ১৫০ লাইন পরে ...
}
সংস্করণ ২কাজগুলো static method-এ
static void add(String[] cats, int[] amounts, ...) { ... }
static void list(String[] cats, int[] amounts, int n) { ... }

// main ছোট হলো, কিন্তু প্রতিটি method-এ
// একই তিনটি জিনিস ঘুরছে: cats, amounts, n
সংস্করণ ৩ডেটা আর নিয়ম একসাথে — class
final class Expense { // খাত + টাকা + নোট এক object-এ
    Expense(...) { যাচাই এখানে }
}
class ExpenseBook {   // array আর count লুকানো
    private Expense[] items; private int count;
    boolean add(Expense e) { ... }
}
সংস্করণ ৪কমান্ডও object — interface
interface Command { String name(); void run(...); }
class AddCommand implements Command { ... }
class ListCommand implements Command { ... }

Command cmd = find(w[0]);
cmd.run(book, w);   // main আর কখনো বদলাবে না
  1. এক বিশাল main. শুরুতে সবাই এভাবেই লেখেন, আর ৫০ লাইন পর্যন্ত এটা ঠিকই চলে। সমস্যা: একটি খরচের তথ্য দুটি আলাদা array-তে ছড়ানো — cats[3] আর amounts[3] একসাথে বদলাতে ভুলে গেলেই খাতা গরমিল। আর যাচাই কোথায় হবে, তার কোনো নির্দিষ্ট জায়গা নেই।
  2. Method-এ ভাগ। পর্ব ১২-এর পরামর্শ মেনে প্রতিটি কাজ আলাদা method. main পড়া সহজ হলো। কিন্তু নতুন গন্ধ: প্রতিটি method একই তিনটি parameter ঘোরাচ্ছে। যখন কয়েকটি ডেটা সবসময় একসাথে চলে, তারা একটি object হতে চায়।
  3. Class — ডেটার সাথে তার নিয়ম। Expense তিনটি তথ্য একসাথে রাখে আর জন্মের সময় যাচাই করে; ExpenseBook array আর count লুকিয়ে রাখে (পর্ব ১৭-এর encapsulation). এখন ভুল খাত বা ঋণাত্মক টাকা খাতায় ঢোকার কোনো পথ নেই।
  4. কমান্ডও object. শেষ ধাপে if-else শিকলটা উঠে গেল। main loop শুধু খোঁজে আর run ডাকে; কোন class-এর run চলবে, তা চলার সময় JVM ঠিক করে। নতুন কমান্ড মানে নতুন class — পুরোনো কোড অক্ষত। এই পর্বের বাকি অংশ এই সংস্করণটিই ধাপে ধাপে বানাবে।
C/C++/Python জানলে

আমাদের C++ আর Python কোর্সেও মাঝপথে একটি CLI প্রজেক্ট আছে, আর তুলনাটা শিক্ষণীয়। Python সংস্করণে কমান্ড বাছাই হয় match দিয়ে, ডেটা থাকে dict-এর list-এ — কোনো class ছাড়াই; C++ সংস্করণে std::vector জায়গা নিজে বাড়ায়, তাই নির্দিষ্ট মাপের array-র ঝামেলা নেই। Java-তে আমরা ইচ্ছা করে interface আর array বেছে নিয়েছি, কারণ এই মুহূর্তে কোর্সে collections আসেনি। দেখুন Python-এর মাঝপথের প্রজেক্ট আর C++-এর মাঝপথের প্রজেক্ট.

৪ · ধাপ ১ — Category আর Expense: যাচাই করা, অপরিবর্তনীয় ডেটা

নিচ থেকে ওপরে বানানো সবচেয়ে নিরাপদ: আগে সেই class, যে অন্য কারও ওপর নির্ভর করে না। Category-এর কাজ শুধু বৈধ খাতগুলো জানা। পর্ব ১৮-এর static final দিয়ে ধ্রুবক বানানো হলো:

final class Category {
    static final String FOOD = "food", TRANSPORT = "transport",
                        BILL = "bill", OTHER = "other";
    static final String[] ALL = {FOOD, TRANSPORT, BILL, OTHER};

    private Category() {}            // object বানানোর দরকার নেই

    static boolean isValid(String name) {
        for (String c : ALL) if (c.equals(name)) return true;
        return false;
    }
}

দুটি খুঁটিনাটি লক্ষ করুন। private constructor থাকায় কেউ new Category() লিখতে পারে না — এই class শুধু ধ্রুবক আর static method-এর আধার, যাকে বলে utility class (Java-র নিজের Math class-ও এমন)। আর তুলনা হয়েছে equals দিয়ে, == দিয়ে নয় — Scanner থেকে আসা String আর literal একই object না-ও হতে পারে (পর্ব ০৮)।

একটি লুকানো ফাঁক: final শুধু ভ্যারিয়েবলটিকে আটকায়, array-র ভেতরটা নয়। Category.ALL = new String[3]; compile হবে না, কিন্তু Category.ALL[0] = "cinema"; দিব্যি compile হয় আর চলে — যেকোনো class বৈধ খাতের তালিকা বদলে দিতে পারে! String ধ্রুবকের আরেক দুর্বলতা: method-এ যেকোনো String পাঠানো যায়, compiler ধরতে পারে না যে "fod" কোনো খাত নয়। পর্ব ২৪-এর enum দুটি সমস্যাই মেটাবে — তখন Category হবে enum Category { FOOD, TRANSPORT, BILL, OTHER }, আর ভুল খাত compile-এর সময়ই ধরা পড়বে।

Expense — জন্মের সময়ই যাচাই

একটি খরচের তিনটি তথ্য: খাত, টাকা, নোট। মূল নকশা-সিদ্ধান্ত হলো যাচাই কোথায় হবে। উত্তর: constructor-এ। যদি constructor ভুল ডেটা পেলে object তৈরিই না করে, তাহলে প্রোগ্রামের কোথাও একটি অবৈধ Expense থাকা অসম্ভব। পর্ব ০২-এর ভাষায়, এটা class-এর invariant: “প্রতিটি Expense-এর টাকা ধনাত্মক আর খাত বৈধ” — সবসময় সত্য।

final class Expense {
    private final String category, note;
    private final int amount;                  // পূর্ণ টাকা

    Expense(String category, int amount, String note) {
        if (amount <= 0)
            throw new IllegalArgumentException("amount must be positive");
        if (!Category.isValid(category))
            throw new IllegalArgumentException("unknown category " + category);
        this.category = category;
        this.amount = amount;
        this.note = note;
    }
    String category() { return category; }    // শুধু পড়ার method, setter নেই
    int amount()      { return amount; }
    String note()     { return note; }
}

throw new IllegalArgumentException(...) লাইনটি constructor-কে মাঝপথে থামিয়ে দেয় — object তৈরি হয় না, আর একটি বার্তা ডাকার জায়গায় ফিরে যায়। IllegalArgumentException হলো Java-র নিজের “ভুল argument” সংকেত। Exception-এর পূর্ণ গল্প পর্ব ২৯-এ; এখানে শুধু দুটি জিনিস লাগবে: কীভাবে ছুড়তে হয় (ওপরে), আর অংশ ৭-এ কীভাবে একটি catch দিয়ে বার্তাটি ধরে ছাপতে হয়।

এই class immutable — তৈরি হওয়ার পর কখনো বদলায় না। তিনটি উপাদান মিলে তা নিশ্চিত করে: সব field private final, কোনো setter নেই, আর class নিজে final, যাতে কেউ subclass বানিয়ে বদলানোর মতো আচরণ ঢোকাতে না পারে। লাভ কী? undo-তে সরানো খরচ নিশ্চিন্তে ফেরত দেওয়া যায়, একই object দুই জায়গায় থাকলেও কেউ চুপচাপ বদলে দিতে পারে না, আর String-এর মতোই এটি নিয়ে ভাবা সহজ। Getter-এর নাম getAmount() না রেখে amount() রাখা হয়েছে, কারণ পর্ব ২৫-এর record ঠিক এই রীতিতে নাম দেয় — তখন পুরো class-টি এক লাইনের record হয়ে যেতে পারবে।

টাকা কেন int, double নয়? double দশমিক ভগ্নাংশ হুবহু রাখতে পারে না — 0.1 + 0.2 ছাপে 0.30000000000000004 (পর্ব ০৭)। টাকার হিসাবে এই ছোট গরমিল জমে বড় হয়। আসল ব্যাংক বা bKash-এর মতো সিস্টেম টাকাকে পয়সায় পূর্ণসংখ্যা (long) হিসেবে রাখে, অথবা BigDecimal ব্যবহার করে। আমাদের MVP পয়সা বাদ দিয়ে পূর্ণ টাকায় int নিয়েছে — সহজ, আর হিসাব নির্ভুল।

ধাপের শেষে প্রোগ্রাম চলে: প্রতিটি ধাপ শেষে একটি ছোট main দিয়ে যাচাই করুন — new Expense("food", 120, "cha") ঠিকঠাক তৈরি হয় কি না, আর new Expense("food", -5, "x") সত্যিই থামে কি না। পুরো টুল একবারে লিখে শেষে চালালে দশটি bug একসাথে আসে; প্রতি ধাপে চালালে আসে একটি একটি করে।

৫ · ধাপ ২ — ExpenseBook: array আর count দিয়ে খাতা

খাতায় অনেক খরচ। পর্ব ১৩-এর array-এর মাপ তৈরির সময়ই ঠিক হয়ে যায়, পরে বাড়ে না। তাই প্রচলিত কৌশল: একটি বড় array, আর একটি count — কয়টি ঘর আসলে ভরা। count একসাথে দুটি তথ্য দেয়: খাতায় কয়টি খরচ, আর পরের খরচ কোন ঘরে বসবে।

class ExpenseBook {
    private final Expense[] items;
    private int count = 0;                    // items[0..count-1] ভরা

    ExpenseBook(int capacity) { items = new Expense[capacity]; }

    boolean add(Expense e) {
        if (count == items.length) return false;   // খাতা ভরা
        items[count++] = e;
        return true;
    }
    Expense removeLast() {
        if (count == 0) return null;              // বাতিলের কিছু নেই
        Expense e = items[--count];
        items[count] = null;                      // পুরোনো reference মুছে দাও
        return e;
    }
    int size() { return count; }
    Expense get(int i) { return items[i]; }
    int total(String cat) {                       // cat == null মানে সব খাত
        int sum = 0;
        for (int i = 0; i < count; i++)
            if (cat == null || items[i].category().equals(cat)) sum += items[i].amount();
        return sum;
    }
}

এই class-এর invariant: items[0] থেকে items[count-1] পর্যন্ত প্রতিটি ঘরে একটি বৈধ Expense, বাকি সব ঘর null. array আর count দুটিই private, তাই বাইরের কেউ এই নিয়ম ভাঙতে পারে না — শুধু add আর removeLast দিয়ে খাতা বদলায়, আর দুটিই নিয়ম মেনে চলে। নিচের ডায়াগ্রামে চার ঘরের একটি খাতায় যোগ, ভরে যাওয়া, undo আর আবার যোগ দেখুন — count কীভাবে সরে, আর খালি হওয়া ঘর কীভাবে আবার ব্যবহার হয়:

1boolean add(Expense e) {2    if (count == items.length) return false;3    items[count++] = e;4    return true;5}6Expense removeLast() {7    if (count == 0) return null;8    Expense e = items[--count];9    items[count] = null;10    return e;11}
খাতার ভেতরটা (capacity 4)
count
items[0]
items[1]
items[2]
items[3]
  1. নতুন খাতা। new Expense[4] চারটি ঘর বানায়, প্রতিটিতে reference-এর ডিফল্ট মান null. count = 0 — খাতা খালি, আর পরের খরচ যাবে ঘর ০-তে।
  2. প্রথম যোগ। items[count++] = e — আগে পুরোনো count (০) দিয়ে ঘর বাছাই, তারপর count এক বাড়ে। এক লাইনে দুটি কাজ; পর্ব ০৭-এর post-increment এখানে ঠিক মাপমতো কাজে লাগল।
  3. দ্বিতীয় যোগ। একই লাইন, এবার ঘর ১. খেয়াল করুন, “পরের খালি ঘর খোঁজা”-র কোনো loop নেই — count নিজেই বলে দেয়। তাই যোগ করা সবসময় এক ধাপের কাজ।
  4. তৃতীয় যোগ। ঘর ২ ভরল, count = 3. যোগফল হিসাবের loop চলে শুধু i < count পর্যন্ত, তাই খালি ঘর ৩-এর null-এ কখনো হাত পড়ে না।
  5. চতুর্থ যোগ — খাতা ভরা। count এখন array-র দৈর্ঘ্যের সমান (৪)। এটা কোনো error নয়, শুধু একটি অবস্থা — যা পরের ধাপে কাজে লাগবে।
  6. ভরা খাতায় যোগের চেষ্টা। লাইন ২-এর পাহারা: count == items.length, তাই false ফেরত — কোনো ঘর ছোঁয়া হয়নি। এই পাহারা না থাকলে items[4] লিখতে গিয়ে ArrayIndexOutOfBoundsException আসত (পর্ব ১৩)। Method-টি boolean ফেরত দিয়ে caller-কে জানায়, কী বার্তা দেবে তা caller ঠিক করুক।
  7. Undo. items[--count] — আগে count কমে ৩, তারপর সেই ঘর থেকে খরচটি নেওয়া। তারপর ঘরে null বসানো হলো। কেন? না বসালেও প্রোগ্রাম ঠিক চলত, কারণ loop count-এর পরে যায় না। কিন্তু তখন array একটি অপ্রয়োজনীয় object আঁকড়ে থাকত, আর garbage collector তাকে সরাতে পারত না।
  8. খালি ঘরের পুনর্ব্যবহার। নতুন খরচ বসল ঠিক সেই ঘর ৩-এ, যেটা undo খালি করেছিল। array-with-count কৌশলে শেষের ঘরটি একটি stack-এর চূড়ার মতো — সবার শেষে যে এল, undo-তে সে-ই আগে যায় (LIFO, পর্ব ১২-এর call stack-এর মতো)।

get(i)-এ পাহারা নেই। book.get(3) যদি count ২ থাকা অবস্থায় ডাকা হয়, ফল আসবে null — যা পরে NullPointerException ঘটাবে। আমাদের প্রোগ্রামে শুধু list কমান্ড get ডাকে, আর সে i < size() মেনে চলে, তাই আপাতত নিরাপদ। কিন্তু public method-এ এই ধরনের ফাঁক রাখা ঝুঁকিপূর্ণ; অনুশীলনে এটা ঠিক করার সুযোগ আছে।

সৎ স্বীকারোক্তি: নির্দিষ্ট মাপের array-র সবচেয়ে বড় দুর্বলতা — মাপ আগে থেকে অনুমান করতে হয়। কম ধরলে খাতা ভরে যায়, বেশি ধরলে মেমরি নষ্ট। পর্ব ৩০-এর ArrayList এই কাজটাই নিজে করে: ভরে গেলে ভেতরে একটি বড় array বানিয়ে পুরোনোগুলো কপি করে নেয়। মজার ব্যাপার, তখন আমাদের শুধু ExpenseBook-এর ভেতরটা বদলাতে হবে — বাকি কোনো class জানেই না খাতার ভেতরে array না ArrayList. Encapsulation-এর আসল পুরস্কার এটাই।

৬ · ধাপ ৩ — Command interface আর polymorphic dispatch

এবার কমান্ডগুলো। প্রতিটি কমান্ড class Command interface মানবে — অর্থাৎ নিজের নাম বলতে পারবে, আর কাজ করতে পারবে। AddCommand সবচেয়ে বেশি কাজ করে, তাই সেটিই দেখা যাক:

interface Command {
    String name();
    void run(ExpenseBook book, String[] words);
}

class AddCommand implements Command {
    public String name() { return "add"; }
    public void run(ExpenseBook book, String[] w) {
        if (w.length < 3) { System.out.println("usage: add <category> <amount> [note]"); return; }
        Expense e = new Expense(w[1], Integer.parseInt(w[2]), w.length > 3 ? w[3] : "-");
        System.out.println(book.add(e) ? "ok, saved #" + book.size() : "error: book is full");
    }
}

Interface-এর method গুলো স্বয়ংক্রিয়ভাবে public, তাই implement করার সময়ও public লিখতে হয় — না লিখলে compiler আপত্তি করে, কারণ override করার সময় access কমানো যায় না (পর্ব ১৯)। AddCommand নিজে কিছু যাচাই করে না: টাকা সংখ্যা কি না, তা দেখে Integer.parseInt, আর খাত ও ধনাত্মকতা দেখে Expense-এর constructor. প্রতিটি নিয়ম এক জায়গায় — একে বলে দায়িত্বের পরিষ্কার ভাগ।

সঠিক কমান্ড খোঁজা

নাম থেকে কমান্ড খুঁজতে আমরা একটি সাধারণ array আর loop ব্যবহার করব (নাম থেকে মান খোঁজার আসল কাঠামো Map আসবে পর্ব ৩১-এ)। সব কমান্ড object একটি Command[]-এ থাকে — array-র টাইপ interface, কিন্তু ভেতরের object-গুলো চার ভিন্ন class-এর। এটাই polymorphism-এর হৃদয়: একটি ভ্যারিয়েবল টাইপ, অনেক আসল আকার। total লিখলে কী ঘটে, ধাপে ধাপে দেখুন:

1static final Command[] COMMANDS = {new AddCommand(),2    new ListCommand(), new TotalCommand(), new UndoCommand()};3static Command find(String name) {4    for (Command c : COMMANDS)5        if (c.name().equals(name)) return c;6    return null;7}8Command cmd = find(w[0]);   // w[0] = "total"9cmd.run(book, w);
loop-এর প্রতিটি ধাপে
c-এর ঘোষিত টাইপ
c-এর আসল class
c.name()
মিলল?
  1. খোঁজ শুরু। ব্যবহারকারী লিখেছেন total. find-এর কাছে শুধু একটি String আর Command-এর একটি array — সে জানে না, জানার দরকারও নেই, array-তে কোন কোন class আছে।
  2. প্রথম ঘর। c-এর ঘোষিত টাইপ Command, কিন্তু আসল object একটি AddCommand. c.name() ডাকলে JVM আসল class দেখে AddCommand-এর name() চালায় — উত্তর "add". মেলেনি।
  3. দ্বিতীয় ঘর। একই লাইন c.name(), কিন্তু এবার চলল সম্পূর্ণ আলাদা method — ListCommand-এর। কোডের একটি লাইন, চলার সময় ভিন্ন ভিন্ন method: এটাই dynamic dispatch (পর্ব ২০)।
  4. মিলে গেল। TotalCommand-এর নাম "total". find সেই object-টিই ফেরত দেয়; caller-এর cmd এখন একটি TotalCommand-এর দিকে তাকিয়ে আছে, যদিও তার টাইপ লেখা Command.
  5. run — main-এর অজান্তে সঠিক কাজ। cmd.run(book, w) লাইনে compiler শুধু নিশ্চিত হয়েছিল যে Command-এর একটি run আছে। কোনটা চলবে, তা ঠিক হলো এখন, চলার সময়: TotalCommand.run. main loop-এ কোথাও if (word.equals("total")) লেখা নেই — নতুন কমান্ড যোগ করলেও লেখা লাগবে না।

কেন abstract class নয়, interface? পর্ব ২১-এর নিয়ম মনে করুন: যখন subclass-গুলোর মধ্যে ভাগ করার মতো field বা কোড থাকে, তখন abstract class. আমাদের কমান্ডগুলোর কোনো সাধারণ অবস্থা নেই — শুধু একটি চুক্তি: “নাম বলো, কাজ করো।” চুক্তির জন্য interface সঠিক হাতিয়ার, আর একটি class একাধিক interface মানতে পারে, কিন্তু একটির বেশি class extend করতে পারে না।

৭ · ধাপ ৪ — পুরো প্রোগ্রাম: একটি লাইনের যাত্রা

সব টুকরো তৈরি। এখন main — যে Scanner দিয়ে লাইন পড়ে, ভাগ করে, কমান্ড খোঁজে, কাজ দেয়, আর ভুল হলে বার্তা ছাপে। পুরো কোড দেখার আগে একটি লাইন add food 120 cha singara-কে টুলের ভেতর দিয়ে অনুসরণ করুন:

  1. ইনপুট লাইনnextLine()Scanner
  2. ভাগ করাsplitশব্দের array
  3. Command objectfindAddCommand
  4. ExpenseBookaddযাচাই আর সংরক্ষণ
  5. আউটপুটprintlnফলাফল
ধাপ ১Scanner একটি পুরো লাইন দিল
String line = in.nextLine().trim();
line == "add food 120 cha singara"
ধাপ ২split("\\s+", 4) — সর্বোচ্চ ৪ টুকরো
w[0] = "add"
w[1] = "food"
w[2] = "120"
w[3] = "cha singara"   // নোটের ফাঁকা জায়গা অক্ষত
ধাপ ৩নাম থেকে object
Command cmd = find("add");
// cmd-এর আসল class: AddCommand
cmd.run(book, w);
ধাপ ৪parse → যাচাই → খাতায়
Integer.parseInt("120")        → 120
new Expense("food", 120, "cha singara")  // যাচাই পাস
book.add(e)  → items[0] = e, count = 1
ধাপ ৫ব্যবহারকারী যা দেখেন
> add food 120 cha singara
ok, saved #1
  1. পড়া। nextLine() Enter পর্যন্ত পুরো লাইন দেয়; trim() দুই পাশের বাড়তি ফাঁকা সরায়। শব্দ শব্দ করে next() দিয়ে পড়লে নোটের “cha singara” দুই টুকরো হয়ে যেত — পর্ব ০৯-এর পুরো লাইন পড়ার কৌশল এখানে জরুরি।
  2. ভাগ। split("\\s+", 4): এক বা একাধিক ফাঁকা জায়গায় ভাগ, কিন্তু সর্বোচ্চ চার টুকরো — চতুর্থ টুকরো লাইনের বাকি সবটা। তাই “add food 120 cha singara”-তে নোট অক্ষত থাকে। এই সীমা না দিলে নোট কেটে চার-পাঁচটি আলাদা শব্দ হতো।
  3. খোঁজা। প্রথম শব্দ "add" দিয়ে find একটি AddCommand object ফেরত দিল (অংশ ৬-এর ডায়াগ্রাম)। নাম না মিললে null — তখন main “unknown command” ছাপে, আর কোনো কমান্ড চলে না।
  4. যাচাই ও সংরক্ষণ। "120" String থেকে সংখ্যা হলো (parse); constructor টাকা আর খাত যাচাই করল; খাতা জায়গা দিল। এই তিন জায়গার যেকোনোটিতে সমস্যা হলে কাজ থেমে যায় আর একটি error বার্তা ফেরে — খাতা অক্ষত থাকে।
  5. ছাপা। book.add true দিয়েছে, তাই “ok, saved #1”. আবার প্রথম ধাপে ফিরে পরের লাইনের অপেক্ষা — একেই বলে read–eval–print loop, যে ছকে প্রায় সব CLI টুল আর JShell-ও চলে।

এবার পুরো টুলটি এক ফাইলে। লম্বা — Java-তে আটটি class আর একটি interface জায়গা নেয় — কিন্তু প্রতিটি অংশ আপনি আগেই দেখেছেন। “চালান” চাপলে নিচের স্ক্রিপ্ট করা ইনপুট দিয়ে পুরো একটি সেশন চলবে:

import java.util.Scanner;

final class Category {
    static final String[] ALL = {"food", "transport", "bill", "other"};
    private Category() {}
    static boolean isValid(String c) {
        for (String k : ALL) if (k.equals(c)) return true;
        return false;
    }
}
final class Expense {
    private final String category, note;
    private final int amount;
    Expense(String category, int amount, String note) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        if (!Category.isValid(category)) throw new IllegalArgumentException("unknown category " + category);
        this.category = category; this.amount = amount; this.note = note;
    }
    String category() { return category; }
    int amount() { return amount; }
    String note() { return note; }
}
class ExpenseBook {
    private final Expense[] items;
    private int count = 0;
    ExpenseBook(int capacity) { items = new Expense[capacity]; }
    boolean add(Expense e) {
        if (count == items.length) return false;
        items[count++] = e; return true;
    }
    Expense removeLast() {
        if (count == 0) return null;
        Expense e = items[--count]; items[count] = null; return e;
    }
    int size() { return count; }
    Expense get(int i) { return items[i]; }
    int total(String cat) {
        int sum = 0;
        for (int i = 0; i < count; i++)
            if (cat == null || items[i].category().equals(cat)) sum += items[i].amount();
        return sum;
    }
}
interface Command { String name(); void run(ExpenseBook book, String[] w); }
class AddCommand implements Command {
    public String name() { return "add"; }
    public void run(ExpenseBook book, String[] w) {
        if (w.length < 3) { System.out.println("usage: add <category> <amount> [note]"); return; }
        Expense e = new Expense(w[1], Integer.parseInt(w[2]), w.length > 3 ? w[3] : "-");
        System.out.println(book.add(e) ? "ok, saved #" + book.size() : "error: book is full");
    }
}
class ListCommand implements Command {
    public String name() { return "list"; }
    public void run(ExpenseBook book, String[] w) {
        if (book.size() == 0) { System.out.println("(no expenses yet)"); return; }
        System.out.printf("%-3s %-10s %6s  %s%n", "#", "CATEGORY", "TAKA", "NOTE");
        for (int i = 0; i < book.size(); i++) {
            Expense e = book.get(i);
            System.out.printf("%-3d %-10s %6d  %s%n", i + 1, e.category(), e.amount(), e.note());
        }
    }
}
class TotalCommand implements Command {
    public String name() { return "total"; }
    public void run(ExpenseBook book, String[] w) {
        String cat = w.length > 1 ? w[1] : null;
        System.out.printf("total %s: %d tk%n", cat == null ? "all" : cat, book.total(cat));
    }
}
class UndoCommand implements Command {
    public String name() { return "undo"; }
    public void run(ExpenseBook book, String[] w) {
        Expense e = book.removeLast();
        System.out.println(e == null ? "nothing to undo" : "undone: " + e.category() + " " + e.amount());
    }
}
class Main {
    static final Command[] COMMANDS = {new AddCommand(), new ListCommand(), new TotalCommand(), new UndoCommand()};
    static Command find(String name) {
        for (Command c : COMMANDS) if (c.name().equals(name)) return c;
        return null;
    }
    public static void main(String[] args) {
        ExpenseBook book = new ExpenseBook(4);          // ছোট খাতা, যাতে "ভরা" অবস্থা দেখা যায়
        Scanner in = new Scanner(System.in);
        while (in.hasNextLine()) {
            String line = in.nextLine().trim();
            if (line.isEmpty()) continue;
            System.out.println("> " + line);              // স্ক্রিপ্ট করা সেশনে কমান্ডটি দেখানো
            String[] w = line.split("\\s+", 4);
            if (w[0].equals("quit")) break;
            Command cmd = find(w[0]);
            if (cmd == null) { System.out.println("error: unknown command " + w[0]); continue; }
            try { cmd.run(book, w); }
            catch (NumberFormatException ex) { System.out.println("error: amount must be a whole number"); }
            catch (IllegalArgumentException ex) { System.out.println("error: " + ex.getMessage()); }
        }
        System.out.printf("bye: %d expenses, %d tk%n", book.size(), book.total(null));
    }
}

ইনপুট: data-stdin-এ ১৭টি লাইন — খালি খাতায় list; তিনটি বৈধ add (চা-শিঙাড়া ১২০, রিকশা ৬০, বিদ্যুৎ বিল ১৫০০); তিনটি ভুল add (টাকার জায়গায় abc, অজানা খাত snacks, ঋণাত্মক −৩০); চতুর্থ বৈধ খরচ বিরিয়ানি ২৫০; ভরা খাতায় কলম ১০; undo; আবার কলম; তারপর list, total, total food, অজানা dance, অসম্পূর্ণ add আর quit. আউটপুট হাতে মিলিয়ে নিন: শুরুতে (no expenses yet); তারপর ok, saved #1, #2, #3; তিনটি ভুলের জন্য যথাক্রমে error: amount must be a whole number, error: unknown category snacks, error: amount must be positive; বিরিয়ানিতে ok, saved #4; কলমে error: book is full; undone: food 250; আবার কলমে ok, saved #4. টেবিলে চারটি সারি — food 120, transport 60, bill 1500, other 10 — টাকার কলাম ডানে সারিবদ্ধ। তারপর total all: 1690 tk, total food: 120 tk, error: unknown command dance, ব্যবহারবিধির লাইন, আর শেষে bye: 4 expenses, 1690 tk.

খুঁটিনাটি: (১) catch-এর ক্রম গুরুত্বপূর্ণ। NumberFormatException আসলে IllegalArgumentException-এরই একটি subclass. তাই একটিমাত্র catch (IllegalArgumentException ex) দুই ধরনের ভুলই ধরত, কিন্তু তখন abc-এর বার্তা হতো Java-র নিজের, ব্যবহারকারীর কাছে দুর্বোধ্য (For input string: "abc"). নির্দিষ্টটি আগে, সাধারণটি পরে — উল্টো ক্রমে লিখলে javac compile error দেয়, কারণ দ্বিতীয় catch-এ আর কখনো পৌঁছানো যাবে না। (২) hasNextLine() ইনপুট শেষ হলে false দেয়, তাই quit ছাড়া ইনপুট শেষ হলেও প্রোগ্রাম ভদ্রভাবে সারাংশ ছেপে থামে। (৩) printf-এর %-10s মানে দশ ঘরে বাঁ-ঘেঁষা, %6d মানে ছয় ঘরে ডান-ঘেঁষা সংখ্যা — তাই টেবিল সোজা থাকে (পর্ব ০৯)। (৪) কোড অনলাইন runner-এর জন্য এক ফাইলে, আর main-ওয়ালা class public ছাড়া লেখা; নিজের কম্পিউটারে কীভাবে আলাদা ফাইলে সাজাবেন, তা অংশ ৯-এ।

৮ · হাতে টেস্ট — edge case-এর টেবিল

প্রোগ্রাম “চলছে” মানে শুধু এটুকু প্রমাণ যে আপনার ভাবা ইনপুটে চলছে। আসল প্রশ্ন: যে ইনপুট আপনি ভাবেননি, তাতে কী হয়? সীমানার কাছের বা অদ্ভুত ইনপুটকে বলে edge case. JUnit দিয়ে স্বয়ংক্রিয় টেস্ট শিখবেন পর্ব ৪৭-এ; এখন একটি টেবিল আর হাতে চালানোই যথেষ্ট। প্রতিটি সারিতে আগে প্রত্যাশা লিখুন, তারপর চালিয়ে মেলান:

ইনপুটপ্রত্যাশাআসলে যা ঘটে
খালি খাতায় list, undoবিনয়ী বার্তা(no expenses yet), nothing to undo ✔
add food abcটাকা সংখ্যা নয়amount must be a whole number ✔
add food 0, add food -30প্রত্যাখ্যানamount must be positive ✔
add food 12.50পয়সা চলবে না“whole number” বার্তা ✔ — নিয়মমতো, কিন্তু বার্তায় “পয়সা নয়” বলা আরও ভালো হতো
add food 99999999999অবাস্তব বড় টাকাint-এর সীমা (প্রায় ২১৪ কোটি) পার, তাই parseInt ব্যর্থ — বার্তা “whole number”, যা বিভ্রান্তিকর ⚠
ADD food 50?unknown command ADD — কমান্ড case-sensitive; ঠিক করবেন কি না, একটি নকশা-সিদ্ধান্ত
add food 50 (বাড়তি ফাঁকা)স্বাভাবিকভাবে যোগtrim আর \\s+-এর কারণে ঠিকঠাক ✔
total snacksঅজানা খাত জানানোtotal snacks: 0 tk — নীরবে শূন্য ⚠ (অনুশীলন ২)
অনেক বড় অঙ্কের অনেক খরচসঠিক মোটtotal-এর int sum উপচে পড়তে পারে (পর্ব ০৭-এর integer overflow) ⚠ — long নিলে নিরাপদ
খালি লাইনউপেক্ষাcontinue — কিছুই ছাপে না ✔

লক্ষ করুন, টেবিলে চারটি ⚠ ধরা পড়ল — কোনোটিই crash নয়, কিন্তু প্রতিটি হয় বিভ্রান্তিকর বার্তা, নয়তো নীরব ভুল। নীরব ভুল সবচেয়ে বিপজ্জনক, কারণ ব্যবহারকারী টেরই পান না। এটাই Kent Beck-এর নামে প্রচলিত ক্রমের দ্বিতীয় ধাপ: আগে চালাও, তারপর ঠিক করো, তারপর দরকার হলে দ্রুত করো (ভাবনার প্রশ্ন ৩)। আমাদের টুল প্রথম ধাপ পার করেছে; টেবিলটি দ্বিতীয় ধাপের কাজের তালিকা।

পরীক্ষা ও ইন্টারভিউতে
  • Immutable class বানানোর নিয়ম? — সব field private final, কোনো setter নেই, class final (বা constructor private), আর mutable কিছু থাকলে তার কপি দেওয়া।
  • Interface না abstract class — কখন কোনটা? — শুধু চুক্তি (কী করতে হবে) হলে interface; ভাগ করার মতো field বা কোড থাকলে abstract class. একটি class অনেক interface মানতে পারে, কিন্তু একটিই class extend করতে পারে।
  • Command pattern কী? — একটি অনুরোধ বা কাজকে object বানানো, যাতে তা রাখা, সারিতে দেওয়া, লগ করা বা undo করা যায়।
  • একাধিক catch-এ subclass আগে কেন? — সাধারণ (superclass) catch আগে থাকলে নির্দিষ্টটিতে কখনো পৌঁছানো যায় না, তাই javac compile error দেয়।
  • একটি .java ফাইলে কয়টি public class? — বড়জোর একটি top-level public class, আর ফাইলের নাম হবে সেই class-এর নামে।

৯ · নিজের কম্পিউটারে — একাধিক ফাইলের আসল প্রজেক্ট

অনলাইন runner-এ সব এক ফাইলে রাখতে হয়েছে। আসল Java প্রজেক্টে নিয়ম হলো: প্রতিটি top-level class বা interface নিজের ফাইলে, আর public class-এর ফাইলের নাম হুবহু class-এর নাম। পর্ব ০৩-এ ইনস্টল করা JDK দিয়ে একটি ফোল্ডার বানান:

hisab-khata/
├── Category.java
├── Expense.java
├── ExpenseBook.java
├── Command.java
├── AddCommand.java
├── ListCommand.java
├── TotalCommand.java
├── UndoCommand.java
└── Main.java          ← public class Main { public static void main(...) }

ফাইলগুলোর class-গুলোতে public বসাতে পারেন (public class Main, public final class Expense ইত্যাদি) — একই ফোল্ডারে থাকা class-গুলো এমনিতেও একে অপরকে দেখতে পায়, কারণ তারা একই package-এ (এখানে নামহীন default package). Main-এ দুটি ছোট বদল করুন: স্ক্রিপ্টের জন্য লাইন আবার ছাপার (println("> " + line)) বদলে লুপের শুরুতে System.out.print("> "); লিখে prompt দেখান, আর খাতার capacity ৪ থেকে বাড়িয়ে ১০০০ করুন। তারপর ফোল্ডারে terminal খুলে:

javac -d out *.java          # সব ফাইল compile, .class যাবে out ফোল্ডারে
java -cp out Main             # চালান; quit লিখে বের হোন

javac একসাথে সব ফাইল compile করে, আর প্রতিটি class-এর জন্য একটি .class ফাইল বানায় — নয়টি সোর্স ফাইল থেকে নয়টি .class. -cp out JVM-কে বলে class কোথায় খুঁজবে (পর্ব ০৩-এর classpath). একই সেশন বারবার টেস্ট করতে কমান্ডগুলো একটি session.txt ফাইলে লিখে রাখুন আর ইনপুট হিসেবে দিন — Linux/macOS terminal বা Windows-এর cmd-তে java -cp out Main < session.txt; PowerShell-এ < চলে না, সেখানে Get-Content session.txt | java -cp out Main. প্রতিবার কোড বদলের পর একই ফাইল চালিয়ে আউটপুট মিলিয়ে নেওয়া — এটাই স্বয়ংক্রিয় টেস্টের প্রথম রূপ।

jar --create --file hisab.jar --main-class Main -C out .
java -jar hisab.jar           # এক ফাইলে পুরো টুল, যেকোনো JDK-ওয়ালা কম্পিউটারে চলে

javac ছাড়া সরাসরি? Java 11 থেকে java Main.java লিখে এক ফাইলের প্রোগ্রাম compile না করেই চালানো যায় (পর্ব ০৩)। কিন্তু Java 21-এ এই সুবিধা শুধু এক ফাইলের জন্য — Main.java অন্য ফাইলের class ব্যবহার করলে চলবে না। একাধিক ফাইলের সোর্স সরাসরি চালানোর সুবিধা এসেছে Java 22-এ। এই কোর্স Java 21 LTS ধরে চলে, তাই javac পথটাই নিন। আর প্রজেক্ট বড় হলে হাতে javac না চালিয়ে Maven বা Gradle ব্যবহার করা হয় — পর্ব ৪৬.

পরের ধাপ — টুলটিকে বাড়ানোর ধারণা

enum আর record

Category হবে enum (ভুল খাত compile-এর সময় ধরা পড়বে), Expense হবে এক লাইনের record — constructor-এর যাচাই থাকবে।

পর্ব ২৪–২৫

ArrayList আর Map

নির্দিষ্ট array-র বদলে ArrayList — capacity-র চিন্তা শেষ; কমান্ড খুঁজতে loop-এর বদলে Map.

পর্ব ৩০–৩১

ফাইলে সেভ আর তারিখ

বন্ধ করলেও হিসাব থাকবে; প্রতিটি খরচে তারিখ, আর “এই মাসের মোট”।

পর্ব ৩৭–৩৮

নিজস্ব exception আর JUnit

ভুলের ধরন অনুযায়ী আলাদা exception, আর প্রতিটি edge case-এর জন্য স্বয়ংক্রিয় টেস্ট।

পর্ব ২৯, ৪৭

১০ · AI-এর যুগে — AI-কে নকশা-রিভিউয়ার বানান, লেখক নয়

এই পুরো প্রজেক্ট একটি AI-কে এক বাক্যে বললে সে কয়েক সেকেন্ডে লিখে দেবে। তাহলে নিজে বানানোর মানে কী? মানে হলো, AI-এর কোড চলবে, কিন্তু আপনি জানবেন না কেন constructor-এ যাচাই, কেন items[count] = null, কেন catch-এর এই ক্রম — আর যেদিন AI ভুল করবে, সেদিন ধরতেও পারবেন না। সঠিক ব্যবহার হলো AI-কে pair programming-এর সঙ্গী বানানো: আপনি চালক, AI পাশে বসা রিভিউয়ার। তাকে দিন আপনার নকশা আর কোড, চান সমালোচনা — পুরো সমাধান নয়।

AI প্রায়ই যা করেকেন সমস্যাআপনি যা করবেন
শুরুতেই ArrayList, HashMap, Stream, record দিয়ে লেখেযা এখনো শেখেননি, তা দিয়ে লেখা কোড আপনি যাচাই করতে পারবেন নাprompt-এ স্পষ্ট বলুন কোন কোন জিনিস ব্যবহার করা যাবে
টাকার জন্য doubleদশমিকের গরমিল জমেপূর্ণসংখ্যা বা BigDecimal চান, কারণ জিজ্ঞেস করুন
setter-সহ mutable Expenseযেকোনো জায়গা থেকে খাতার খরচ চুপচাপ বদলানো যায়immutable class চান, যাচাই constructor-এ
undo-তে শুধু count--array পুরোনো object আঁকড়ে থাকেঘরটি null করা হয়েছে কি না দেখুন
“সব input handle করা হয়েছে” দাবিপ্রায়ই সংখ্যার সীমা, ফাঁকা লাইন, case-এর কথা ভাবা হয় নাঅংশ ৮-এর টেবিল দিয়ে নিজে চালিয়ে দেখুন

নিজের নকশা আর কোড AI-কে দিয়ে রিভিউ চাওয়ার একটি কাঠামো। লক্ষ করুন, এখানে কোড লিখে দিতে বলা হচ্ছে না:

আমি Java 21 শিখছি। এখন পর্যন্ত জানি: class, constructor, encapsulation, static/final, inheritance, polymorphism, interface, array, Scanner, switch expression. এখনো ArrayList, Map, enum, record, Stream শিখিনি — এগুলো ব্যবহার করবে না। আমি একটি খরচের হিসাব রাখার CLI টুল বানিয়েছি। নিচে আমার CRC কার্ড আর পুরো কোড দিলাম। [এখানে CRC টেবিল আর কোড] তুমি আমার কোড নতুন করে লিখবে না। শুধু রিভিউ করো: ১. কোন class-এর দায়িত্ব অস্পষ্ট বা বেশি? কোনো god class আছে? ২. এমন কোনো উপায় আছে কি, যাতে একটি অবৈধ Expense তৈরি হতে পারে বা ExpenseBook-এর invariant ভাঙে? ৩. এমন ৫টি ইনপুট দাও, যেগুলোতে আমার প্রোগ্রাম ভুল বা বিভ্রান্তিকর আচরণ করবে বলে তোমার ধারণা — প্রতিটির জন্য প্রত্যাশিত আউটপুট লেখো। ৪. নতুন একটি "report" কমান্ড যোগ করতে আমার কোন কোন ফাইল বদলাতে হবে? ৫. প্রতিটি পরামর্শের পাশে লেখো: এটা "এখনই ঠিক করা উচিত" নাকি "পরের পর্বগুলোর পরে"।

AI-এর রিভিউ হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Graphical app আর ওয়েবসাইটের যুগে ছোট command-line টুল কেন এখনো গুরুত্বপূর্ণ? Unix দর্শন এ বিষয়ে কী বলে?

১৯৭০-এর দশকে Bell Labs-এ Unix তৈরির সময় একটি চিন্তাধারা গড়ে ওঠে, যাকে আজ বলা হয় Unix দর্শন. এর সবচেয়ে পরিচিত রূপ Doug McIlroy-র, যিনি Unix-এ pipe-এর ধারণা প্রস্তাব করেছিলেন (১৯৭৩ সালে Unix-এ যোগ হয়)। Peter Salus-এর A Quarter Century of Unix (১৯৯৪) বইয়ে তাঁর কথাগুলো এভাবে সংক্ষেপ করা আছে: এমন প্রোগ্রাম লেখো যা একটি কাজ করে এবং ভালোভাবে করে; এমন প্রোগ্রাম লেখো যা একে অপরের সাথে কাজ করে; আর এমন প্রোগ্রাম লেখো যা text stream সামলায়, কারণ text হলো সর্বজনীন interface.

কেন এটা আজও টিকে আছে। একটি text-ভিত্তিক টুল অন্য টুলের সাথে জোড়া লাগানো যায়। git log | grep fix | wc -l — তিনটি আলাদা টুল, কেউ কাউকে চেনে না, কিন্তু মিলে একটি প্রশ্নের উত্তর দেয়: কয়টি commit-এ “fix” আছে। Graphical app এভাবে জোড়া লাগে না। আরেকটি কারণ স্বয়ংক্রিয়তা: যা কমান্ডে চলে, তা স্ক্রিপ্টে লেখা যায়, রাতে সার্ভারে নিজে চলে, CI-তে প্রতিটি commit-এ চলে। আজকের সফটওয়্যার শিল্পের প্রায় পুরো যন্ত্রপাতি — git, docker, kubectl, mvn, gradle, এমনকি javac আর java নিজে — command-line টুল।

আমাদের টুলের সাথে সম্পর্ক। এই পর্বে আমরা অজান্তেই Unix দর্শনের একটি সুফল পেয়েছি: যেহেতু টুলটি standard input থেকে text লাইন পড়ে, তাই একটি session.txt ফাইল দিয়েই পুরো সেশন টেস্ট করা গেল — অনলাইন runner-এর data-stdin-ও ঠিক তাই করে। প্রোগ্রাম যদি কেবল মাউস-ক্লিকে চলত, প্রতিটি টেস্ট হাতে করতে হতো। আবার উল্টো দিকও আছে: আমাদের আউটপুট মানুষের জন্য সাজানো টেবিল, অন্য প্রোগ্রামের জন্য সুবিধাজনক নয়। বাস্তব টুলগুলো তাই প্রায়ই দুটি আউটপুট রাখে — মানুষের জন্য সুন্দর, আর যন্ত্রের জন্য CSV বা JSON (যেমন অনেক টুলের --json option).

Command line শেখা কেন ক্যারিয়ারের দক্ষতা। বাংলাদেশের software কোম্পানিগুলোতে backend বা DevOps-এ কাজ করতে গেলে প্রায়ই শুরু থেকেই Linux সার্ভারে SSH করে কমান্ড চালাতে হয় — সেখানে কোনো graphical interface নেই। যিনি নিজে একটি CLI টুল বানিয়েছেন, তিনি বোঝেন ইনপুট, আউটপুট, error বার্তা আর exit কীভাবে কাজ করে।

মূল উপলব্ধি: ছোট, এক-কাজের, text-ভিত্তিক টুল জোড়া লাগানো যায়, স্ক্রিপ্টে চালানো যায়, আর সহজে টেস্ট করা যায়। পঞ্চাশ বছর পরেও McIlroy-র তিন বাক্য সফটওয়্যার নকশার সবচেয়ে টেকসই উপদেশগুলোর একটি।

প্র ০২“Command pattern” কোথা থেকে এলো, আর কমান্ডকে object বানালে undo কেন প্রায় বিনা খরচে পাওয়া যায়?

১৯৯৪ সালে চারজন লেখক — Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software বইটি প্রকাশ করেন। লেখকদের ডাকনাম হয়ে যায় “Gang of Four” বা GoF. বইটিতে ২৩টি design pattern আছে — বারবার দেখা দেওয়া নকশা-সমস্যার পরীক্ষিত সমাধান, নাম দেওয়া, যাতে প্রোগ্রামাররা এক শব্দে একটি পুরো নকশা নিয়ে কথা বলতে পারেন। তার একটি Command. বইয়ের সংজ্ঞার সারকথা: একটি অনুরোধকে object হিসেবে মোড়ানো, যাতে বিভিন্ন অনুরোধ দিয়ে কাজ চালানো, অনুরোধ সারিতে রাখা বা লগ করা, আর undo করা যায় এমন কাজ সমর্থন করা যায়।

কেন object হলে undo সহজ। একটি method ডাক একটি ঘটনা — ঘটে, শেষ হয়, কোনো চিহ্ন থাকে না। কিন্তু একটি object থাকে। ধরুন প্রতিটি কমান্ড class-এ run-এর পাশে আরেকটি method আছে: undo. AddCommand যে খরচ যোগ করেছে তা নিজের field-এ মনে রাখে, আর undo-তে সেটি সরায়। এখন main loop প্রতিটি সফল কমান্ড একটি history array-তে রাখে। ব্যবহারকারী undo চাইলে history-র শেষ object নিয়ে তার undo() ডাকা — কোন ধরনের কমান্ড ছিল, তা main-এর জানার দরকার নেই, polymorphism সেটা সামলায়। Redo চাইলে দ্বিতীয় একটি array. আমাদের বর্তমান undo শুধু “শেষ যোগ” বাতিল করে, কারণ এখন একটিই কমান্ড খাতা বদলায়; বেশি কমান্ড এলে এই পূর্ণ নকশাই লাগবে।

বাস্তবে কোথায়। প্রায় প্রতিটি টেক্সট এডিটর, ছবি সম্পাদনার সফটওয়্যার আর IDE-র Ctrl+Z এই নীতিতে চলে। Java-র নিজের Swing লাইব্রেরিতে javax.swing.undo package আছে, যেখানে UndoableEdit interface আর UndoManager class ঠিক এই কাঠামো দেয়। Runnable interface-ও এক অর্থে একটি command — “চালানোর মতো একটি কাজ”, যা thread বা executor-এর হাতে তুলে দেওয়া যায় (পর্ব ৪০–৪১)। ডেটাবেসের transaction log আর ব্যাংকের লেনদেনের রেকর্ডও একই দর্শন: প্রতিটি পরিবর্তন একটি রেকর্ড, যা পরে আবার চালানো বা উল্টানো যায়।

সতর্কতা। GoF বইটি নিজেও বলে, pattern একটি সমস্যার সমাধান — সমস্যা না থাকলে pattern শুধু বাড়তি জটিলতা। চারটি কমান্ডের টুলে switch-ই যথেষ্ট ছিল; আমরা Command বেছেছি শেখার জন্য আর undo/history-র ভবিষ্যৎ মাথায় রেখে। পর্ব ৪৮-এ আরও pattern আর তাদের অপব্যবহারের কথা আসবে।

মূল উপলব্ধি: কাজকে object বানালে কাজটি রাখা, গোনা, সারিতে দেওয়া আর উল্টানো যায়। undo আসলে একটি data structure-এর প্রশ্ন — আর Command pattern সেই data structure-এ রাখার মতো জিনিস তৈরি করে দেয়।

প্র ০৩“Make it work, make it right, make it fast” — এই ক্রম কেন? প্রথম দিন থেকে দ্রুত কোড লিখলে সমস্যা কী?

“Make it work, make it right, make it fast” বাক্যটি সাধারণত Kent Beck-এর নামে প্রচলিত — Extreme Programming আর test-driven development-এর অন্যতম প্রবর্তক, আর কাকতালীয়ভাবে CRC কার্ডেরও সহ-স্রষ্টা। বাক্যটির উৎস নিয়ে সামান্য অনিশ্চয়তা আছে, কিন্তু অর্থ নিয়ে নেই: তিনটি আলাদা লক্ষ্য, আর ক্রমটাই আসল উপদেশ।

Make it work — আগে চালাও। প্রথম লক্ষ্য একটি প্রোগ্রাম যা পুরো পথ পার হয়: ইনপুট থেকে আউটপুট পর্যন্ত, হয়তো কুৎসিত, কিন্তু চলে। আমাদের নকশা-বিবর্তনের “সংস্করণ ১” — এক বিশাল main — এই ধাপ। এর মূল্য হলো তথ্য: চালিয়ে দেখলেই বোঝা যায় চাহিদা ঠিক ছিল কি না। অনেক সময় দেখা যায়, যে feature নিয়ে সবচেয়ে বেশি ভাবা হয়েছিল, ব্যবহারকারীর তা লাগেই না।

Make it right — তারপর ঠিক করো। চলমান প্রোগ্রামকে পরিষ্কার করা: class-এ ভাগ, নাম ঠিক করা, edge case সামলানো। আচরণ না বদলে কোডের গঠন উন্নত করাকে বলে refactoring, আর Martin Fowler-এর ১৯৯৯ সালের Refactoring বই এর ভাষা তৈরি করেছে। গুরুত্বপূর্ণ শর্ত: refactoring-এর আগে-পরে একই টেস্ট চালিয়ে নিশ্চিত হতে হয় যে কিছু ভাঙেনি — আমাদের session.txt ঠিক সেই কাজ করে। অংশ ৮-এর ⚠ চিহ্নগুলো এই ধাপের কাজের তালিকা।

Make it fast — শেষে, আর মেপে। Donald Knuth ১৯৭৪ সালের একটি প্রবন্ধে লিখেছিলেন, অকাল optimization সব অনিষ্টের মূল (অন্তত বেশিরভাগের) — তবে একই প্রবন্ধে তিনি এটাও বলেন যে গুরুত্বপূর্ণ ৩ শতাংশ কোডে দক্ষতা উপেক্ষা করা যাবে না। সমস্যা হলো, কোন ৩ শতাংশ, তা অনুমানে প্রায়ই ভুল হয়; তাই মাপতে হয় (profiler দিয়ে)। আমাদের খাতায় total প্রতিবার পুরো array হাঁটে — হাজার খরচে এক মিলিসেকেন্ডেরও কম লাগে। চলমান যোগফল একটি field-এ রেখে একে “দ্রুত” করা যায়, কিন্তু তখন undo-তেও সেটা ঠিক রাখতে হবে — নতুন bug-এর সুযোগ, বিনিময়ে কোনো অনুভবযোগ্য লাভ নেই।

উল্টো ক্রমের বিপদ। শুরুতেই দ্রুততার জন্য লেখা কোড সাধারণত জটিল, আর জটিল কোড ঠিক করা কঠিন। “Right” না হয়ে “fast” হলে ভুল উত্তর দ্রুত আসে।

মূল উপলব্ধি: প্রতিটি ধাপ পরেরটির ভিত্তি — চলমান প্রোগ্রাম ছাড়া কী ঠিক করতে হবে বোঝা যায় না, আর সঠিক ও পরিষ্কার কোড ছাড়া নিরাপদে দ্রুত করা যায় না।

প্র ০৪নির্দিষ্ট মাপের array-র সীমাবদ্ধতাই কি ArrayList-এর মতো dynamic array জন্ম দিয়েছে? প্রতিবার ভরে গেলে কপি করা কি ধীর নয়?

আমাদের ExpenseBook(4) চার খরচেই ভরে যায়। সহজ সমাধান মনে হতে পারে: ভরে গেলে এক ঘর বড় একটি নতুন array বানাও, সব কপি করো। কিন্তু হিসাব করুন — n-তম যোগে n-১টি কপি, তাই n খরচ যোগ করতে মোট কপি প্রায় n²/২. এক লাখ খরচে প্রায় পাঁচশো কোটি কপি — অচল।

দ্বিগুণ করার কৌশল। সমাধান হলো এক ঘর নয়, অনুপাতে বাড়ানো — যেমন প্রতিবার দ্বিগুণ। তখন কপি হয় কালেভদ্রে: ৪ থেকে ৮-এ যাওয়ার সময় ৪টি, ৮ থেকে ১৬-তে ৮টি, ১৬ থেকে ৩২-এ ১৬টি… n পর্যন্ত মোট কপি ৪ + ৮ + ১৬ + … যা n-এর দ্বিগুণের কম। অর্থাৎ গড়ে প্রতি যোগে দুটিরও কম কপি — ধ্রুব খরচ। কোনো একটি যোগ ব্যয়বহুল হলেও (যখন কপি হয়), অনেকগুলো যোগের ওপর গড়লে খরচ সমান। এই ধরনের গড় হিসাবকে বলে amortized analysis; Robert Tarjan ১৯৮৫ সালের একটি প্রবন্ধে এই পদ্ধতিকে আনুষ্ঠানিক রূপ দেন। এমন নিজে বড় হওয়া array-কে বলে dynamic array.

বাস্তব সংখ্যা। OpenJDK-র ArrayList ভরে গেলে প্রায় দেড় গুণ বাড়ে (পুরোনো মাপ + তার অর্ধেক), আর ডিফল্ট প্রথম মাপ ১০ — যদিও আধুনিক সংস্করণে array তৈরি হয় প্রথম যোগের সময়, তার আগে নয়। দেড় গুণে কপি একটু বেশি হয়, কিন্তু অব্যবহৃত মেমরি কম থাকে — এটি একটি সচেতন আপস। C++-এর std::vector-এ বৃদ্ধির অনুপাত বাস্তবায়নভেদে আলাদা (কোথাও ২, কোথাও ১.৫)। Java-র ArrayList আসলে ভেতরে একটি Object[] আর একটি size — আমাদের items আর count-এর হুবহু ধারণা, শুধু ভরে গেলে নিজে বড় হয়।

কেন তবু array শিখলাম। কারণ ArrayList কোনো জাদু নয় — এই পর্বের array-with-count তার ভেতরের ছবি। যিনি এটা জানেন, তিনি বোঝেন কেন ArrayList-এর শেষে যোগ দ্রুত কিন্তু শুরুতে ঢোকানো ধীর (সবাইকে এক ঘর সরাতে হয়), কেন আগে থেকে মাপ জানা থাকলে new ArrayList<>(1000) লেখা উপকারী, আর কেন ইন্টারভিউতে “dynamic array নিজে বানাও” প্রশ্ন আসে। পর্ব ৩০-এ এগুলোর পূর্ণ আলোচনা; অনুশীলন ৩-এ আপনি নিজেই একটি ছোট সংস্করণ বানাবেন।

মূল উপলব্ধি: নির্দিষ্ট মাপ থেকে মুক্তির চাবি হলো জ্যামিতিক বৃদ্ধি — মাঝে মাঝে বড় খরচ, কিন্তু গড়ে ধ্রুব। ArrayList হলো array আর count-এর ওপর এই একটি চতুর নিয়ম।

প্র ০৫পেশাদার Java CLI টুল কীভাবে বানানো হয়? Java-র ধীর শুরু কি command-line টুলের জন্য বাধা?

আমাদের টুল নিজে split দিয়ে কমান্ড ভাঙে। বাস্তব CLI টুলের চাহিদা অনেক বেশি: --help বার্তা, -v বা --verbose-এর মতো option, ঐচ্ছিক ও বাধ্যতামূলক argument, ভুল হলে স্পষ্ট বার্তা, subcommand (git commit, git push-এর মতো)। প্রতিবার এগুলো হাতে লেখা অপচয়, তাই লাইব্রেরি আছে। পুরোনো ও পরিচিত Apache Commons CLI আর JCommander; আধুনিক Java-তে বহুল ব্যবহৃত picocli. picocli-তে class-এর ওপর @Command, field-এর ওপর @Option বা @Parameters annotation দিলে লাইব্রেরি নিজেই argument পার্স করে, টাইপ রূপান্তর করে, আর help বার্তা তৈরি করে। মজার মিল: picocli-তে প্রতিটি subcommand সাধারণত একটি আলাদা class — আমাদের AddCommand, ListCommand-এর মতোই। (annotation কীভাবে কাজ করে, তা পর্ব ৪৫-এ।)

শুরুর সময়ের সমস্যা। একটি ছোট CLI টুল প্রায়ই এক সেকেন্ডেরও কম চলে। কিন্তু Java প্রোগ্রাম চালাতে আগে JVM চালু হতে হয়, class load হয়, আর শুরুতে কোড interpreter-এ ধীরে চলে যতক্ষণ না JIT গরম হয় (পর্ব ০৫)। দীর্ঘ সময় চলা সার্ভারে এটা নগণ্য, কিন্তু যে টুল দিনে শতবার ছোট কাজে ডাকা হয়, সেখানে এই অপেক্ষা চোখে পড়ে। এ কারণেই অনেক CLI টুল ঐতিহ্যগতভাবে C, Go বা Rust-এ লেখা হয়।

GraalVM native image. এর একটি জোরালো উত্তর GraalVM-এর native-image টুল। এটি ahead-of-time compile করে পুরো Java অ্যাপ্লিকেশনকে একটি স্বয়ংসম্পূর্ণ native executable বানায় — চালাতে আলাদা JVM লাগে না, শুরু হয় খুব দ্রুত, মেমরিও সাধারণত কম লাগে। বিনিময়ে কিছু শর্ত: build-এর সময় পুরো প্রোগ্রাম বিশ্লেষণ করা হয় (“closed world” ধারণা), তাই reflection বা চলার সময় class load করার মতো dynamic কাজের জন্য আগে থেকে কনফিগারেশন দিতে হয়; build নিজে সময় নেয়; আর যে প্ল্যাটফর্মের জন্য বানানো, সেখানেই চলে — “একবার লেখো, সব জায়গায় চালাও”-এর বদলে প্রতি প্ল্যাটফর্মে আলাদা build. picocli native image-এর জন্য বিশেষ সহায়তা দেয়, তাই এই জুটি Java-তে দ্রুত CLI বানানোর একটি জনপ্রিয় পথ।

অন্য পথ। সব টুলের native image লাগে না। jar ফাইল আর java -jar এখনো সবচেয়ে সরল বিতরণ; JDK-র jlink দিয়ে শুধু দরকারি module নিয়ে ছোট runtime বানানো যায়, আর jpackage (Java 16 থেকে স্থায়ী) দিয়ে Windows-এর installer বা macOS-এর অ্যাপ প্যাকেজ বানানো যায়।

মূল উপলব্ধি: পেশাদার CLI-তে argument পার্সিং লাইব্রেরি করে, আর শুরুর সময় গুরুত্বপূর্ণ হলে native image. কিন্তু ভেতরের নকশা — প্রতিটি কমান্ড একটি class, যাচাই করা ডেটা, পরিষ্কার error — ঠিক এই পর্বের মতোই থাকে।

অনুশীলন

চারটি কাজই টুলটিকে বাড়ানো। প্রতিটির পর অংশ ৭-এর সেশন আবার চালিয়ে দেখুন, পুরোনো আচরণ ভাঙেনি তো।

  1. নতুন কমান্ড report: প্রতিটি খাতের মোট একটি সারিবদ্ধ টেবিলে দেখাবে, আর শেষে সর্বমোট। এটা যোগ করতে কোন কোন জায়গায় কোড লিখতে বা বদলাতে হবে? main loop-এ কি হাত দিতে হবে?
    class ReportCommand implements Command {
        public String name() { return "report"; }
        public void run(ExpenseBook book, String[] w) {
            for (String c : Category.ALL)
                System.out.printf("%-10s %6d%n", c, book.total(c));
            System.out.printf("%-10s %6d%n", "ALL", book.total(null));
        }
    }
    // Main-এ শুধু তালিকায় একটি এন্ট্রি:
    // static final Command[] COMMANDS = {..., new UndoCommand(), new ReportCommand()};

    দুটি জায়গা: একটি নতুন class, আর COMMANDS array-তে একটি এন্ট্রি। main loop, find, অন্য কোনো কমান্ড — কিছুই বদলায়নি। এটাই open/closed principle-এর ব্যবহারিক রূপ। খেয়াল করুন, নতুন কমান্ড পুরোনো book.total(cat) ব্যবহার করেছে — খাতার হিসাবের নিয়ম এখনো এক জায়গায়।

  2. নীরব শূন্য বন্ধ করুন: total snacks এখন total snacks: 0 tk ছাপে। এমনভাবে বদলান যাতে অজানা খাতে error: unknown category snacks আসে। নতুন যাচাইয়ের কোড লিখবেন, নাকি আগের কোনো কোড আবার ব্যবহার করবেন?

    আগের Category.isValid আবার ব্যবহার করুন — একই নিয়ম দুই জায়গায় দুইভাবে লিখলে একদিন দুটি আলাদা হয়ে যাবে। TotalCommand.run-এ cat বের করার পরে একটি লাইন যোগ করুন: if (cat != null && !Category.isValid(cat)) { System.out.println("error: unknown category " + cat); return; }. বিকল্পভাবে throw new IllegalArgumentException("unknown category " + cat) লিখলে main-এর বিদ্যমান catch একই বার্তা ছাপবে — তখন বার্তার বানানও Expense-এর সাথে স্বয়ংক্রিয়ভাবে মিলবে। দুটিই ঠিক; দ্বিতীয়টি error-ছাপার দায়িত্ব এক জায়গায় রাখে।

  3. নিজের dynamic array: ExpenseBook বদলান যাতে খাতা ভরে গেলে আর false না দিয়ে দ্বিগুণ মাপের নতুন array বানিয়ে পুরোনোগুলো কপি করে নেয়। কোন কোন field-এর final সরাতে হবে? অন্য কোনো class-এ কি বদল লাগবে?
    private Expense[] items;                     // final সরাতে হলো
    boolean add(Expense e) {
        if (count == items.length)
            items = java.util.Arrays.copyOf(items, items.length * 2);
        items[count++] = e;
        return true;
    }

    items এখন অন্য array-র দিকে তাকাতে পারে, তাই তার final সরাতে হয়েছে; Arrays.copyOf (পর্ব ১৩) নতুন বড় array বানিয়ে পুরোনো মানগুলো কপি করে, বাকি ঘর null. অন্য কোনো class বদলায়নি — কারণ array private ছিল। add এখন সবসময় true দেয়, তাই AddCommand-এর “book is full” শাখা কখনো চলবে না; পরিষ্কার রাখতে চাইলে সেটি সরিয়ে দিন। একটি ফাঁদ: শুরুর capacity ০ হলে 0 * 2-ও ০, নতুন array-তেও কোনো ঘর নেই, আর items[0]-এ লিখতে গিয়ে ArrayIndexOutOfBoundsException আসবে। তাই constructor-এ capacity কমপক্ষে ১ নিশ্চিত করুন। ArrayList ঠিক এই ধরনের সূক্ষ্মতা সামলায় (ভাবনার প্রশ্ন ৪)।

  4. পর্ব ৩০-এর পরে ফিরে আসুন: ArrayList শেখার পর ExpenseBook-এর array আর count-এর বদলে একটি ArrayList<Expense> ব্যবহার করুন। এখনই অনুমান করে লিখে রাখুন — কোন কোন method-এর ভেতর বদলাবে, আর কোন কোন class-এ এক অক্ষরও বদলাবে না?

    শুধু ExpenseBook-এর ভেতর বদলাবে: field হবে private final ArrayList<Expense> items = new ArrayList<>();; add হবে items.add(e); removeLast হবে খালি না হলে items.remove(items.size() - 1); size হবে items.size(); get হবে items.get(i) — যা এখন নিজেই সীমার বাইরে গেলে exception দেয়, তাই অংশ ৫-এর পাহারাহীন get-এর সমস্যাও মিটে যায়। count field আর null বসানোর লাইন উধাও হবে। Expense, Category, চারটি কমান্ড আর Main-এ কোনো বদল নেই, কারণ তারা শুধু ExpenseBook-এর public method চেনে, ভেতরের array নয়। অনুমান আর বাস্তব মিললে বুঝবেন encapsulation সত্যিই কাজ করেছে।

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

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

ক্রমCommand interface দিয়ে বানানো একটি Java CLI টুলে একটি কমান্ড লাইনের যাত্রার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⌨️ Scanner-এর nextLine() একটি পুরো লাইন পড়ে
  2. ✂️ split দিয়ে লাইনটি শব্দের array-তে ভাগ হয়
  3. 🔎 প্রথম শব্দ দিয়ে Command array থেকে মানানসই object খোঁজা হয়
  4. ⚙️ সেই object-এর run চলে — ইনপুট parse ও যাচাই হয়, খাতা বদলায়
  5. 🖨️ ফল বা error বার্তা ছাপা হয়, loop পরের লাইনের অপেক্ষা করে

একটি Java class-এ Expense[] items (দৈর্ঘ্য ৫) আর int count = 0 দিয়ে খাতা রাখা হয়; add করে items[count++] = e, আর removeLast করে items[--count]. তিনবার add আর একবার removeLast-এর পর পরের add কোন index-এ বসবে?

  • 0
  • 1
  • 2
  • 3

Java-তে একটি try-এর পরে প্রথমে catch (IllegalArgumentException e) আর তার নিচে catch (NumberFormatException e) লিখলে কী হবে?

  • দুটিই ঠিকমতো কাজ করবে
  • compile error, কারণ দ্বিতীয় catch-এ কখনো পৌঁছানো যাবে না
  • চলার সময় দুটি catch-ই একসাথে চলবে
  • ক্রমে কিছু যায় আসে না, JVM সবচেয়ে নির্দিষ্টটি বেছে নেয়

Gang of Four-এর Command design pattern-এর মূল ধারণা কোনটি?

  • কমান্ড-লাইন argument পড়ার একটি লাইব্রেরি
  • একটি class-এর শুধু একটি object নিশ্চিত করা
  • সব কমান্ড একটি বড় switch-এ রাখা
  • একটি অনুরোধ বা কাজকে object বানানো, যাতে তা রাখা, সারিতে দেওয়া বা undo করা যায়

একটি Java CLI টুলে প্রতিটি কমান্ড Command interface মানে, আর main loop কমান্ডগুলোর একটি array থেকে নাম মিলিয়ে run ডাকে। নতুন একটি কমান্ড যোগ করতে কী লাগবে?

  • একটি নতুন class আর কমান্ডের array-তে একটি এন্ট্রি
  • main loop-এ একটি নতুন if-else শাখা
  • Command interface-এ একটি নতুন method
  • প্রতিটি বিদ্যমান কমান্ড class-এ বদল

Java-তে একটি immutable class বানাতে নিচের কোনটি দরকার নেই?

  • সব field private final রাখা
  • কোনো setter না রাখা
  • সব field static করা
  • class-কে final করা, যাতে subclass না বানানো যায়

একটি Java প্রজেক্টে public class Main আছে। এটি কোন ফাইলে থাকতে হবে, আর সব ফাইল compile করার প্রচলিত কমান্ড কোনটি?

  • যেকোনো নামের .java ফাইলে; java *.java
  • Main.java ফাইলে; javac *.java
  • Main.java ফাইলে; java -compile Main
  • Main.class ফাইলে; javac Main.class

শব্দকোষ

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

মনে রাখুন

  1. আগে কাগজ, পরে কোড: কমান্ডের টেবিল, নিয়ম আর “কী বানাব না”; তারপর বিশেষ্য-ক্রিয়া, CRC কার্ড আর UML — প্রতিটি class-এর একটি স্পষ্ট দায়িত্ব।
  2. যাচাই constructor-এ, ডেটা immutable: private final field, setter নেই, final class — তাহলে অবৈধ বা চুপচাপ বদলানো object অসম্ভব।
  3. Array আর count: count একসাথে আকার আর পরের খালি ঘর; ভরা অবস্থা পাহারা দিন, undo-তে ঘর null করুন। ArrayList (পর্ব ৩০) এই ধারণারই নিজে-বড়-হওয়া রূপ।
  4. Command interface: প্রতিটি কমান্ড একটি class; main শুধু খোঁজে আর run ডাকে, dynamic dispatch বাকিটা করে। নতুন কমান্ড = নতুন class, পুরোনো কোড অক্ষত।
  5. টেস্ট আর বাস্তব প্রজেক্ট: edge case-এর টেবিল আর session ফাইল দিয়ে প্রতিবার যাচাই; নিজের কম্পিউটারে এক class এক ফাইল, javac -d out *.java, java -cp out Main.

পরের পর্বে: মডিউল ৪ শুরু। প্রতিটি Java object উত্তরাধিকারসূত্রে Object class থেকে কয়েকটি method পায় — equals, hashCode, toString — আর তুলনার জন্য আছে compareTo. দুটি Expense কখন “সমান”, আর println(expense) লিখলে কেন অদ্ভুত লেখা আসে — সেই প্রশ্নগুলো দিয়েই শুরু।

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

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