Replace Type Code with Subclasses: যখন প্রতিটা ধরন সত্যিই আলাদা আচরণ করে
Replace Type Code with Subclasses refactoring শেখো ডে-স্কলার/বোর্ডার/হোস্টেলার গল্পের মাধ্যমে। TypeScript আর C#-এ switch কীভাবে মুছে যায়, আর Class vs Subclasses vs State/Strategy — কোনটা কখন নেবে সেটাও বুঝবে।
তিন ধরনের ছাত্র, একজন ক্লান্ত ক্লার্ক
ধরো একটা residential school-এর কথা। সেই school-এর admission register-এ "Student Type" নামে একটা column আছে। সেখানে অফিস ক্লার্ক জামাল ভাই একটা মাত্র অক্ষর লেখেন: D, B, বা H।
- D মানে Day-scholar — সকাল ৮টায় আসে, বিকেল ৩টায় চলে যায়, শুধু tuition fee দেয়, বাসা থেকে আনা টিফিন খায়।
- B মানে Boarder — সোমবার থেকে শুক্রবার school-এ থাকে, সপ্তাহান্তে বাড়ি যায়, tuition-এর সাথে পাঁচ দিনের mess fee দেয়।
- H মানে Hosteller — hostel-এ পুরোসময় থাকে, tuition + পুরো mess fee + hostel fee দেয়, বাজারে যেতেও gate pass লাগে।
এখন দেখো school-এ কী হয়। প্রতিদিন তিনজন আলাদা মানুষ ওই একটা letter দেখে সিদ্ধান্ত নেন। fee bill ছাপানোর সময় জামাল ভাই letter দেখেন — D হলে এক formula, B হলে আরেক formula, H হলে ভিন্ন formula। বিকেল ৩টার bell বাজলে গেটম্যান রহিম ছাত্রের ID card-এ letter দেখেন — D বের হয়, B আর H থাকে। রাতের dinner plan করার সময় নাসরিন আপা মেস ম্যানেজার letter দেখেন — H ছাত্রদের পুরো count, B ছাত্রদের weekday-এ, D ছাত্রদের একদম না।
দেখছো কি? একই তিন-ভাগ check জামাল ভাই, রহিম, আর নাসরিন আপা — সবাই আলাদা করে করছেন। প্রত্যেকে নিজের মাথায়, নিজের খাতায় নিয়ম লিখে রেখেছেন। একটা rulebook-এর তিনটা copy। কী ভুল হতে পারে?
গত term-এ উত্তর এসে গেল। School একটা নতুন type চালু করলো — W, মানে weekly boarder যাদের শনিবারও class আছে। জামাল ভাই তার ledger-এ fee-র নিয়ম আপডেট করলেন। নাসরিন আপা তার kitchen book-এ dinner count ঠিক করলেন। কিন্তু কেউ রহিমকে বললো না। দুই সপ্তাহ ধরে confused W-ছাত্ররা বিকেল ৩টায় gate-এ আটকে যাচ্ছে — কোনো নিয়ম নেই তাদের জন্য। একটা ছেলে, তারিক, টানা চারদিন বাসায় যেতে পারেনি — তার বাবা principal-এর office-এ এসে রাগ করলেন। Principal একটাই প্রশ্ন করলেন: "এই নিয়মগুলো আর কত জায়গায় লেখা আছে?" কেউ নিশ্চিত করে বলতে পারলো না। Software-এও এটাই সবচেয়ে ভয়ংকর উত্তর।
একটা কথা মনে রেখো: একজন day-scholar আর একজন hosteller শুধু আলাদা label না — তারা সত্যিই আলাদা আচরণ করে। আলাদা fee। আলাদা time। আলাদা gate rule। আর আরেকটা কথা — এই school-এ ছাত্রের type admission-এর সময় একবার ঠিক হয়। D হিসেবে ভর্তি হওয়া কেউ চুপচাপ H হয়ে যায় না। কেউ hostel-এ উঠলে পুরনো record বন্ধ করে নতুন admission দেওয়া হয়। একই record-এ type কখনো বদলায় না।
যখন type code আচরণ নির্ধারণ করে, আর type object-এর পুরো জীবনে fixed থাকে — তখন সঠিক refactoring হলো Replace Type Code with Subclasses: প্রতিটা kind-এর নিজস্ব class দাও, আর প্রতিটা class তার নিজের নিয়ম বহন করুক।
আর type W-এর সপ্তাহটা দুই জগতে কেমন ছিল — switch ladder-এর জগতে, আর refactoring-এর পরে subclass-এর জগতে।
Replace Type Code with Subclasses আসলে কী?
Replace Type Code with Class post-এ আমরা দেখেছিলাম এমন type code যেগুলো pure label — school house যেখানে house অনুযায়ী কোনো আচরণ বদলায় না। সেখানে একটা value class যথেষ্ট।
এই post-এ ভারী case। Field-এ type code আছে ('D' | 'B' | 'H', বা 0/1/2), কিন্তু এই code method-এর ভেতরে দেখা হয় type অনুযায়ী আলাদা আচরণ করানোর জন্য। প্রতিটা method-এ একটা করে switch বা if-else-if ladder গজিয়ে ওঠে। fee method-এ switch। gate method-এ switch। mess method-এ switch। এটাই Switch Statements smell — একই branching copy-paste হয়ে তিনটা staff notebook-এর মতো আলাদা হয়ে যাচ্ছে।
Replace Type Code with Subclasses বলে:
- Host class-কে abstract করো (বা interface দাও)।
- প্রতিটা type code-এর জন্য একটা করে subclass তৈরি করো:
DayScholar,Boarder,Hosteller। - প্রতিটা switch branch-কে matching subclass-এ override হিসেবে নিয়ে যাও।
monthlyFee()-এর switch মিলিয়ে যায়: প্রতিটা subclass সরাসরি জানে তার নিজের fee। - একটাই switch রাখো — factory method-এ, যেটা register-এর raw letter থেকে সঠিক subclass বানায়। Creation-ই একমাত্র জায়গা যেখানে code এখনো দেখা হয়, আর সেটা মাত্র একবার।
এরপর language নিজেই branching করে। student.monthlyFee() call করলে runtime নিজে দেখে object-এর real class কোনটা, আর সঠিক method চালায়। এই built-in dispatch-ই হলো polymorphism, আর এই refactoring হলো Replace Conditional with Polymorphism-এর classic scaffolding। Martin Fowler-এর Refactoring বইয়ের দ্বিতীয় edition-এ দুটোকে pair হিসেবে দেখানো হয়েছে: আগে subclass তৈরি করো, তারপর conditional-গুলো সেগুলোতে ভাঁজ করে ফেলো।
এক লাইনে বলতে গেলে: Replace Type Code with Subclasses একটা behaviour-driving type code-কে এক subclass per type-এ রূপান্তর করে, যাতে ছড়িয়ে ছিটিয়ে থাকা switch-গুলো polymorphic method call-এ পরিণত হয় — type construction-এর সময় ঠিক হয়ে যায়, চিরতরে।
"চিরতরে" কথাটা এমনি লেখা হয়নি। Object-এর class বানানোর মুহূর্তে ঠিক হয়, কোনো mainstream language পরে সেটা বদলাতে দেয় না। এ কারণেই এই refactoring demand করে যে type object-এর পুরো জীবনে অপরিবর্তনীয় থাকতে হবে। আমাদের school-টা এটা design দিয়েই নিশ্চিত করেছে: type বদলাতে হলে নতুন admission, নতুন object। যদি তোমার domain-এ একই object-এর type বদলাতে হয় — যেমন SIM prepaid থেকে postpaid হচ্ছে — তাহলে subclasses ঠিক tool না। তৃতীয় বিকল্প Replace Type Code with State/Strategy সেখানে কাজ করবে।
College corner: runtime-এ object কেন class বদলাতে পারে না? একটু ভাবো — runtime একটা object-কে memory-র একটা block হিসেবে রাখে। সেই block-এর shape (field, size) আর method dispatch table (vtable) class allocation-এর সময় fixed হয়ে যায়। Class বদলাতে গেলে live memory reshape করতে হবে, আর সেটার দিকে সব reference rewire করতে হবে — এ কারণেই C#, Java, TypeScript, C++ সবাই এটা বারণ করে। (Python technically __class__ assign করতে দেয়, আর Smalltalk-এ become: আছে, কিন্তু দুটোই last-resort surgery হিসেবে ধরা হয়, design হিসেবে না।) "Type বদলানো"র সৎ, portable উপায় হলো composition — বদলানো অংশটাকে একটা replaceable field হিসেবে রাখো — ঠিক এটাই State/Strategy করে।
কখন এটা দরকার?
এই লক্ষণগুলো একসাথে থাকলে বুঝবে:
- Type code আছে — একটা letter, number, বা string যেটা বলছে "এটা কী ধরনের জিনিস।" এটাই Primitive Obsession।
- Method-গুলো সেটার উপর branch করছে।
switch (this.type)বাif (type === 'H')দেখা যাচ্ছেmonthlyFee(),canLeaveAt3pm(),dinnerCount()-এর ভেতরে। Type অনুযায়ী আচরণ সত্যিই আলাদা। এটাই Switch Statements smell — Fowler এর নাম দিয়েছেন "Repeated Switches" কারণ repetition-টাই আসল সমস্যা। - একই ladder বেশ কয়েকটা method-এ repeat হচ্ছে। তিনটা method, তিনটা D/B/H branching-এর copy। W type যোগ করতে গেলে তিনটাই ঠিক করতে হবে — আর compiler বলবে না কোনটা miss হলো। রহিমের কথা মনে আছে তো?
- কিছু field শুধু কিছু type-এর জন্য meaningful।
hostelRoomNumberসব student-এর উপর বসে আছে, কিন্তু শুধু hosteller-দের জন্য মানে আছে। Day-scholar-দের কাছে এটাnull, আর সবাই সেটা নিয়ে সাবধানে হাঁটছে। - Live object-এ type কখনো বদলায় না। এটা current code দেখে না, domain expert-দের জিজ্ঞেস করে confirm করো। "D থেকে H হওয়া কি হয়?" উত্তর যদি "না, নতুন admission দিই" হয় — তাহলে subclasses fit। উত্তর যদি "হ্যাঁ, একই record-এ বদলায়" হয় — থামো, State/Strategy ব্যবহার করো।
শুধু প্রথম লক্ষণ থাকলে (code আছে, কিন্তু আচরণ বদলায় না) — heavy machinery আনার দরকার নেই, একটা value class বা enum-ই যথেষ্ট। নিচের decision guide এই choice-টা easy করে দেয়।
Before আর After — এক নজরে
School-এর billing code refactoring-এর আগে এরকম ছিল। switch কতটা count করো, আর কল্পনা করো প্রতিটা switch একটা করে staff notebook।
// BEFORE: one class, one type code, switches everywhere
type StudentType = "D" | "B" | "H";
class Student {
constructor(
public name: string,
public type: StudentType,
public tuitionFee: number,
public hostelRoomNumber: string | null, // only means something for H
) {}
monthlyFee(): number {
switch (this.type) {
case "D": return this.tuitionFee;
case "B": return this.tuitionFee + 5 * 800; // 5-day mess
case "H": return this.tuitionFee + 7 * 800 + 3000; // mess + hostel
}
}
canLeaveAt3pm(): boolean {
switch (this.type) { // the SAME ladder again
case "D": return true;
case "B": return false;
case "H": return false;
}
}
}আর পরে। একটা abstract base, তিনটা subclass, একটা factory।
// AFTER: each kind carries its own rules
abstract class Student {
constructor(public name: string, protected tuitionFee: number) {}
abstract monthlyFee(): number;
abstract canLeaveAt3pm(): boolean;
static fromRegister(
type: "D" | "B" | "H",
name: string,
tuitionFee: number,
hostelRoomNumber?: string,
): Student {
switch (type) { // the ONE remaining switch: creation only
case "D": return new DayScholar(name, tuitionFee);
case "B": return new Boarder(name, tuitionFee);
case "H": return new Hosteller(name, tuitionFee, hostelRoomNumber!);
}
}
}
class DayScholar extends Student {
monthlyFee(): number { return this.tuitionFee; }
canLeaveAt3pm(): boolean { return true; }
}
class Boarder extends Student {
monthlyFee(): number { return this.tuitionFee + 5 * 800; }
canLeaveAt3pm(): boolean { return false; }
}
class Hosteller extends Student {
constructor(name: string, tuitionFee: number,
public hostelRoomNumber: string) { // lives ONLY where it belongs
super(name, tuitionFee);
}
monthlyFee(): number { return this.tuitionFee + 7 * 800 + 3000; }
canLeaveAt3pm(): boolean { return false; }
}তিনটা লাভ লক্ষ্য করো। এক, behavioural switch-গুলো চলে গেছে — student.monthlyFee() সরাসরি কাজ করে, কারণ প্রতিটা object নিজের formula জানে। দুই, hostelRoomNumber শুধু Hosteller-এ আছে — day-scholar আর meaningless null বহন করছে না। তিন, abstract method মানে ভুলে গেলে compile error — class WeeklyBoarder extends Student বানালে compiler monthlyFee() আর canLeaveAt3pm() লেখার আগে build করতেই দেবে না। রহিমকে আর কেউ ভুলতে পারবে না — compiler নিজেই তা নিশ্চিত করে।
এই result-এর shape হলো textbook polymorphic hierarchy — ছোট, flat, আর honest।
আর runtime-এ fee bill ছাপানোর সময় আসলে কী হয়। দেখো, H letter মাত্র একবার দেখা হয় — creation-এর সময় — এরপর কেউ আর জিজ্ঞেস করে না "তুমি কোন type?" সবাই শুধু জিজ্ঞেস করে "তোমার fee কত?" আর সঠিক উত্তর আসে।
তিনটার মধ্যে কোনটা নেব?
তিনটা type-code refactoring আছে, শুরুতে সবাই confusion-এ পড়ে। এটা easy করার একটা trick আছে — শুধু দুটো প্রশ্ন করো।
প্রশ্ন ১: আচরণ কি code অনুযায়ী বদলায়? Method-গুলো কি type অনুযায়ী ভিন্ন কাজ করে — সেটার উপর switch/if ladder আছে?
প্রশ্ন ২: Runtime-এ কি type বদলাতে পারে? একই object কি জীবনে এক type থেকে আরেক type-এ যেতে পারে?
| আচরণ কি code অনুযায়ী বদলায়? | Runtime-এ type বদলাতে পারে? | এই refactoring নাও | বাস্তব উদাহরণ |
|---|---|---|---|
| না — pure label | যাই হোক | Replace Type Code with Class (বা plain enum) | School house badge — Red, Blue, Green, Yellow; সবার আচরণ একই |
| হ্যাঁ | না — object-এর পুরো জীবনে fixed | Replace Type Code with Subclasses | Day-scholar বনাম boarder বনাম hosteller — আলাদা fee আর timing, কিন্তু record কখনো type বদলায় না |
| হ্যাঁ | হ্যাঁ — একই object type বদলায় | Replace Type Code with State/Strategy | SIM যেটা prepaid থেকে postpaid হয় — একই number, নতুন আচরণ |
আমাদের D/B/H ছাত্ররা মাঝের row-এ বসে আছে: fee আর gate rule ভিন্ন (প্রশ্ন ১ — হ্যাঁ), আর record কখনো type বদলায় না (প্রশ্ন ২ — না)। Subclasses সবচেয়ে সহজ tool যেটা fit করে — কোনো extra delegation object না, কোনো indirection না, শুধু plain inheritance।
Two-axis map-এ D/B/H code বসে bottom-right কোণে: আচরণ অনেক ভিন্ন, কিন্তু type admission-এর পাথরে খোদাই।
এই table আর map তিনটা post-এই আছে — ইচ্ছা করে। যেটাতেই প্রথমে আসো, পুরো map নিয়ে যেতে পারো।
আরেকটা visual যেটা অনেক কথা বলে। কেউ কেউ ভাবে: "একটা ছাত্র hostel-এ উঠলে type বদলায় না?" দেখো school আসলে কীভাবে model করে — record বন্ধ হয়, নতুন record খোলে। কোনো object এক type থেকে আরেক type-এ যায় না; types-এর মাঝে কোনো arrow নেই।
তোমার domain যদি এই model মানতে না চায় — যদি একই live object সত্যিই type flip করে — তাহলে ওই missing arrow-টাই State/Strategy তোমার জন্য এঁকে দেয়।
ধাপে ধাপে, নিরাপদভাবে
Big-bang rewrite জিনিস ভাঙে। এখানে gentle, সবসময়-compile-হওয়া পথ দেখাচ্ছি, Fowler-এর mechanics থেকে নেওয়া।
ধাপ ১: Type code-কে self-encapsulate করো। সব read একটা getter দিয়ে যাক। এটা তোমাকে একটা single seam দেয় কাজ করার।
class Student {
private _type: "D" | "B" | "H";
get type() { return this._type; } // every reader now uses this
// ...
}ধাপ ২: Factory introduce করো। একটা static creation method যোগ করো, এখন সেটা শুধু constructor call করুক। Codebase-এর সব new Student(...) এটার মাধ্যমে route করো। আচরণ একদম আগের মতো, শুধু creation centralize হলো।
ধাপ ৩: Empty subclass তৈরি করো, প্রতিটা code-এর জন্য একটা। শুরুতে প্রতিটা subclass শুধু getter override করে নিজেকে চেনায়:
class DayScholar extends Student {
get type(): "D" { return "D"; } // intermediate stage: subclass exists,
} // but all logic still sits in the baseFactory-কে প্রতিটা code-এর জন্য সঠিক subclass return করাতে update করো। Compile করো, test চালাও। Program আগের মতোই কাজ করছে — base class-এর switch এখনো চলছে — কিন্তু hierarchy এখন exist করছে।
ধাপ ৪: একটা একটা করে method push down করো। একটা switching method নাও, যেমন monthlyFee()। Base-এ সেটাকে abstract declare করো, আর প্রতিটা branch matching subclass-এ নিয়ে যাও। Compile করো, test করো। তারপর পরের method নাও। এক method per step, মাঝে মাঝে green test।
ধাপ ৫: Dead code মুছে ফেলো। যখন কোনো behavioural switch আর বাকি নেই, base থেকে _type field আর তার constants সরিয়ে ফেলো। Letter এখন শুধু factory-র parameter-এ আর I/O boundary-তে থাকে।
দুটো ফাঁদ এড়িয়ে চলো। এক, সব method একসাথে push down করো না — কোনো test fail করলে বুঝতে পারবে না কোন move সমস্যা করেছে। এক method per step রাখলে প্রতিটা failure ছোট আর obvious হয়। দুই, factory-র switch মুছে ফেলার চেষ্টা করো না। একটা switch, এক জায়গায়, flat data থেকে object বানাচ্ছে — এটা স্বাভাবিক আর healthy। সমস্যাটা ছিল "switch আছে" না, সমস্যা ছিল "একই switch সব জায়গায় copy-paste হয়ে আছে"।
একটা বড় বাস্তব উদাহরণ
উদাহরণটা একটু বড় করি, real software-এর মতো। School-এর app-কে নাসরিন আপার জন্য dinner headcount আর জামাল ভাইয়ের জন্য fee receipt-ও বের করতে হবে। Refactoring-এর আগে ওই দুটো feature-ও নিজস্ব D/B/H ladder বানিয়েছিল, আর W-ছাত্রের ঘটনা ঠিক গল্পের মতোই হয়েছিল। Refactoring-এর পরে পুরো ছবিটা:
abstract class Student {
constructor(public name: string, protected tuitionFee: number) {}
abstract monthlyFee(): number;
abstract canLeaveAt3pm(): boolean;
abstract dinnersPerWeek(): number;
abstract typeLabel(): string;
receipt(): string {
// SHARED behaviour stays on the base — no duplication
return `${this.name} (${this.typeLabel()}): Rs. ${this.monthlyFee()}`;
}
}
class DayScholar extends Student {
monthlyFee() { return this.tuitionFee; }
canLeaveAt3pm() { return true; }
dinnersPerWeek() { return 0; }
typeLabel() { return "Day-scholar"; }
}
class Boarder extends Student {
monthlyFee() { return this.tuitionFee + 5 * 800; }
canLeaveAt3pm() { return false; }
dinnersPerWeek() { return 5; }
typeLabel() { return "Boarder"; }
}
class Hosteller extends Student {
constructor(name: string, tuitionFee: number,
public hostelRoomNumber: string) {
super(name, tuitionFee);
}
monthlyFee() { return this.tuitionFee + 7 * 800 + 3000; }
canLeaveAt3pm() { return false; }
dinnersPerWeek() { return 7; }
typeLabel() { return "Hosteller"; }
}
// NEW REQUIREMENT: weekly boarders with Saturday classes.
// We ADD a class. We edit NOTHING that already works.
class WeeklyBoarder extends Student {
monthlyFee() { return this.tuitionFee + 6 * 800; }
canLeaveAt3pm() { return false; }
dinnersPerWeek() { return 6; }
typeLabel() { return "Weekly Boarder"; }
}
// Features no longer branch at all:
function dinnerHeadcount(students: Student[]): number {
return students.reduce((sum, s) => sum + s.dinnersPerWeek(), 0);
}
function gateList(students: Student[]): Student[] {
return students.filter((s) => s.canLeaveAt3pm());
}WeeklyBoarder-টা একটু মনোযোগ দিয়ে দেখো, কারণ এটাই পুরো reward। নতুন type এলো, আমরা একটা নতুন file touch করলাম। dinnerHeadcount, gateList, আর receipt একটা character-ও বদলায়নি — তবুও সবগুলো weekly boarder-দের সঠিকভাবে handle করছে, automatically। তারিক রহিমের gate দিয়ে অনায়াসে বের হতে পারে, কারণ রহিমের gate code-কে W সম্পর্কে কিছুই শিখতে হয়নি। Compiler আমাদের প্রতিটা প্রশ্নের উত্তর দিতে বাধ্য করেছে (abstract ভুলে যাওয়া impossible করে দিয়েছে)। এটাই বিখ্যাত open/closed principle কাজে লাগছে: extension-এর জন্য open, modification-এর জন্য closed। আগের দুনিয়ায় type W মানে তিনটা switch ladder খুঁজে বের করে প্রার্থনা করা যে সব পাওয়া গেছে।
School-এর developer হিসাব রাখতেন — নতুন student type এলে কত জায়গা edit করতে হয়, refactoring-এর আগে আর পরে। Chart নিজেই বলছে — আর মনে রেখো, আগের দুনিয়ায় প্রতিটা "edit করার জায়গা" মানেই একটা ভুলে যাওয়ার জায়গা।
College corner: "এক নতুন subclass, অন্য কোথাও শূন্য edit" — এই property-র programming-language theory-তে একটা নির্দিষ্ট নাম আছে: এই design নতুন type যোগ করাকে optimize করে। বিপরীত design — switch সহ closed set of types — নতুন operation যোগ করাকে optimize করে। এই trade-off-কে বলে expression problem। Object-oriented hierarchy নতুন type সস্তা করে, নতুন operation ব্যয়সাধ্য করে (একটা নতুন abstract method প্রতিটা subclass-কে স্পর্শ করে); functional-style sum type with pattern matching নতুন operation সস্তা করে, নতুন type ব্যয়সাধ্য করে। কোনটা universally ভালো না — জিজ্ঞেস করো তোমার code কোন direction-এ grow করে। School-এর student type — type দিয়ে grow করে (W এলো, আরো আসবে), তাই subclasses সঠিক দিক।
C# আর Python-এ একই Refactoring
C# এই refactoring-কে first-class support দেয়, আর lighter end-এ কিছু interesting alternative-ও আছে।
সরাসরি hierarchy — abstract base, sealed subclass, একটা factory:
public abstract class Student
{
public string Name { get; }
protected decimal TuitionFee { get; }
protected Student(string name, decimal tuitionFee)
=> (Name, TuitionFee) = (name, tuitionFee);
public abstract decimal MonthlyFee();
public abstract bool CanLeaveAt3Pm();
public static Student FromRegister(char type, string name,
decimal tuitionFee, string? roomNumber = null) =>
type switch // the one creation switch
{
'D' => new DayScholar(name, tuitionFee),
'B' => new Boarder(name, tuitionFee),
'H' => new Hosteller(name, tuitionFee, roomNumber!),
_ => throw new ArgumentException($"Unknown student type: {type}")
};
}
public sealed class DayScholar(string name, decimal fee) : Student(name, fee)
{
public override decimal MonthlyFee() => TuitionFee;
public override bool CanLeaveAt3Pm() => true;
}
public sealed class Boarder(string name, decimal fee) : Student(name, fee)
{
public override decimal MonthlyFee() => TuitionFee + 5 * 800m;
public override bool CanLeaveAt3Pm() => false;
}
public sealed class Hosteller(string name, decimal fee, string roomNumber)
: Student(name, fee)
{
public string HostelRoomNumber { get; } = roomNumber;
public override decimal MonthlyFee() => TuitionFee + 7 * 800m + 3000m;
public override bool CanLeaveAt3Pm() => false;
}abstract member TypeScript-এর মতোই compiler guarantee দেয়: নতুন subclass যদি MonthlyFee() ভুলে যায়, compile-ই হবে না।
Plain enum কী কাজ করবে না? C# enum StudentType { DayScholar, Boarder, Hosteller } আর switch expression লোভনীয়, আর modern C# missing enum member-এর জন্য warn-ও করে। কিন্তু আচরণ এখনো type-এর বাইরে থাকে — যেসব class switch ধরে রাখে সেগুলোতে ছড়িয়ে। নতুন member মানে সব জায়গা revisit করা। Enum-গুলো Class refactoring-এর no-behaviour case-এর জন্য সুন্দর; repeated behavioural switch সরানোর কাজে এরা পারে না।
মাঝামাঝি পথ: behaviour সহ smart enum। Ardalis-এর SmartEnum library একটা elegant trick সাপোর্ট করে — enum class নিজেই abstract, আর প্রতিটা named instance একটা ছোট subclass যেটা আচরণ সরবরাহ করে:
using Ardalis.SmartEnum;
public abstract class StudentType : SmartEnum<StudentType>
{
public static readonly StudentType DayScholar = new DayScholarType();
public static readonly StudentType Boarder = new BoarderType();
public static readonly StudentType Hosteller = new HostellerType();
private StudentType(string name, int value) : base(name, value) { }
public abstract decimal MonthlyFee(decimal tuition);
private sealed class DayScholarType() : StudentType("DayScholar", 1)
{ public override decimal MonthlyFee(decimal t) => t; }
private sealed class BoarderType() : StudentType("Boarder", 2)
{ public override decimal MonthlyFee(decimal t) => t + 5 * 800m; }
private sealed class HostellerType() : StudentType("Hosteller", 3)
{ public override decimal MonthlyFee(decimal t) => t + 7 * 800m + 3000m; }
}এটা সব এক file-এ রাখে আর free parsing দেয় (StudentType.FromValue(2)), ছোট variation-এ কাজে লাগে। আরো rich type-এর জন্য — যেখানে subclass-এ নিজস্ব field আছে যেমন HostelRoomNumber আর অনেক আচরণ আছে — উপরের full hierarchy পরিষ্কার। দুটোই আজকের refactoring-এর honest implementation: আচরণ type-এর উপর থাকে, switch মিলিয়ে যায়।
Python-এ, abstract base class machinery compiler-এর কাজ করে instantiation-এর সময় — কোনো rule ভুলে যাওয়া subclass instance তৈরিই করতে পারবে না:
from abc import ABC, abstractmethod
class Student(ABC):
def __init__(self, name: str, tuition_fee: int):
self.name = name
self.tuition_fee = tuition_fee
@abstractmethod
def monthly_fee(self) -> int: ...
@abstractmethod
def can_leave_at_3pm(self) -> bool: ...
class DayScholar(Student):
def monthly_fee(self) -> int:
return self.tuition_fee
def can_leave_at_3pm(self) -> bool:
return True
class Hosteller(Student):
def __init__(self, name: str, tuition_fee: int, room: str):
super().__init__(name, tuition_fee)
self.hostel_room_number = room # lives only where it belongs
def monthly_fee(self) -> int:
return self.tuition_fee + 7 * 800 + 3000
def can_leave_at_3pm(self) -> bool:
return False
# WeeklyBoarder missing monthly_fee()? TypeError the moment you instantiate it.College corner: modern language-গুলো একটা সুন্দর refinement যোগ করেছে — sealed hierarchy। Java-র sealed interface Student permits DayScholar, Boarder, Hosteller, Kotlin-এর sealed class, আর C#-এর closed pattern-matching over known hierarchy — সব compiler-কে জানায় subclass-এর complete list। Payoff হলো অন্য দিক থেকে exhaustiveness: sealed type-এর উপর switch যদি কোনো case miss করে, compile fail হয় — ঠিক enum switch-এর মতো। TypeScript discriminated union আর never check দিয়ে একই কাজ করে। Sealed hierarchy দুই দিকের safety একসাথে দেয়: behaviour-এর জন্য polymorphism যেটা type-এর উপর থাকে, আর boundary code-এর জন্য exhaustive matching (serializer, mapper) যেটা legitimately types enumerate করে।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Behavioural switch চলে যায় — polymorphism নিজেই branch করে | Runtime-এ type কখনো বদলানো যাবে না — বদলালে এটা ভুল tool; State/Strategy ব্যবহার করো |
| প্রতিটা type-এর নিয়ম এক cohesive class-এ থাকে | একটা class per type — behaviour-free label-এর জন্য heavy machinery (class/enum ব্যবহার করো) |
| নতুন type যোগ হয়, patch হয় না — existing code অপরিবর্তিত থাকে (open/closed) | শুধু একটা type code hierarchy drive করতে পারে; দ্বিতীয় varying code combinatorial subclass তৈরি করে |
abstract method compiler-কে দিয়ে প্রতিটা behaviour demand করায় | Factory-তে একটা creation switch থাকে (স্বাভাবিক, কিন্তু আছে) |
| Type-specific field (hostel room) শুধু যে type-এর দরকার তার কাছেই থাকে | Inheritance subclass-কে base-এর সাথে couple করে; গভীর hierarchy rigid হয়ে যেতে পারে |
| প্রতিটা subclass ছোট আর independently testable | Persistence-এ mapping দরকার: flat type column-কে factory দিয়ে round-trip করতে হবে |
কোন কোন smell সারায়?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Switch Statements | Primary cure — repeated behavioural switch polymorphic dispatch-এ রূপান্তর হয় |
| Primitive Obsession | আচরণ নিয়ন্ত্রণকারী letter code real type হয়ে যায় |
| Shotgun surgery | Type যোগ করলে শুধু এক নতুন subclass touch করতে হয়, codebase-এর সব switch না |
| Null-padded field সহ Data class | Type-specific field শুধু সেই subclass-এ যায় যেটার জন্য meaningful |
| Duplicate conditional logic | প্রতিটা type-এর নিয়মের single source of truth হলো সেই type-এর নিজের class |
পুরো ব্যাপারটা এক ছবিতে
Quick Revision Box
+----------------------------------------------------------------+
| REPLACE TYPE CODE WITH SUBCLASSES - REVISION CARD |
+----------------------------------------------------------------+
| Problem : type code DRIVES behaviour -> same switch ladder |
| copy-pasted across methods, drifting out of sync |
| Demand : type is FIXED for the object's whole life |
| Solution : abstract base + one subclass per code, |
| switch branches become overrides, |
| ONE switch survives - in the factory (creation) |
| Result : new type = new subclass; old code untouched; |
| compiler enforces every abstract behaviour |
| |
| WHICH OF THE THREE? |
| no behaviour varies -> CLASS / ENUM |
| behaviour varies, type fixed -> SUBCLASSES (this one) |
| behaviour varies + type changes-> STATE / STRATEGY |
+----------------------------------------------------------------+Practice করে দেখো
তোমার পালা। একটা courier company তার shipment-কে code করে: "S" = Standard, "E" = Express, "O" = Overnight। নিচের code-এ smell আগে থেকেই আছে — একই ladder দুইবার, আর একটা তৃতীয় feature (insurance) সেটা আবার copy করতে আসছে।
class Shipment {
constructor(
public weightKg: number,
public kind: "S" | "E" | "O",
) {}
price(): number {
if (this.kind === "S") return this.weightKg * 40;
if (this.kind === "E") return this.weightKg * 70 + 50;
return this.weightKg * 120 + 150; // overnight
}
deliveryDays(): number {
if (this.kind === "S") return 5;
if (this.kind === "E") return 2;
return 1;
}
}ধাপে ধাপে refactor করো:
- আগে দুটো প্রশ্ন confirm করো: আচরণ ভিন্ন (হ্যাঁ — price আর days), আর একটা shipment booking-এর পর kind বদলায় না (ধরে নাও হ্যাঁ)। তাহলে Subclasses সঠিক choice।
Shipment-কে abstract করোabstract price()আরabstract deliveryDays()সহ, আর একটা static factoryShipment.fromCode(kind, weightKg)লেখো যেটায় একটাই creation switch থাকবে।StandardShipment,ExpressShipment, আরOvernightShipmentতৈরি করো, এক method-এর branch এক সময়ে নিয়ে যাও — move-এর মাঝে compile করো, ঠিক উপরের পাঁচ safe step-এর মতো।- এখন নতুন requirement যোগ করো:
"SD"= Same-Day delivery (priceweightKg * 200 + 300, 0 days)। নিজে দেখো — শুধু একটা class আর এক factory line যোগ করেছো, আর compiler দুটো method লেখার আগে build করতে দেয়নি। - Bonus চিন্তা: marketing team বলছে "একজন customer booked Standard shipment Express-এ upgrade করতে পারবে পার্থক্য দিয়ে — একই shipment, নতুন kind।" Decision table-এর কোন row-এ এই requirement চলে গেলো? এক বাক্যে বলো তুমি কী করবে।
প্রশ্ন ৫-এ যদি তোমার মাথায় আসে "একই object এখন type বদলাচ্ছে, তাহলে State/Strategy লাগবে" — তুমি সম্পূর্ণ ready এই family-র তৃতীয় post-এর জন্য: Replace Type Code with State/Strategy, যেখানে একটা জেদি SIM card তার number বদলাতে রাজি না, কিন্তু rules বদলাতে খুশি।
সচরাচর জিজ্ঞাসা
- Replace Type Code with Class আর Replace Type Code with Subclasses-এর পার্থক্য কী?
- Replace Type Code with Class শুধু value-গুলোর নাম দেয় আর validate করে — যখন code গুলো pure label মাত্র। Replace Type Code with Subclasses তখন লাগে যখন code আচরণ নির্ধারণ করে — মানে method গুলো type অনুযায়ী ভিন্ন কাজ করে — আর প্রতিটা আচরণ তার নিজস্ব subclass-এ চলে যায়, switch উধাও হয়ে যায়।
- Object-এর পুরো জীবনে type কেন fixed থাকতে হবে?
- কারণ object-এর class construction-এর সময় ঠিক হয়ে যায়, পরে আর বদলানো যায় না। যদি একই object runtime-এ type বদলাতে হয় — যেমন একটা account basic থেকে premium হচ্ছে — তাহলে subclasses কাজ করবে না। সেক্ষেত্রে State/Strategy দরকার, যেটা একটা collaborator object swap করে।
- Factory method-এ একটা switch থেকেই যাওয়া কি সমস্যা?
- না, এই একটা switch সম্পূর্ণ স্বাভাবিক আর ভালো। D, B বা H code থেকে সঠিক object বানানোর কাজটা কোথাও না কোথাও হতেই হবে। এর সুবিধা হলো এটা ঠিক এক জায়গায় হয়, বহু method-এ copy-paste হয়ে ছড়িয়ে পড়ে না।
- একই object-এ দুটো আলাদা type code ভিন্ন আচরণ করলে কী করব?
- দুটোতেই subclass করলে combination বিস্ফোরণ হবে — যেমন DayScholarScienceStream, BoarderScienceStream ইত্যাদি। একটা object-এর মাত্র একটাই superclass chain থাকতে পারে। দ্বিতীয় varying dimension-এর জন্য composition ব্যবহার করো — একটা code-এর জন্য subclass রাখো, আরেকটার জন্য State/Strategy object।
- এই refactoring আর Replace Conditional with Polymorphism-এর সম্পর্ক কী?
- Replace Type Code with Subclasses class hierarchy তৈরি করে — এটা কাঠামো বানায়। Replace Conditional with Polymorphism তারপর প্রতিটা switch branch-কে matching subclass-এ নিয়ে যায়। বাস্তবে দুটো একসাথেই করো: আগে subclass তৈরি করো, তারপর conditional-গুলো override-এ রূপান্তর করো।
আরো দেখো
সম্পর্কিত পাঠ
Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা
Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।
Replace Type Code with Class: ম্যাজিক নম্বরকে একটা আসল পরিচয় দাও
Replace Type Code with Class রিফ্যাক্টরিং শেখো একটা স্কুলের গল্প দিয়ে — TypeScript আর C#-এ before/after দেখো, আর কখন Class, Subclasses বা State/Strategy বেছে নেবে সেটা একটা সহজ decision table দিয়ে বুঝে নাও।
Replace Type Code with State/Strategy: যখন Type নিজেই বদলে যায়
Replace Type Code with State/Strategy refactoring শেখো prepaid থেকে postpaid SIM-এর গল্পের মাধ্যমে — TypeScript আর C#-এ swappable plan object, আর কখন Class vs Subclasses vs State/Strategy বেছে নেবে তার পুরো guide।