এই পর্বে যা শিখবেন
- চাহিদা লেখা আর “কী বানাব না” ঠিক করা — একটি ছোট কিন্তু সম্পূর্ণ 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: বাড়ানোর জন্য খোলা, বদলানোর জন্য বন্ধ।
এই নকশায় পৌঁছানো এক লাফে হয় না। একটি নতুন প্রোগ্রামার যেভাবে ধাপে ধাপে এখানে আসেন, নিচে দেখুন — প্রতিটি ধাপে কোন সমস্যা পরের ধাপকে বাধ্য করল, সেটা খেয়াল করুন:
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 void add(String[] cats, int[] amounts, ...) { ... }
static void list(String[] cats, int[] amounts, int n) { ... }
// main ছোট হলো, কিন্তু প্রতিটি method-এ
// একই তিনটি জিনিস ঘুরছে: cats, amounts, n
final class Expense { // খাত + টাকা + নোট এক object-এ
Expense(...) { যাচাই এখানে }
}
class ExpenseBook { // array আর count লুকানো
private Expense[] items; private int count;
boolean add(Expense e) { ... }
}
interface Command { String name(); void run(...); }
class AddCommand implements Command { ... }
class ListCommand implements Command { ... }
Command cmd = find(w[0]);
cmd.run(book, w); // main আর কখনো বদলাবে না
- এক বিশাল main. শুরুতে সবাই এভাবেই লেখেন, আর ৫০ লাইন পর্যন্ত এটা ঠিকই চলে। সমস্যা: একটি খরচের তথ্য দুটি আলাদা array-তে ছড়ানো —
cats[3]আরamounts[3]একসাথে বদলাতে ভুলে গেলেই খাতা গরমিল। আর যাচাই কোথায় হবে, তার কোনো নির্দিষ্ট জায়গা নেই। - Method-এ ভাগ। পর্ব ১২-এর পরামর্শ মেনে প্রতিটি কাজ আলাদা method. main পড়া সহজ হলো। কিন্তু নতুন গন্ধ: প্রতিটি method একই তিনটি parameter ঘোরাচ্ছে। যখন কয়েকটি ডেটা সবসময় একসাথে চলে, তারা একটি object হতে চায়।
- Class — ডেটার সাথে তার নিয়ম।
Expenseতিনটি তথ্য একসাথে রাখে আর জন্মের সময় যাচাই করে;ExpenseBookarray আর count লুকিয়ে রাখে (পর্ব ১৭-এর encapsulation). এখন ভুল খাত বা ঋণাত্মক টাকা খাতায় ঢোকার কোনো পথ নেই। - কমান্ডও object. শেষ ধাপে if-else শিকলটা উঠে গেল। main loop শুধু খোঁজে আর
runডাকে; কোন class-এরrunচলবে, তা চলার সময় JVM ঠিক করে। নতুন কমান্ড মানে নতুন class — পুরোনো কোড অক্ষত। এই পর্বের বাকি অংশ এই সংস্করণটিই ধাপে ধাপে বানাবে।
আমাদের 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}
| count | |
| items[0] | |
| items[1] | |
| items[2] | |
| items[3] |
- নতুন খাতা।
new Expense[4]চারটি ঘর বানায়, প্রতিটিতে reference-এর ডিফল্ট মানnull.count = 0— খাতা খালি, আর পরের খরচ যাবে ঘর ০-তে। - প্রথম যোগ।
items[count++] = e— আগে পুরোনোcount(০) দিয়ে ঘর বাছাই, তারপরcountএক বাড়ে। এক লাইনে দুটি কাজ; পর্ব ০৭-এর post-increment এখানে ঠিক মাপমতো কাজে লাগল। - দ্বিতীয় যোগ। একই লাইন, এবার ঘর ১. খেয়াল করুন, “পরের খালি ঘর খোঁজা”-র কোনো loop নেই —
countনিজেই বলে দেয়। তাই যোগ করা সবসময় এক ধাপের কাজ। - তৃতীয় যোগ। ঘর ২ ভরল,
count = 3. যোগফল হিসাবের loop চলে শুধুi < countপর্যন্ত, তাই খালি ঘর ৩-এরnull-এ কখনো হাত পড়ে না। - চতুর্থ যোগ — খাতা ভরা।
countএখন array-র দৈর্ঘ্যের সমান (৪)। এটা কোনো error নয়, শুধু একটি অবস্থা — যা পরের ধাপে কাজে লাগবে। - ভরা খাতায় যোগের চেষ্টা। লাইন ২-এর পাহারা:
count == items.length, তাইfalseফেরত — কোনো ঘর ছোঁয়া হয়নি। এই পাহারা না থাকলেitems[4]লিখতে গিয়েArrayIndexOutOfBoundsExceptionআসত (পর্ব ১৩)। Method-টি boolean ফেরত দিয়ে caller-কে জানায়, কী বার্তা দেবে তা caller ঠিক করুক। - Undo.
items[--count]— আগেcountকমে ৩, তারপর সেই ঘর থেকে খরচটি নেওয়া। তারপর ঘরেnullবসানো হলো। কেন? না বসালেও প্রোগ্রাম ঠিক চলত, কারণ loopcount-এর পরে যায় না। কিন্তু তখন array একটি অপ্রয়োজনীয় object আঁকড়ে থাকত, আর garbage collector তাকে সরাতে পারত না। - খালি ঘরের পুনর্ব্যবহার। নতুন খরচ বসল ঠিক সেই ঘর ৩-এ, যেটা 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);
| c-এর ঘোষিত টাইপ | |
| c-এর আসল class | |
| c.name() | |
| মিলল? |
- খোঁজ শুরু। ব্যবহারকারী লিখেছেন
total.find-এর কাছে শুধু একটি String আরCommand-এর একটি array — সে জানে না, জানার দরকারও নেই, array-তে কোন কোন class আছে। - প্রথম ঘর।
c-এর ঘোষিত টাইপCommand, কিন্তু আসল object একটিAddCommand.c.name()ডাকলে JVM আসল class দেখেAddCommand-এরname()চালায় — উত্তর"add". মেলেনি। - দ্বিতীয় ঘর। একই লাইন
c.name(), কিন্তু এবার চলল সম্পূর্ণ আলাদা method —ListCommand-এর। কোডের একটি লাইন, চলার সময় ভিন্ন ভিন্ন method: এটাই dynamic dispatch (পর্ব ২০)। - মিলে গেল।
TotalCommand-এর নাম"total".findসেই object-টিই ফেরত দেয়; caller-এরcmdএখন একটিTotalCommand-এর দিকে তাকিয়ে আছে, যদিও তার টাইপ লেখাCommand. - 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-কে টুলের ভেতর দিয়ে অনুসরণ করুন:
- ইনপুট লাইন
nextLine()Scanner - ভাগ করা
splitশব্দের array - Command object
findAddCommand - ExpenseBook
addযাচাই আর সংরক্ষণ - আউটপুট
printlnফলাফল
String line = in.nextLine().trim();
line == "add food 120 cha singara"
w[0] = "add" w[1] = "food" w[2] = "120" w[3] = "cha singara" // নোটের ফাঁকা জায়গা অক্ষত
Command cmd = find("add");
// cmd-এর আসল class: AddCommand
cmd.run(book, w);
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
- পড়া।
nextLine()Enter পর্যন্ত পুরো লাইন দেয়;trim()দুই পাশের বাড়তি ফাঁকা সরায়। শব্দ শব্দ করেnext()দিয়ে পড়লে নোটের “cha singara” দুই টুকরো হয়ে যেত — পর্ব ০৯-এর পুরো লাইন পড়ার কৌশল এখানে জরুরি। - ভাগ।
split("\\s+", 4): এক বা একাধিক ফাঁকা জায়গায় ভাগ, কিন্তু সর্বোচ্চ চার টুকরো — চতুর্থ টুকরো লাইনের বাকি সবটা। তাই “add food 120 cha singara”-তে নোট অক্ষত থাকে। এই সীমা না দিলে নোট কেটে চার-পাঁচটি আলাদা শব্দ হতো। - খোঁজা। প্রথম শব্দ
"add"দিয়েfindএকটিAddCommandobject ফেরত দিল (অংশ ৬-এর ডায়াগ্রাম)। নাম না মিললেnull— তখন main “unknown command” ছাপে, আর কোনো কমান্ড চলে না। - যাচাই ও সংরক্ষণ।
"120"String থেকে সংখ্যা হলো (parse); constructor টাকা আর খাত যাচাই করল; খাতা জায়গা দিল। এই তিন জায়গার যেকোনোটিতে সমস্যা হলে কাজ থেমে যায় আর একটি error বার্তা ফেরে — খাতা অক্ষত থাকে। - ছাপা।
book.addtrueদিয়েছে, তাই “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 নেই, classfinal(বা 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-কে দিয়ে রিভিউ চাওয়ার একটি কাঠামো। লক্ষ করুন, এখানে কোড লিখে দিতে বলা হচ্ছে না:
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 — ঠিক এই পর্বের মতোই থাকে।
অনুশীলন
চারটি কাজই টুলটিকে বাড়ানো। প্রতিটির পর অংশ ৭-এর সেশন আবার চালিয়ে দেখুন, পুরোনো আচরণ ভাঙেনি তো।
-
নতুন কমান্ড
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, আর
COMMANDSarray-তে একটি এন্ট্রি। main loop,find, অন্য কোনো কমান্ড — কিছুই বদলায়নি। এটাই open/closed principle-এর ব্যবহারিক রূপ। খেয়াল করুন, নতুন কমান্ড পুরোনোbook.total(cat)ব্যবহার করেছে — খাতার হিসাবের নিয়ম এখনো এক জায়গায়। -
নীরব শূন্য বন্ধ করুন:
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-ছাপার দায়িত্ব এক জায়গায় রাখে। -
নিজের 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 বদলায়নি — কারণ arrayprivateছিল।addএখন সবসময়trueদেয়, তাইAddCommand-এর “book is full” শাখা কখনো চলবে না; পরিষ্কার রাখতে চাইলে সেটি সরিয়ে দিন। একটি ফাঁদ: শুরুর capacity ০ হলে0 * 2-ও ০, নতুন array-তেও কোনো ঘর নেই, আরitems[0]-এ লিখতে গিয়েArrayIndexOutOfBoundsExceptionআসবে। তাই constructor-এ capacity কমপক্ষে ১ নিশ্চিত করুন। ArrayList ঠিক এই ধরনের সূক্ষ্মতা সামলায় (ভাবনার প্রশ্ন ৪)। -
পর্ব ৩০-এর পরে ফিরে আসুন: 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-এর সমস্যাও মিটে যায়।countfield আরnullবসানোর লাইন উধাও হবে।Expense,Category, চারটি কমান্ড আরMain-এ কোনো বদল নেই, কারণ তারা শুধুExpenseBook-এর public method চেনে, ভেতরের array নয়। অনুমান আর বাস্তব মিললে বুঝবেন encapsulation সত্যিই কাজ করেছে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমCommand interface দিয়ে বানানো একটি Java CLI টুলে একটি কমান্ড লাইনের যাত্রার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ⌨️ Scanner-এর nextLine() একটি পুরো লাইন পড়ে
- ✂️ split দিয়ে লাইনটি শব্দের array-তে ভাগ হয়
- 🔎 প্রথম শব্দ দিয়ে Command array থেকে মানানসই object খোঁজা হয়
- ⚙️ সেই object-এর run চলে — ইনপুট parse ও যাচাই হয়, খাতা বদলায়
- 🖨️ ফল বা 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
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- আগে কাগজ, পরে কোড: কমান্ডের টেবিল, নিয়ম আর “কী বানাব না”; তারপর বিশেষ্য-ক্রিয়া, CRC কার্ড আর UML — প্রতিটি class-এর একটি স্পষ্ট দায়িত্ব।
- যাচাই constructor-এ, ডেটা immutable: private final field, setter নেই, final class — তাহলে অবৈধ বা চুপচাপ বদলানো object অসম্ভব।
- Array আর count: count একসাথে আকার আর পরের খালি ঘর; ভরা অবস্থা পাহারা দিন, undo-তে ঘর
nullকরুন। ArrayList (পর্ব ৩০) এই ধারণারই নিজে-বড়-হওয়া রূপ। - Command interface: প্রতিটি কমান্ড একটি class; main শুধু খোঁজে আর
runডাকে, dynamic dispatch বাকিটা করে। নতুন কমান্ড = নতুন class, পুরোনো কোড অক্ষত। - টেস্ট আর বাস্তব প্রজেক্ট: edge case-এর টেবিল আর session ফাইল দিয়ে প্রতিবার যাচাই; নিজের কম্পিউটারে এক class এক ফাইল,
javac -d out *.java,java -cp out Main.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৩ · equals, hashCode, toString আর compareToপরবর্তী পর্বপ্রতিটি object-এর উত্তরাধিকারসূত্রে পাওয়া method আর তাদের চুক্তি।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python · মাঝপথের প্রজেক্টপাশের ট্র্যাকএকই ধরনের CLI টুল — function, dict আর match দিয়ে।
- C++ · মাঝপথের প্রজেক্টপাশের ট্র্যাকvector, header ফাইল আর তিন স্তরে ভাগ করা CLI টুল।