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।
SIM যখন মন বদলালো
ধরো জামাল চাচার একটা ছোট মোবাইল সার্ভিসিং দোকান আছে — পুরান ঢাকার কাছাকাছি। বারো বছর ধরে একই নম্বর ব্যবহার করছেন। নম্বরটা দোকানের সাইনবোর্ডে লেখা, ভিজিটিং কার্ডে প্রিন্ট করা, আর পাঁচশো customer-এর ফোনে সেভ করা। নম্বর কোনোদিন বদলানো যাবে না। নম্বরটাই দোকান।
বছরের পর বছর চাচার SIM ছিল prepaid। prepaid জীবনের নিয়ম: আগে রিচার্জ, পরে কথা। প্রতিটা call balance কেটে নেয়। Balance শেষ হলে call বন্ধ — কিন্তু অন্য কোনো ঝামেলা নেই। মাস শেষে কোনো bill আসে না। চাচার এই নিয়মকানুন পছন্দ ছিল, কিন্তু মাসে মাসে রিচার্জের দোকানে যেতে বিরক্ত লাগতো। আর বছরে দুবার এমন হতো — balance চুপচাপ শেষ হয়ে গেছে, customer-এর গুরুত্বপূর্ণ call মিস।
গত মাসে তার মেয়ে সুমাইয়া — যে ঢাকায় CSE পড়ে — শেষমেশ রাজি করালো SIM-টা postpaid-এ convert করতে। Same SIM, same নম্বর, same ফোন, same সাইনবোর্ড। কিন্তু নিয়মকানুন পুরো উল্টে গেল। এখন আগে কথা বলো, পরে bill দাও। Call balance কাটে না, bill বাড়ে। Credit limit আছে, recharge নেই। আর time মতো bill না দিলে connection suspend — এটা prepaid জীবনে কখনো হতো না।
একটু থামো। এই ছোট গল্পে দুটো জিনিস লুকিয়ে আছে — আর এই দুটোই আজকের পুরো lesson:
- Plan type-এর উপর behaviour ভিন্ন। prepaid-এ call করলে balance কাটে; postpaid-এ same call করলে bill বাড়ে। Recharge মানে আছে prepaid-এ, postpaid-এ কোনো মানে নেই।
- Type বদলেছে SAME SIM-এ। জামাল চাচা নতুন নম্বর নেননি। Same object — তার SIM, তার identity, তার বারো বছরের নম্বর — runtime-এ এক set নিয়ম থেকে অন্য set-এ চলে গেল।
এবার programmer-এর মতো ভাবো, যেভাবে সুমাইয়া বাসে বসে ভাবছিল। যদি আমরা inheritance দিয়ে model করতাম — PrepaidSim class আর PostpaidSim class — তাহলে আটকে যেতাম। Object-এর class জন্মের সময় ঠিক হয়, পরে বদলানো যায় না। একটা PrepaidSim object কখনো PostpaidSim হতে পারবে না। পুরনো object destroy করে নতুন বানাতে হতো, সব data copy করতে হতো — আর পাঁচশো customer-এর ফোনে save করা পুরনো reference কোথায় যেতো?
Telecom company-র কাছে একটা চালাক mental model আছে — সেটাই আজকের refactoring। SIM একটা fixed জিনিস। Plan হলো SIM-এর file-এর ভেতরে রাখা একটা আলাদা card — সেটা বের করে বদলানো যায়। prepaid card সরিয়ে postpaid card লাগাও, same SIM তখনই নতুন নিয়মে চলে। এটাই Replace Type Code with State/Strategy: varying behaviour-কে একটা swappable object-এ নিয়ে যাও।
Replace Type Code with State/Strategy কী?
এটা type-code family-র তৃতীয় সদস্য — আর সবচেয়ে flexible। তার দুই ভাই হলো Replace Type Code with Class (pure label-এর জন্য) আর Replace Type Code with Subclasses (behaviour vary করে কিন্তু কখনো বদলায় না এই ক্ষেত্রে)। এই refactoring সবচেয়ে কঠিন combination handle করে: type code behaviour drive করে আর object-এর lifetime-এ বদলায়।
কীভাবে করবে? এই recipe follow করো:
- একটা interface define করো যা vary করে —
PlanBehaviourwithmakeCall(),recharge(),monthEnd()। - প্রতিটা type code-এর জন্য একটা implementation লেখো —
PrepaidPlan,PostpaidPlan। একটা type-এর সব rule এক জায়গায়। - Host-এ একটা field দাও যেটা current implementation ধরে রাখে। SIM-এ একটা
planreference। - Delegate করো। Host-এর method-গুলো আর code-এ switch করবে না — current plan object-এ forward করবে।
- Transition মানে swap। Postpaid-এ convert করা মানে একটাই assignment:
this.plan = new PostpaidPlan()। Host-এর identity অক্ষত; শুধু behaviour-card বদলেছে।
এটা কাজ করে কারণ inheritance-এর বদলে composition ব্যবহার করা হয়। Subclass-এ variation object-এর নিজের class-এ bake হয়ে যায় — birth certificate-এর মতো চিরকালের জন্য। Composition variation রাখে একটা part-এ, আর part replace করা যায় যখন খুশি। এটাই subclass-এর যা সাধ্য নেই।
এক লাইনে সারসংক্ষেপ: Replace Type Code with State/Strategy প্রতিটা type-এর behaviour একটা common interface-এর পেছনে swappable object-এ নিয়ে যায়, যাতে same host runtime-এ সেই object replace করে behaviour বদলাতে পারে — subclassing দিয়ে যা কোনোদিন সম্ভব না।
একটু deeper ভাবো: "live object-এর class বদলাও" কেন forbidden? কারণ allocation-এর সময় runtime object-এর memory layout (fields) আর dispatch table (vtable) fix করে দেয়। Class বদলানো মানে প্রতিটা existing reference-এর নিচে memory reshape করা। কিছু dynamic language এই trap দেখায় — Python-এ instance.__class__ assign করা যায়, Smalltalk-এ become: আছে, JavaScript-এ Object.setPrototypeOf দিয়ে prototype মিউটেট করা যায়। কিন্তু সবগুলোই documented trap: JIT optimization ভাঙে, invariant নষ্ট হয়। সব language-এ সম্মানজনক উত্তর একটাই — host-এ identity রাখো, changeable behaviour part-এ রাখো, আর part swap করো।
State না Strategy — কোনটা?
এই refactoring-এর নামে দুটো design pattern আছে কারণ structure দুটোর জন্য একই; শুধু intent আলাদা।
- State pattern: swappable object-গুলো একটা lifecycle-এর phase represent করে। কোনটা legal transition, পরে কী হবে — state object নিজেই সিদ্ধান্ত নিতে পারে। Prepaid আর Postpaid আর Suspended — এটা একটা ছোট state machine। চাচার SIM-এর গল্পটা State pattern।
- Strategy pattern: swappable object-গুলো interchangeable algorithm — বাইরে থেকে client বেছে দেয়, কোনো progression নেই। Route-finding method বা discount calculation বেছে নেওয়া — এটা Strategy।
Refactoring করার আগে label বেছে নেওয়া জরুরি না — আগে swappable object বানাও, intent নিজেই বলে দেবে কোনটা। দুটো pattern নিয়ে আলাদা post আছে: State আর Strategy।
Interview tip: Gang of Four বইতে State আর Strategy-র structure diagram literally একই। একটা ভালো পার্থক্য: জিজ্ঞেস করো কে object বদলায় আর কতবার। Strategy সাধারণত construction-এর সময় একবার inject হয় আর খুব কম বদলায়; State object-এর life-এ বারবার churn করে, আর legal transition graph domain rule-এর অংশ। আরেকটা structural fingerprint: state প্রায়ই host-এর back-reference রাখে যাতে transition trigger করতে পারে; strategy সাধারণত host-কে চেনে না — সব কিছু parameter হিসেবে পায়।
কখন দরকার হয়?
এই combination-টা দেখলেই বুঝবে:
- Type code আছে আর methods সেটায় branch করে।
switch (this.planType)লেখা আছেmakeCall(),recharge(),monthEnd()-এ — same ladder copy-paste করা প্রতিটা method-এ। এটা Switch Statements smell আর Primitive Obsession-এর মিলিত রূপ। - Code শুধু পড়া না, লেখাও হয়। কোথাও
sim.planType = POSTPAIDআছে — type live object-এ mutate করছে। এই একটা observation-ই subclasses বাতিল করে দেয়। - "Same identity, new rules" domain language-এ আছে। "Plan convert করো", "account upgrade করো", "subscription suspend করো", "user promote করো"। Order Pending থেকে Paid থেকে Shipped-এ যাচ্ছে। Same object phase-এ phase-এ যাচ্ছে।
- Illegal transition bug ঘটাচ্ছে। Suspended SIM somehow call করছে; unshipped order delivered হয়ে গেছে। Transition rule যখন switches জুড়ে ছড়িয়ে থাকে, কেউ এক জায়গায় enforce করতে পারে না।
- দুটো independent code একটা object-এ vary করছে। Subclassing দুই dimension-এ করলে
PrepaidStudentSim,PostpaidStudentSim,PrepaidSeniorSim— explosion। Composition প্রতিটা dimension আলাদা swappable object দিয়ে handle করে।
কখন করবে না সেটাও জানো। Type যদি কখনো live object-এ না বদলায়, তাহলে simpler Subclasses ব্যবহার করো — delegation layer maintain করতে হবে না। যদি code-এর পেছনে কোনো behaviour-ই না থাকে, তাহলে value class বা enum যথেষ্ট। এই refactoring তিনটার মধ্যে সবচেয়ে powerful — আর সবচেয়ে বেশি machinery; শুধু তখনই ব্যবহার করো যখন দুটো sign-ই সত্যি।
আগে আর পরে — এক নজরে
এই হলো refactoring-এর আগের SIM — একটা class, একটা mutable type code, switch বাড়তেই থাকছে:
// BEFORE: a mutable type code with behaviour switches everywhere
const PREPAID = 0;
const POSTPAID = 1;
class Sim {
planType = PREPAID; // changes at runtime!
balance = 0; // prepaid only
monthlyBill = 0; // postpaid only
makeCall(minutes: number): void {
switch (this.planType) {
case PREPAID:
if (this.balance < minutes) throw new Error("Recharge first");
this.balance -= minutes;
break;
case POSTPAID:
this.monthlyBill += minutes * 1.2;
break;
}
}
monthEnd(): string {
switch (this.planType) { // the SAME switch again
case PREPAID: return "No bill. Balance carries forward.";
case POSTPAID: return `Bill generated: Rs. ${this.monthlyBill}`;
default: throw new Error("Unknown plan");
}
}
}আর এই হলো পরে। Plan একটা swappable object; SIM delegate করে:
// AFTER: behaviour lives in a swappable plan object
interface PlanBehaviour {
makeCall(minutes: number): void;
monthEnd(): string;
}
class PrepaidPlan implements PlanBehaviour {
private balance = 0;
recharge(amount: number) { this.balance += amount; }
makeCall(minutes: number) {
if (this.balance < minutes) throw new Error("Recharge first");
this.balance -= minutes;
}
monthEnd() { return "No bill. Balance carries forward."; }
}
class PostpaidPlan implements PlanBehaviour {
private monthlyBill = 0;
makeCall(minutes: number) { this.monthlyBill += minutes * 1.2; }
monthEnd() { return `Bill generated: Rs. ${this.monthlyBill}`; }
}
class Sim {
constructor(
readonly number: string, // identity NEVER changes
private plan: PlanBehaviour = new PrepaidPlan(),
) {}
// The transition: swap the card, keep the SIM
convertToPostpaid() { this.plan = new PostpaidPlan(); }
convertToPrepaid() { this.plan = new PrepaidPlan(); }
makeCall(minutes: number) { this.plan.makeCall(minutes); }
monthEnd(): string { return this.plan.monthEnd(); }
}
const uncleSim = new Sim("98390-12345");
uncleSim.convertToPostpaid(); // same SIM, same number, new rules — at runtimeতিনটা জিনিস দেখো। Switch গুলো গেছে — makeCall এখন একটাই delegating line। Plan-specific field গুলো নিজের জায়গায় চলে গেছে: balance শুধু PrepaidPlan-এ, monthlyBill শুধু PostpaidPlan-এ; SIM-এ আর meaningless field নেই। আর convertToPostpaid() — এটাই পুরো miracle: একটা assignment পুরো object-এর behaviour বদলে দেয়, আর identity — সেই বারো বছরের নম্বর — অটুট থাকে।
Design-এর static shape একবার মনোযোগ দিয়ে দেখো: host একটা interface compose করে, আর implementation গুলো সেই interface থেকে ঝুলে থাকে — host থেকে না।
আর এই হলো runtime-এ কথোপকথন। Diagram-এর মাঝখানটা দেখো: দুটো call-এর মাঝে swap হচ্ছে, কিন্তু caller কিছুই টের পাচ্ছে না — শুধু নতুন behaviour দেখছে।
তিনটার মধ্যে কোনটা বেছে নেবে?
এই হলো পুরো type-code family-র map — তিনটা post-এই একই guide আছে, তাই যেকোনো একটা পড়লেই পুরো choice জানতে পারবে। তোমার type code নিয়ে দুটো প্রশ্ন করো:
প্রশ্ন ১: Code-এর উপর behaviour vary করে? Method-গুলো কি type অনুযায়ী আলাদা কাজ করে — switch/if ladder আছে?
প্রশ্ন ২: Runtime-এ type বদলাতে পারে? Same object কি তার lifetime-এ এক type থেকে অন্যটায় যেতে পারে?
| Code-এ behaviour vary করে? | Runtime-এ type বদলায়? | কোন refactoring বেছে নেবে | বাস্তব উদাহরণ |
|---|---|---|---|
| না — pure label | যাই হোক | Replace Type Code with Class (বা plain enum) | School-এর house badge — Red, Blue, Green, Yellow; সবার behaviour একই |
| হ্যাঁ | না — পুরো life-এ fixed | Replace Type Code with Subclasses | Day scholar বনাম boarder — আলাদা fee আর schedule, কিন্তু record কখনো type flip করে না |
| হ্যাঁ | হ্যাঁ — same object type switch করে | Replace Type Code with State/Strategy | Prepaid থেকে postpaid-এ যাওয়া SIM — same নম্বর, নতুন behaviour |
জামাল চাচার SIM দুটো প্রশ্নেই হ্যাঁ বলে — behaviour vary করে (prepaid আর postpaid-এর rule আলাদা) আর type same live object-এ flip করে। Bottom row। শুধু State/Strategy fit করে, কারণ শুধু composition-ই construction-এর পরে behaviour বদলাতে দেয়।
Two-axis map-এ SIM আছে একদম top-right কোণে — maximum behaviour difference, maximum runtime movement। সেই কোণটা শুধু আজকের refactoring-এর।
পুরো family মনে রাখার trick: label মানে badge, fixed kind মানে birth certificate, changing kind মানে replaceable card। Badge শুধু নাম দেয়। Birth certificate একবার ঠিক হয়, আর বদলায় না। Wallet-এর card যেকোনোদিন swap করা যায়, আর তুমি তুমিই থাকো।
Step-by-step — নিরাপদ পথে
এই refactoring-এ moving part বেশি, তাই ছোট ছোট ধাপ আরও বেশি জরুরি। Fowler-এর mechanics follow করো।
Step 1: Type code-কে self-encapsulate করো। প্রতিটা read আর write getter আর setter দিয়ে করো। এখন code-এর একমাত্র দরজা তোমার কাছে।
class Sim {
private _planType = PREPAID;
get planType() { return this._planType; }
set planType(value: number) { this._planType = value; } // the one doorway
}Step 2: Interface আর empty implementation বানাও। PlanBehaviour define করো আর PrepaidPlan আর PostpaidPlan empty shell হিসেবে লেখো। এখনো কেউ use করছে না; সব compile হচ্ছে।
Step 3: State field plant করো আর setter-এর সাথে tie করো। এটা key intermediate stage — পুরনো আর নতুন একসাথে আছে। Type code এখনো আছে, কিন্তু প্রতিটা write-এ plan object-ও swap হচ্ছে:
class Sim {
private plan: PlanBehaviour = new PrepaidPlan(); // new — planted
set planType(value: number) {
this._planType = value; // old — still alive
this.plan = value === PREPAID
? new PrepaidPlan()
: new PostpaidPlan(); // kept in sync
}
}Step 4: একটা একটা করে method move করো। monthEnd() দিয়ে শুরু করো। Interface-এ add করো, প্রতিটা switch branch matching plan class-এ move করো, host delegate করুক: monthEnd() { return this.plan.monthEnd(); }। Compile করো, test করো, breath নাও। তারপর makeCall()। এক ধাপে এক method।
Step 5: Type-specific field move করো। balance চলে যাক PrepaidPlan-এ, monthlyBill চলে যাক PostpaidPlan-এ। Host slim হয়ে শুধু identity আর plan reference রাখে।
Step 6: Code assignment-কে named transition দিয়ে replace করো, তারপর code delete করো। প্রতিটা sim.planType = POSTPAID হয়ে যাক sim.convertToPostpaid()। যখন _planType-এর কোনো reader আর নেই, field আর constants delete করো। Database বা API boundary-তে stored string-কে load-এ সঠিক plan object-এ map করতে একটা small factory দরকার।
সবচেয়ে বিপজ্জনক মুহূর্ত হলো Step 3 থেকে 6-এর মাঝে, যখন পুরনো code আর নতুন object দুটোই একসাথে বেঁচে আছে। কোনো write path যদি setter bypass করে সরাসরি _planType set করে, তাহলে plan object silently stale হয়ে যাবে আর SIM ভুল plan-এ behave করবে। এই কারণেই Step 1 (self-encapsulation) optional না — শুরু করার আগে সব side door বন্ধ করো। আর transition-এ data নিয়েও ভাবো: prepaid থেকে postpaid-এ convert হলে বাকি balance-এর কী হবে? সিদ্ধান্ত নাও (refund করো, বা first bill-এ credit দাও) আর সেটার test লেখো; accident-এ হারিয়ে যেতে দিও না।
বড় real-life উদাহরণ
Real telecom জীবনে তৃতীয় phase আছে: Suspended। Postpaid-এ convert হওয়ার দুই মাস পরে জামাল চাচা সেটা শিখলেন কঠিনভাবে — ব্যস্ত ঈদের মৌসুম, ভুলে যাওয়া bill, আর এক মঙ্গলবার সকালে দোকানের নম্বর চুপ হয়ে গেল। Call আসছে না, call যাচ্ছে না। নম্বর আছে, SIM জীবিত, কিন্তু নিয়ম আবার বদলে গেল — এবার company-র সিদ্ধান্তে, তার নিজের না। Bill দেওয়ার এক ঘণ্টার মধ্যে activate হলো।
এখন সত্যিকারের state machine হয়েছে, আর State pattern shine করছে। দেখো কীভাবে প্রতিটা state নিজেই পরের state ঠিক করতে পারে:
interface PlanState {
makeCall(sim: Sim, minutes: number): void;
payment(sim: Sim, amount: number): void;
label(): string;
}
class PrepaidState implements PlanState {
private balance = 0;
makeCall(sim: Sim, minutes: number) {
if (this.balance < minutes) throw new Error("Recharge first");
this.balance -= minutes;
}
payment(sim: Sim, amount: number) { this.balance += amount; } // recharge
label() { return "Prepaid"; }
}
class PostpaidState implements PlanState {
private bill = 0;
private static readonly CREDIT_LIMIT = 2000;
makeCall(sim: Sim, minutes: number) {
this.bill += minutes * 1.2;
if (this.bill > PostpaidState.CREDIT_LIMIT) {
sim.transitionTo(new SuspendedState(this.bill)); // state decides next state
}
}
payment(sim: Sim, amount: number) { this.bill = Math.max(0, this.bill - amount); }
label() { return "Postpaid"; }
}
class SuspendedState implements PlanState {
constructor(private dues: number) {}
makeCall(_sim: Sim, _minutes: number): void {
throw new Error("Connection suspended. Please clear dues.");
}
payment(sim: Sim, amount: number) {
this.dues -= amount;
if (this.dues <= 0) {
sim.transitionTo(new PostpaidState()); // pay dues -> reactivated
}
}
label() { return "Suspended"; }
}
class Sim {
private state: PlanState = new PrepaidState();
constructor(readonly number: string) {}
transitionTo(next: PlanState) {
console.log(`${this.number}: ${this.state.label()} -> ${next.label()}`);
this.state = next;
}
convertToPostpaid() { this.transitionTo(new PostpaidState()); }
makeCall(minutes: number) { this.state.makeCall(this, minutes); }
payment(amount: number) { this.state.payment(this, amount); }
}SuspendedState.makeCall পড়ো আর enjoy করো: "suspended SIM call করতে পারবে না" — এই rule এখন ঠিক একটা obvious জায়গায়, বিশাল switch-এর ভেতরে forgotten if হিসেবে না। Transition rule-ও explicit — credit limit cross করলে suspend; dues clear হলে reactivate। Host নিজেকে (sim) state method-এ pass করে যাতে state transitionTo trigger করতে পারে — এটা standard State-pattern handshake। নতুন একটা phase যোগ করতে — যেমন বিদেশ গেলে SafeCustodyState — মানে শুধু একটা class add করা আর সংশ্লিষ্ট transition। আর কিছু বদলায় না।
যদি variation-এর কোনো lifecycle না থাকতো — ধরো SIM তিনটা billing algorithm offer করে (per-second, per-minute, bulk-pack) যেটা customer app থেকে বেছে নেয় — same structure হতো Strategy: client বাইরে থেকে algorithm set করে, এক strategy থেকে আরেকটায় কোনো "flow" নেই। Same skeleton, ভিন্ন আত্মা।
সুমাইয়া, CS student হিসেবে, হিসাব রাখলো প্রতিটা নতুন plan phase codebase-এ আগে আর পরে কত খরচ করে। Switch world-এ নতুন phase মানে প্রতিটা ladder edit করা আর প্রতিটা test ঠোকানো; state world-এ মানে একটা নতুন class লেখা আর তার transition wire করা।
College corner: Python আসলে forbidden trick permit করে — sim.__class__ = PostpaidSim কাজ "করবে" — আর একবার sandbox-এ দেখার মতো কেন কেউ এটা ship করে না: নতুন class-এর __init__ কখনো run হয়নি, তাই তার invariant নেই; পুরনো class-এর যা state ছিল সেটা এখন misinterpret হচ্ছে; type checker, serializer, আর teammates সবাই ধরে নেয় class stable। Python-এর clean version আজকের TypeScript-এর মতোই দেখতে — একটা PlanState protocol, phase প্রতি একটা class, আর self._state attribute reassign হয়। Composition language-এর limitation-এর workaround না; এটা সেই design যেটা identity আর behaviour আলাদা রাখে এমনকি যে language hack allow করে তাতেও।
from abc import ABC, abstractmethod
class PlanState(ABC):
@abstractmethod
def make_call(self, sim: "Sim", minutes: int) -> None: ...
class Prepaid(PlanState):
def __init__(self) -> None:
self.balance = 0
def make_call(self, sim: "Sim", minutes: int) -> None:
if self.balance < minutes:
raise RuntimeError("Recharge first")
self.balance -= minutes
class Postpaid(PlanState):
def __init__(self) -> None:
self.bill = 0.0
def make_call(self, sim: "Sim", minutes: int) -> None:
self.bill += minutes * 1.2
class Sim:
def __init__(self, number: str) -> None:
self.number = number # identity never changes
self._state: PlanState = Prepaid()
def convert_to_postpaid(self) -> None:
self._state = Postpaid() # the swap, not a class change
def make_call(self, minutes: int) -> None:
self._state.make_call(self, minutes)C#-এ same refactoring
C# version একদম one-to-one, আর interface আর expression-bodied member দিয়ে সুন্দর দেখায়:
public interface IPlanState
{
void MakeCall(Sim sim, int minutes);
void Payment(Sim sim, decimal amount);
string Label { get; }
}
public sealed class PrepaidState : IPlanState
{
private decimal _balance;
public string Label => "Prepaid";
public void MakeCall(Sim sim, int minutes)
{
if (_balance < minutes) throw new InvalidOperationException("Recharge first");
_balance -= minutes;
}
public void Payment(Sim sim, decimal amount) => _balance += amount;
}
public sealed class PostpaidState : IPlanState
{
private decimal _bill;
private const decimal CreditLimit = 2000m;
public string Label => "Postpaid";
public void MakeCall(Sim sim, int minutes)
{
_bill += minutes * 1.2m;
if (_bill > CreditLimit) sim.TransitionTo(new SuspendedState(_bill));
}
public void Payment(Sim sim, decimal amount) => _bill = Math.Max(0, _bill - amount);
}
public class Sim
{
public string Number { get; }
private IPlanState _state = new PrepaidState();
public Sim(string number) => Number = number;
public void TransitionTo(IPlanState next) => _state = next;
public void ConvertToPostpaid() => TransitionTo(new PostpaidState());
public void MakeCall(int minutes) => _state.MakeCall(this, minutes);
public void Payment(decimal amount) => _state.Payment(this, amount);
}C#-এ কিছু specific জিনিস জেনে রাখো:
- Plain enum এই কাজ করতে পারবে না।
enum PlanType { Prepaid, Postpaid }আর switch expression মানে আগের ছবি ফিরে আসবে: behaviour type-এর বাইরে থাকবে, নতুন plan মানে প্রতিটা switch আবার ঘাঁটতে হবে। Enum সেই no-behaviour case-এর জন্য Class refactoring-এ। - Smart enum কাছে আসে কিন্তু থামে। Ardalis-style smart enum per-value behaviour রাখতে পারে, যেটা stateless strategy-like variation-এর জন্য ভালো। কিন্তু প্রতিটা plan-এর নিজের mutable data (
_balance,_bill) আর lifecycle transition দরকার হলে, full state class-ই সৎ ঘর। - Stateless strategy share করা যায়। যদি implementation-এ কোনো per-host data না থাকে — pure algorithm যেমন billing formula — তাহলে একটা instance বানাও আর সব জায়গায় reuse করো (dependency injection-এ singleton হিসেবেও চলে)। আমাদের plan state-এ per-SIM টাকা আছে, তাই প্রতিটা SIM নিজের instance পায়।
- Modern C# pattern matching (
sim.State switch { PrepaidState => ..., ... }) boundary-তে ঠিক আছে — serialization, display-এ। কিন্তু behavioural switch যদি state type-এর উপরে আবার উঁকি দেয়, refactoring leak করছে।
College corner: তৃতীয় bullet-টা আসলে Flyweight idea চুপচাপ Strategy-র সাথে মিলছে। Stateless strategy object pure behaviour — কোনো field নেই, তাই একটা shared instance সব host সুরক্ষিতভাবে ব্যবহার করতে পারে, thread-এর পারেও। DI container strategy-কে singleton হিসেবে register করে এই কারণেই। যেই মুহূর্তে strategy বা state-এ per-host mutable field আসে (আমাদের _balance), sharing একটা bug: দুটো SIM একটাই balance share করবে। Rule of thumb: stateless মানে একটা instance share করো; stateful মানে প্রতিটা host বা প্রতিটা transition-এ fresh instance।
এই refactoring আসলে দুটো classic Gang of Four pattern-এর দরজা। Swappable object যখন lifecycle model করে rules সহ, তখন তুমি State pattern বানিয়েছো; যখন interchangeable algorithm model করে client দ্বারা বেছে দেওয়া, তখন Strategy। দুটো post পরের বার পড়ো — প্রতিটা diagram চেনা মনে হবে, কারণ তুমি নিজের হাতে structure টা বানিয়েছো।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Runtime-এ behaviour বদলায় একটা reference swap করে — subclass দিয়ে impossible | Extra indirection আর ছোট object graph — type কখনো না বদলালে overkill (subclass ব্যবহার করো) |
| Behavioural switch collapse হয়ে one-line delegation হয় | Host transition manage করে; state-এর host pass দরকার হতে পারে, coupling বাড়ে |
| প্রতিটা state/strategy ছোট, cohesive, আর independently testable | Per-host data মানে প্রতিটা transition-এ allocation (stateless হলে share করা যায়) |
| Transition rule explicit, named, আর এক জায়গায় enforceable | অনেক ছোট class-এ ছড়িয়ে behaviour trivial variation-এ scattered মনে হতে পারে |
| State/strategy add করা purely additive — existing code untouched | Transition-এ data cross করলে (leftover balance) explicit, tested handling লাগে |
| State machine আর pluggable algorithm naturally express করে | Persistence-এ stored label load-এ সঠিক object-এ map করতে হবে |
কোন code smell ঠিক করে?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Switch Statements | Mutable code-এ বারবার behavioural switch delegation-এ গলে যায় |
| Primitive Obsession | Mutable int/string code real, swappable object হয়ে যায় |
| Temporary field | শুধু একটা mode-এ meaningful field (balance, monthlyBill) সেই state-এ চলে যায় |
| Shotgun surgery | নতুন state/strategy মানে একটা নতুন class, প্রতিটা switch edit না |
| Combinatorial subclass explosion | দুটো independent varying dimension N cross M subclass না হয়ে দুটো composed object হয় |
পুরো ব্যাপারটা এক ছবিতে
Quick revision box
+----------------------------------------------------------------+
| REPLACE TYPE CODE WITH STATE/STRATEGY - REVISION CARD |
+----------------------------------------------------------------+
| Problem : type code DRIVES behaviour AND CHANGES at runtime |
| -> switches everywhere + subclasses cannot help |
| (an object's class is fixed at birth) |
| Solution : interface for the varying behaviour, |
| one implementation per code, |
| host holds CURRENT one and DELEGATES, |
| transition = swap the reference |
| Result : same identity, new rules — at runtime |
| |
| STATE : lifecycle + transition rules (Prepaid->Suspended) |
| STRATEGY : interchangeable algorithm picked from outside |
| |
| WHICH OF THE THREE? |
| no behaviour varies -> CLASS / ENUM |
| behaviour varies, type fixed -> SUBCLASSES |
| behaviour varies + type changes-> STATE/STRATEGY (this one) |
+----------------------------------------------------------------+Practice করো নিজে
তোমার পালা। একটা food delivery app order track করছে mutable status code দিয়ে, আর switch ইতিমধ্যে বাড়ছে:
const PLACED = 0, COOKING = 1, ON_THE_WAY = 2, DELIVERED = 3;
class Order {
status = PLACED; // changes at runtime!
cancel(): string {
switch (this.status) {
case PLACED: return "Cancelled. Full refund.";
case COOKING: return "Cancelled. 50% refund.";
case ON_THE_WAY: return "Cannot cancel now.";
case DELIVERED: return "Cannot cancel. Order completed.";
default: throw new Error("Unknown status");
}
}
trackingMessage(): string {
switch (this.status) { // the same ladder again
case PLACED: return "Restaurant is confirming your order.";
case COOKING: return "Your food is being prepared.";
case ON_THE_WAY: return "Rider is on the way!";
case DELIVERED: return "Delivered. Enjoy your meal!";
default: throw new Error("Unknown status");
}
}
}এই ধাপগুলো follow করো:
- দুটো প্রশ্ন করো: behaviour vary করে (হ্যাঁ — cancel আর tracking প্রতিটা status-এ আলাদা) আর status same live order-এ বদলায় (হ্যাঁ)। Table-এর bottom row — State/Strategy। কিন্তু intent-এ এটা State না Strategy? Code করার আগে decide করো।
- একটা
OrderStateinterface বানাওcancel(order),trackingMessage()সহ, আর প্রতিটা status-এর জন্য একটা implementation:PlacedState,CookingState,OnTheWayState,DeliveredState। Order-এ একটাstatefield আরtransitionTo(next)method দাও। একটা একটা করে switching method state-এ move করো, host থেকে delegate করো। Move-এর মাঝে compile আর test করো — warning-এর six safe step আর stale-code trap মনে রেখো।- Lifecycle real করো:
confirmCooking(),pickUp(), আরdeliver()transition add করো, আর একটা rule enforce করো state-এর ভেতরে — যেমনDeliveredStateআর কোনো transition করলে throw করবে। - এবার নতুন requirement: "Returned" status, যেখানে cancel বলে "Refund processing" আর tracking বলে "Pickup rider assigned"। Prove করো তুমি শুধু একটা class add করেছো আর একটা transition — কোনো existing method edit না।
- Bonus: যদি app-টা তিনটা tip calculation method দিতো (percentage, flat, round-up) যেটা user বেছে নেয় — সেটা State না Strategy হতো? এক বাক্যে, intent-এর পার্থক্য ব্যবহার করে।
প্রশ্ন ১-এর উত্তর যদি হয় "State — কারণ order-এর lifecycle আছে legal transition সহ", আর প্রশ্ন ৬-এর উত্তর যদি হয় "Strategy — কারণ tip method interchangeable algorithm, কোনো progression নেই" — তাহলে তুমি শুধু একটা refactoring না, একটা sitting-এ দুটো design pattern আয়ত্ত করেছো। জামাল চাচার সাইনবোর্ডের নম্বর বদলায়নি — আর তুমি এখন জানো exactly কীভাবে এমন software বানাতে হয়।
সচরাচর জিজ্ঞাসা
- Runtime-এ type বদলালে subclasses কেন কাজ করে না?
- কারণ কোনো object-এর class একবার তৈরি হলে সেটা আর বদলানো যায় না — কোনো mainstream language-এ এটা সম্ভব না। একটা PrepaidSim object কখনো PostpaidSim হতে পারবে না। State/Strategy এই সমস্যা সমাধান করে একটা আলাদা collaborator object রেখে, যেটা host যেকোনো সময় swap করতে পারে — শুধু একটা assignment দিয়ে।
- State pattern আর Strategy pattern-এর পার্থক্য কী?
- structure একই — host একটা interface-এর পেছনে swappable object-এ কাজ delegate করে। কিন্তু intent আলাদা। State মানে একটা lifecycle — object নিজেই সিদ্ধান্ত নেয় কখন কোন state-এ যাবে, transition-এর rules থাকে। Strategy মানে বাইরে থেকে বেছে দেওয়া algorithm — এখানে কোনো progression নেই, এক strategy থেকে আরেকটায় কোনো flow নেই।
- Plan বদলালে কি SIM-এর data হারিয়ে যায়?
- না, আর এটাই composition-এর সৌন্দর্য। Host object — মানে SIM, তার number আর identity — একদম অপরিবর্তিত থাকে। শুধু ভেতরের ছোট plan object টা replace হয়। যা বদলায় না সেটা host-এ থাকে; যা বদলায় সেটাই swap হয়।
- সাধারণ ক্ষেত্রে কি এই refactoring বেশি heavy না?
- হ্যাঁ, হতে পারে। Runtime-এ যদি type কখনো না বদলায়, তাহলে plain subclasses সহজ — কোনো delegation layer দরকার নেই। আর যদি code-এর পেছনে কোনো behaviour-ই না থাকে, তাহলে value class বা enum-ই যথেষ্ট। State/Strategy তখনই ব্যবহার করো যখন behaviour vary করে আর type live object-এ বদলায়।
- Refactoring-এর পরে state transition কোথায় থাকে?
- দুটো সাধারণ জায়গা। Host নিজে named transition method রাখতে পারে যেমন convertToPostpaid — এটা সহজ। অথবা full State pattern-এ, প্রতিটা state object নিজেই পরের state ঠিক করে, যেটা strict state machine-এর জন্য ভালো যেখানে শুধু নির্দিষ্ট transition legal।
আরো দেখো
সম্পর্কিত পাঠ
State Pattern: একটা object-এর মেজাজ বদলানোর গল্প
State design pattern শেখো সিলিং ফ্যানের রেগুলেটরের গল্প দিয়ে। সহজ TypeScript আর C# code, state diagram, আর real software-এর উদাহরণ সহ।
Strategy Pattern: সাইকেল, বাস, নাকি অটো — তুমিই ঠিক করো
Strategy design pattern শেখো একটা সহজ স্কুলে যাওয়ার গল্পের মাধ্যমে — TypeScript আর C# কোড, runtime swapping, বাস্তব উদাহরণ, আর প্র্যাকটিস exercise সহ।
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 আর পরিষ্কার করা যায়।