Replace Conditional with Polymorphism: প্রতিটি ধরনকে তার নিজের ডেস্ক দাও
Replace Conditional with Polymorphism রিফ্যাক্টরিং শেখো স্কুল রিসেপশনের গল্প দিয়ে — বারবার আসা type switch কীভাবে subclass-এ পরিণত হয়, TypeScript ও C#-এ factory কীভাবে কাজ করে, আর কখন সাধারণ switch রেখে দেওয়াই ভালো সেটাও বুঝবে।
একটা রুলবুক বনাম পাঁচটা ডেস্ক
ধরো, সোমবার সকালে তুমি একটা বড় স্কুলের রিসেপশনে ঢুকলে। সব ধরনের মানুষ আসছে: ফি জানতে আসা অভিভাবক, নতুন ভর্তি নিতে আসা ছাত্র, বই বিক্রেতা, শিক্ষা বোর্ডের পরিদর্শক, আর ট্রান্সফার সার্টিফিকেট নিতে আসা পুরোনো ছাত্র।
পুরোনো ব্যবস্থায়, একজন রিসেপশনিস্ট — নাসরিন আপু — একটা বিশাল রুলবুক নিয়ে সবাইকে সামলান। কেউ আসে; তিনি জিজ্ঞেস করেন, "তুমি কোন ধরনের?" তারপর পাতা উল্টান: অভিভাবক হলে ফি নিয়মের জন্য ১২ নম্বর পাতায় যাও... বিক্রেতা হলে ক্রয় পদ্ধতির জন্য ৪৭ নম্বর পাতায়... পরিদর্শক হলে ৩ নম্বর পাতায়, তাৎক্ষণিক প্রধান শিক্ষককে জানাও... প্রতিটা কাজেই এই একই লুকআপ। পাস ইস্যু করতে হবে? ভিজিটরের ধরন দেখে পাতা উল্টাও। কে কার সাথে দেখা করতে পারবে? আবার ভিজিটরের ধরন দেখে পাতা উল্টাও। একই "তুমি কোন ধরনের?" প্রশ্ন বারবার।
এখন স্কুল বড় হলো। নতুন এক ধরনের ভিজিটর এলো — ধরো, প্রাক্তন ছাত্র দাতা। কাউকে রুলবুকের প্রতিটা পাতা খুঁজে নতুন case যোগ করতে হবে। গত সেমিস্টারে কেরানি রুবেল ৪৭ নম্বর পাতা মিস করেছিল। দুই সপ্তাহ ধরে প্রতিটা দাতাকে ভেন্ডার রেজিস্টারে সই করতে বলা হয়েছিল আর ভেন্ডার পাস ফি দিতে হয়েছিল। একজন দাতা রাগ করে চলে গেলেন। নাসরিন আপু ক্লান্ত, আর রুলবুকে চায়ের দাগ।
নতুন প্রধান শিক্ষক মিসেস ফাতেমা রিসেপশন নতুন করে সাজালেন। এখন পাঁচটা ডেস্ক, প্রতিটা ভিজিটরের ধরনের জন্য একটা: অভিভাবক ডেস্ক, ভর্তি ডেস্ক, ভেন্ডার ডেস্ক, পরিদর্শন ডেস্ক, প্রাক্তন ছাত্র ডেস্ক। প্রতিটা ডেস্কে একজন বিশেষজ্ঞ — সুমাইয়া ভর্তি ডেস্ক চালায়, বুড়ো জামাল ভাই ভেন্ডার ডেস্ক। আর প্রতিটা ডেস্ক তার নিজের কাজ সম্পূর্ণভাবে জানে: নিজের পাস নিয়ম, নিজের পদ্ধতি, নিজের রেজিস্টার। দরজায় একজন গার্ড ঠিক একটাই কাজ করে: ভিজিটরের দিকে একবার তাকায়, সঠিক ডেস্ক দেখিয়ে দেয়। এরপর আর কেউ কখনো জিজ্ঞেস করে না "তুমি কোন ধরনের?" নতুন দাতা ডেস্ক যোগ করতে বাকি কিছু বদলাতে হয় না — শুধু একটা নতুন টেবিল রাখো।
এটাই আজকের রিফ্যাক্টরিং। বিশাল রুলবুক হলো switch (visitorType) ভরা একটা method। ডেস্কগুলো হলো subclass। দরজার গার্ড হলো factory। Replace Conditional with Polymorphism একটা type-based conditional-এর প্রতিটা branch-কে তার নিজের class-এ সরিয়ে দেয়। তোমার switch-এর বদলে ভাষার dynamic dispatch behaviour বেছে নেয়।
Replace Conditional with Polymorphism কী?
এই রিফ্যাক্টরিং একটা নির্দিষ্ট pattern-কে target করে। একটা conditional যেটা type code-এর উপর branch করে — একটা string, enum, বা flag যেটা একটা ধরনের জিনিসের নাম বলে। প্রতিটা branch সেই ধরনের নিজস্ব behaviour হিসেব করে।
একটা মাত্র এই ধরনের switch সহনীয়। আসল সমস্যা, Fowler যেমন জোর দিয়ে বলেন, হলো একই switch বহুগুণ হয়ে যায়: ধরন অনুযায়ী যতো method পরিবর্তন হয় প্রতিটিতেই এটা আবার আসে। এটাই Switch Statements smell-এর পূর্ণ রূপ।
সমাধান হলো "ধরন জিজ্ঞেস করো, তারপর branch করো" কে "object-এর class-কেই branch হতে দাও" দিয়ে replace করা:
- প্রতিটা type-code value-র জন্য একটা subclass তৈরি করো।
ParentVisitor,VendorVisitor,InspectorVisitor— প্রতিটা ডেস্ক তৈরি হয়। - একটা factory set up করো যাতে type code তৈরির সময় ঠিক একবার সঠিক subclass-এ রূপান্তরিত হয়। (Caller সরাসরি base class construct করলে আগে Replace Constructor with Factory Method প্রয়োগ করো।)
- প্রতিটা switch, branch by branch, overriding method-এ সরাও।
case Parent:body হয়ে যায়ParentVisitor.issuePass()।case Vendor:body হয়ে যায়VendorVisitor.issuePass()। - Base method-কে abstract করো (বা সত্যিকারের shared behaviour-এর জন্য default রাখো) যখন প্রতিটা branch বাইরে চলে যায়।
- Switch-গুলো মুছে দাও। প্রতিটা call site হয়ে যায় একটা সহজ লাইন:
visitor.issuePass()।
এখানে গভীর ধারণাটার একটা নাম আছে: Tell, Don't Ask। পুরোনো কোড object-কে তার ধরন সম্পর্কে জিজ্ঞেস করে আর তার হয়ে সিদ্ধান্ত নেয় — নাসরিন আপু প্রতিটা ভিজিটরকে জেরা করছেন। নতুন কোড object-কে তার কাজ করতে বলে আর সে কীভাবে করবে সেটা জানে বলে বিশ্বাস রাখে — গার্ড একটা ডেস্কের দিকে দেখিয়ে দিচ্ছে যেটা আগে থেকেই সবকিছু জানে। সিদ্ধান্তটা যেখানে জ্ঞান আছে সেখানে চলে যায়।
এক লাইনে সারসংক্ষেপ: Replace Conditional with Polymorphism বারবার আসা type-switch-এর প্রতিটা branch-কে একটা subclass-এ overriding method হিসেবে সরিয়ে দেয়। Runtime object-এর class দেখে সঠিক behaviour বেছে নেয়। Switch-এর দাম একবার দেওয়া হয়, factory-তে — প্রতিটা call-এর সময় নয়।
কলেজ কর্নার: এই জাদুর পেছনের যন্ত্রের নাম dynamic dispatch (late binding-ও বলে)। তুমি যখন একটা base-class reference-এ visitor.welcome() call করো, compiler জানতে পারে না কোন body চলবে। সেটা runtime-এ object-এর actual class দেখে ঠিক হয়। C++, C#, আর Java-তে এটা সাধারণত vtable দিয়ে হয়: প্রতিটা class একটা hidden function pointer-এর table বহন করে, প্রতিটা virtual method-এর জন্য একটা slot। Call মানে হলো "এই object যে table বহন করছে তার slot 2 দিয়ে jump করো।" JavaScript আর Python একই কাজ করে prototype chain বা MRO (method resolution order) হেঁটে। খরচ হলো একটা indirect jump — মাত্র কয়েক nanosecond। আর যা পাওয়া যায় সেটা বিশাল: behaviour-এর set open-ended হয়ে যায়। একটা switch compile time-এ বন্ধ থাকে। কিন্তু একটা vtable slot এমন class দিয়েও পূর্ণ হতে পারে যেটা caller লেখার সময় ছিলই না। এই openness হলো Open/Closed Principle-এর মূল কথা: extension-এর জন্য open (নতুন subclass), modification-এর জন্য closed (বিদ্যমান call site-এ কোনো edit নেই)।
কখন এটা দরকার হয়?
শক্তিশালী লক্ষণ যে রুলবুক এখন ডেস্ক হওয়া উচিত:
- একই switch অনেক method-এ আছে।
switch (type)calculateFee()-তে, আবারpassValidity()-তে, আবারwelcomeMessage()-তে। আজ তিনটা কপি, আগামী বছর পাঁচটা। এটাই পাঠ্যপুস্তকের Switch Statements smell। - একটা ধরন যোগ করা মানে treasure hunt। প্রতিটা নতুন visitor type মানে প্রতিটা switch খুঁজে বের করে edit করতে হবে। একটা মিস করো, একটা bug চলে যায় — রুবেলের ভেন্ডার রেজিস্টারের ঘটনা মনে আছে? Subclass-এ, compiler build করতে অস্বীকার করে যতক্ষণ না নতুন class প্রতিটা abstract method implement করে। সম্পূর্ণতা জোর করে নিশ্চিত করা হয়।
- একটা ধরন বোঝার জন্য grep করতে হয়। "একজন পরিদর্শক কীভাবে কাজ করে?" এর অর্থ এখন file জুড়ে
case Inspectorখোঁজা। রিফ্যাক্টরিং-এর পরে, তুমিInspectorVisitor.tsখুললেই সব উত্তর এক স্ক্রিনে পাবে — এক ডেস্ক, এক রেজিস্টার। - Branch-গুলোতে সত্যিকারের behaviour আছে, শুধু label নয়। প্রতিটা case আলাদা কিছু হিসেব করে — ফি, নিয়ম, বার্তা। কোড শুধু label হলে, কোনো behaviour না থাকলে, একটা সাধারণ enum বা Replace Type Code with Class যথেষ্ট।
- ধরনের set বাড়বে বলে আশা করা হচ্ছে। বৃদ্ধিই extension-এর জন্য open shape-কে কাজে লাগায়: নতুন ধরন = নতুন class, বিদ্যমান code-এ শূন্য edit।
আর সৎ বিপরীত লক্ষণ:
- একটা switch, এক জায়গায়, স্থির case? Switch রেখে দাও। দশ লাইনের একটা conditional মুছতে পাঁচটা class-এর hierarchy হলো ceremony, design নয়।
- Stable ধরন না বরং numeric range বা data value-র উপর variation (tax slab, discount tier)? Subclass-এর চেয়ে lookup table বা Strategy object বেশি মানানসই।
- ধরন runtime-এ বদলায় (prepaid থেকে postpaid হয়)? Object তার class বদলাতে পারে না। Composition ব্যবহার করো — State আর Strategy দেখো, আর Replace Type Code with State/Strategy পোস্ট।
মিসেস ফাতেমা ডেস্ক পরিকল্পনা অনুমোদন করার আগে অফিসকে গণনা করতে বললেন — দৈনন্দিন রুটিনে type-question কতোবার আসে। ফলাফল তাকে বোঝাল। প্রায় প্রতিটা কাজেই একই প্রশ্ন বারবার হচ্ছিল:
দুটো ডিজাইন পাশাপাশি রাখলে:
| দৈনন্দিন বাস্তবতা | রুলবুক (switch) | ডেস্ক (subclass) |
|---|---|---|
| "একজন পরিদর্শক কীভাবে কাজ করে?" | প্রতিটা case Inspector পাতা grep করো | একটা file খোলো, এক স্ক্রিনে পড়ো |
| দাতা ধরন যোগ করা | প্রতিটা switch edit করো; একটা মিস, একটা bug | একটা class যোগ করো আর একটা factory line |
| "তুমি কোন ধরনের?" কে জিজ্ঞেস করে | প্রতিটা method, প্রতিটা call-এ | দরজার গার্ড, ঠিক একবার |
| একটা case ভুলে যাওয়া | runtime-এ গ্রাহক খুঁজে পায় | build time-এ compiler ধরে ফেলে |
| একটা ধরন test করা | type code set করো, প্রতিটা switch hit করো | subclass instantiate করো, একা test করো |
Subclass-এর দিকে হাত বাড়ানোর আগে তোমার conditional-কে এই map-এ রাখো:
আগে ও পরে এক নজরে
একটা সংক্ষিপ্ত version দিয়ে শুরু করি। স্কুল বিভিন্ন visitor pass ফি নেয় আর ভিন্ন welcome line print করে:
// BEFORE: the rulebook — every method re-asks "what type are you?"
type VisitorType = "parent" | "vendor" | "inspector";
class Visitor {
constructor(public type: VisitorType, public name: string) {}
passFee(): number {
switch (this.type) {
case "parent": return 0;
case "vendor": return 100;
case "inspector": return 0;
}
}
welcome(): string {
switch (this.type) { // the SAME switch, again
case "parent": return `Welcome ${this.name}, please wait in the lounge.`;
case "vendor": return `${this.name}, please sign the vendor register.`;
case "inspector": return `Welcome ${this.name}. Informing the principal.`;
}
}
}// AFTER: desks — each class knows its own job; one factory routes
abstract class Visitor {
constructor(public name: string) {}
abstract passFee(): number;
abstract welcome(): string;
static create(type: VisitorType, name: string): Visitor {
switch (type) { // the ONLY surviving switch
case "parent": return new ParentVisitor(name);
case "vendor": return new VendorVisitor(name);
case "inspector": return new InspectorVisitor(name);
}
}
}
class ParentVisitor extends Visitor {
passFee() { return 0; }
welcome() { return `Welcome ${this.name}, please wait in the lounge.`; }
}
class VendorVisitor extends Visitor {
passFee() { return 100; }
welcome() { return `${this.name}, please sign the vendor register.`; }
}
class InspectorVisitor extends Visitor {
passFee() { return 0; }
welcome() { return `Welcome ${this.name}. Informing the principal.`; }
}Call site switch থেকে একটা বাক্যে সংকুচিত হয়: visitor.welcome()। একজন vendor যা করে সব এখন VendorVisitor-এ — এক ডেস্ক, এক file, এক স্ক্রিন।
Runtime কথোপকথন "একবার route করো, সবসময় dispatch করো" ছন্দটা স্পষ্ট করে দেখায়:
ধাপে ধাপে, নিরাপদ পথে
এই রিফ্যাক্টরিং-এর বিপদ হলো একসাথে অনেক বেশি সরানো। Fowler-এর mechanics এক সময়ে একটা method-এর একটা branch সরায়, মাঝখানে সবকিছু compile হওয়া আর pass করার সাথে।
ধাপ ১: factory-র মাধ্যমে creation funnel করো। কোনো behaviour সরানোর আগে নিশ্চিত করো কোনো caller সরাসরি new Visitor("vendor", ...) লেখে না। Visitor.create(...) যোগ করো আর caller-দের convert করো। Test এখনও pass হচ্ছে — অন্য কিছু বদলায়নি।
ধাপ ২: খালি subclass তৈরি করো। ParentVisitor, VendorVisitor, InspectorVisitor, সব খালি, সব Visitor extend করে। Factory-কে তাদের দিকে point করো। Behaviour এখনও ১০০% base class switch-এ। Subclass-গুলো এখন পর্যন্ত শুধু নাম-ট্যাগ। Test।
ধাপ ৩: একটা branch সরাও। একটা method বেছে নাও, ধরো passFee(), আর একটা ধরন, ধরো vendor। Subclass-এ override করো। বাকিদের জন্য fallback হিসেবে base switch রাখো:
// INTERMEDIATE: vendor's branch has moved; others still use the rulebook
class VendorVisitor extends Visitor {
override passFee(): number { return 100; } // moved!
}
class Visitor {
passFee(): number {
switch (this.type) {
case "parent": return 0;
// vendor case DELETED from here — the override handles it
case "inspector": return 0;
default: throw new Error(`No fee rule for ${this.type}`);
}
}
}এই অর্ধেক-সরানো অবস্থাটা সম্পূর্ণ নিরাপদ। Vendor-রা override hit করে, বাকি সবাই পুরোনো switch-এ fall through করে। জামাল ভাইয়ের ডেস্ক খুলেছে, কিন্তু নাসরিন আপু এখনও রুলবুক থেকে অভিভাবক ও পরিদর্শকদের সেবা দিচ্ছেন। সংস্কারের সময় রিসেপশন বন্ধ হয় না।
ধাপ ৪: Test চালাও। তারপর একই method-এর পরবর্তী branch সরাও। Test। তারপর পরবর্তী।
ধাপ ৫: Base method-কে abstract করো। যখন প্রতিটা ধরনের override আছে, base switch হলো dead code। এটাকে abstract passFee(): number; দিয়ে replace করো। এখন compiler নিশ্চিত করে প্রতিটা বর্তমান আর ভবিষ্যৎ subclass এটা implement করে।
ধাপ ৬: প্রতিটা switching method-এর জন্য পুনরাবৃত্তি করো। welcome(), তারপর পরবর্তী, এক সময়ে একটা। Factory-র বাইরে কোনো behavioural switch না থাকলে, type-code field নিজেই মুছে দেওয়ার কথা ভাবো। Class হলো এখন type।
Migration নিজেই একটা ছোট state machine। এর প্রতিটা state-ই shippable:
প্রতিটা ধাপের পরে কোড releasable রাখো — এটাই পুরো কারিগরি। মধ্যবর্তী অবস্থা (কিছু branch সরানো, base switch এখনও fallback হিসেবে জীবিত) অবশ্যই সব test pass করতে হবে। কারণ সত্যিকারের রিফ্যাক্টরিং অন্য কাজের পাশাপাশি হয়, আর মাঝপথেও ship করতে হতে পারে। Branch logic "উন্নত" করার লোভও সামলাও সরানোর সময়। আগে সরাও, অভিন্ন behaviour যাচাই করো, পরে আলাদা ধাপে উন্নত করো। Move-এর সাথে edit মেশালে নীরব behaviour change production-এ ঢুকে পড়ে।
একটা বড় বাস্তব উদাহরণ
ধরো স্কুলের accounts office তিনটা student category-র জন্য মাসিক ফি আর transport charge হিসেব করে: day scholar, hosteller, আর scholarship student। আজ দুটো calculation একটা string-এ switch করে:
// BEFORE: two methods, the same three-way switch in each
class Student {
constructor(
public category: "day-scholar" | "hosteller" | "scholarship",
public distanceKm: number,
) {}
monthlyFee(): number {
switch (this.category) {
case "day-scholar": return 2500;
case "hosteller": return 2500 + 4000; // fee + mess
case "scholarship": return 0;
default: throw new Error("Unknown category");
}
}
transportCharge(): number {
switch (this.category) {
case "day-scholar": return this.distanceKm * 60;
case "hosteller": return 0; // lives on campus
case "scholarship": return Math.min(this.distanceKm * 60, 500); // capped
default: throw new Error("Unknown category");
}
}
}রিফ্যাক্টরিং-এর পরে, প্রতিটা category একটা ডেস্কে পরিণত হয় যেটা তার নিজের অঙ্ক জানে:
// AFTER
abstract class Student {
constructor(public distanceKm: number) {}
abstract monthlyFee(): number;
abstract transportCharge(): number;
// one place in the whole system that maps code -> class
static create(category: string, distanceKm: number): Student {
switch (category) {
case "day-scholar": return new DayScholar(distanceKm);
case "hosteller": return new Hosteller(distanceKm);
case "scholarship": return new ScholarshipStudent(distanceKm);
default: throw new Error(`Unknown category: ${category}`);
}
}
}
class DayScholar extends Student {
monthlyFee() { return 2500; }
transportCharge() { return this.distanceKm * 60; }
}
class Hosteller extends Student {
monthlyFee() { return 2500 + 4000; }
transportCharge() { return 0; }
}
class ScholarshipStudent extends Student {
monthlyFee() { return 0; }
transportCharge() { return Math.min(this.distanceKm * 60, 500); }
}এখন growth test চালাও। নতুন একটা category এলো: NCC cadet-রা পুরো ফি দেয় কিন্তু transport বিনামূল্যে পায়। আগের দুনিয়ায় দুটো switch edit করতে হতো (আর গত মাসে কেউ যোগ করা তৃতীয়টাও যেটা তুমি জানো না)। পরের দুনিয়ায় একটা নতুন class লেখো:
class NccCadet extends Student {
monthlyFee() { return 2500; }
transportCharge() { return 0; }
}
// plus one new line in the factory — and nothing else changestransportCharge implement করতে ভুলে গেলে code compile-ই হবে না। "switch-এর একটায় একটা case ভুলে গেছি" bug class চিরতরে বিদায় নিয়েছে — compiler সেই চিন্তা নিজের কাঁধে তুলে নিয়েছে। এই open-for-extension shape, যেখানে নতুন behaviour যোগ করে হয় edit করে নয়, এটাই Open/Closed Principle-এর বাস্তব রূপ।
দেখো দুটো ডিজাইনের মধ্যে বৃদ্ধির খরচ কীভাবে আলাদা হয়। Switch-এ, প্রতিটা নতুন category প্রতিটা switching method-এ edit করতে হয়। Subclass-এ, প্রতিটা নতুন ধরনের খরচ সবসময় একই — একটা class, একটা factory line:
উঠতে থাকা লাইনটা রুলবুক: প্রতিটা switching method-এ একটা edit, চিরকাল। সমতল লাইনটা ডেস্ক ডিজাইন: একটা নতুন class আর একটা factory line, যতই behaviour থাকুক। দুটো লাইন প্রায় সাথে সাথেই ছেদ করে। এজন্যই Fowler switch-এর পুনরাবৃত্তিকে trigger হিসেবে দেখেন, শুধু একটার অস্তিত্বকে নয়।
C#-এ একই রিফ্যাক্টরিং
C# abstract class, expression-bodied member, আর factory-র জন্য switch expression দিয়ে pattern-টাকে আরও সুন্দর করে:
public abstract class Student
{
public double DistanceKm { get; }
protected Student(double distanceKm) => DistanceKm = distanceKm;
public abstract decimal MonthlyFee();
public abstract decimal TransportCharge();
public static Student Create(string category, double km) => category switch
{
"day-scholar" => new DayScholar(km),
"hosteller" => new Hosteller(km),
"scholarship" => new ScholarshipStudent(km),
_ => throw new ArgumentOutOfRangeException(nameof(category))
};
}
public sealed class DayScholar : Student
{
public DayScholar(double km) : base(km) { }
public override decimal MonthlyFee() => 2500m;
public override decimal TransportCharge() => (decimal)DistanceKm * 60m;
}
public sealed class Hosteller : Student
{
public Hosteller(double km) : base(km) { }
public override decimal MonthlyFee() => 2500m + 4000m;
public override decimal TransportCharge() => 0m;
}
public sealed class ScholarshipStudent : Student
{
public ScholarshipStudent(double km) : base(km) { }
public override decimal MonthlyFee() => 0m;
public override decimal TransportCharge() =>
Math.Min((decimal)DistanceKm * 60m, 500m);
}C#-এ কিছু বিশেষ note রাখার মতো:
- Leaf-গুলো
sealedকরো। Concrete subclass-গুলোsealedmark করলে reader জানে hierarchy design অনুযায়ী এক স্তর গভীর। Runtime-ও call devirtualize করতে পারে। - Switch expression হলো polymorphism নয়। আধুনিক pattern matching চমৎকার কাজ করে boundary-তে — serialization, display formatting। কিন্তু তোমার নিজের subclass-এর উপর behavioural switch core logic-এ ফিরে আসতে থাকলে, রুলবুক চুপচাপ আবার বাড়ছে।
- Interface-ও কাজ করে। ধরনগুলো কোনো data বা implementation share না করলে, তিনটা implementing class সহ
interface IStudentCategoryabstract base-এর চেয়ে হালকা। Shared state থাকলে (যেমনDistanceKm) abstract class নাও; শুধু contract থাকলে interface নাও। - Dependency injection factory-কে replace করতে পারে। ASP.NET application-এ code থেকে class-এ mapping প্রায়ই DI registration বা keyed-service lookup-এ থাকে — একই single-door ধারণা, framework-managed।
কলেজ কর্নার: machine level-এ virtual call নিয়ে পড়লে তুমি সেই trade-off-গুলো দেখবে যা engine আর compiler author-রা নিয়ে ঘুম হারায়। একটা vtable-এর মাধ্যমে virtual call inlining আর branch prediction-কে এমনভাবে পরাজিত করতে পারে যা কখনো কখনো একটা hot switch করে না। তাই JIT compiler devirtualization করে — প্রমাণ করে একটা call site-এ সবসময় শুধু একটা class আসে, আর indirect jump-কে একটা direct, inlinable একটা দিয়ে replace করে। C#-এর sealed আর JVM-এর class-hierarchy analysis উভয়ই এটাকে feed করে। Coursework আর interview-এর জন্য practical takeaway: switch আর polymorphism-এর মধ্যে maintainability-র ভিত্তিতে বেছে নাও। সাধারণ application scale-এ performance পার্থক্য noise, আর JIT সেটাও মুছে দিতে ব্যস্ত।
IDE support
কোনো IDE পুরো রূপান্তর এক click-এ করে না — এটা একটা composite রিফ্যাক্টরিং। কিন্তু building block-গুলো ভালোভাবে automated:
- JetBrains Rider / IntelliJ IDEA / ReSharper: Push Members Down রিফ্যাক্টরিং base থেকে subclass-এ একটা method সরায় (per-subclass কপি তৈরি করে যেগুলো তুমি এক branch-এ ছাঁটাই করো)। Extract Superclass / Extract Interface hierarchy তৈরি করে। কিছু ভাষায় inspection একই value-র উপর বারবার switch flag করে আর Replace 'switch' with polymorphic calls style intention offer করে।
- Visual Studio:
Ctrl+.quick action টুকরোগুলো cover করে — Extract base class, Pull members up, Generate overrides (যেটা এক ধাক্কায় প্রতিটা abstract member-এর জন্যoverridestub লেখে — ধাপ ৫-এর জন্য চমৎকার), আর টিকে থাকা factory-র জন্য Convert switch statement to switch expression। - VS Code C#/TypeScript language service-সহ extract-interface আর implement-abstract-class code action offer করে যেটা subclass skeleton তৈরি করে।
বিচারের calls — কোন branch-গুলো ধরন, abstract surface কী হওয়া উচিত — মানুষের কাজ থাকে। IDE শুধু প্রতিটা mechanical ধাপকে typo-মুক্ত করে। মিসেস ফাতেমা কোন ডেস্ক তৈরি হবে সেটা ঠিক করেছিলেন; কারপেন্টার শুধু সেগুলো সোজা করে তৈরি করেছিল।
সুবিধা ও ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| প্রতিটা ধরনের সম্পূর্ণ behaviour একটা class-এ — পড়া ও unit হিসেবে test করা সহজ | একটা স্থির switch-এর জন্য class hierarchy হলো over-engineering |
| একটা ধরন যোগ করা = একটা নতুন subclass; compiler প্রতিটা abstract method enforce করে | বেশি type আর file; indirection "এটা কোথায় চলে?" এক hop কঠিন করে |
| বারবার switch একটা factory mapping-এ সংকুচিত হয় — type code-এর জন্য single door | ভুল axis of variation (numeric range, runtime-changing kind) subclass-কে awkward করে |
| "switch-এর একটায় একটা case ভুলে গেছি" bug class স্থায়ীভাবে শেষ | Hierarchy rigid: দুটো স্বাধীন varying dimension N×M subclass-এ explode করে — তখন composition পছন্দনীয় |
| Extension-এর জন্য open: বৃদ্ধি code যোগ করে হয়, edit করে নয় | Shared behaviour সাবধানে base-এ manage করতে হবে; sibling-এ duplication ঢুকতে পারে |
| State আর Strategy pattern-এর natural gateway | অর্ধেক-শেষ migration (কিছু switch পেছনে রেখে) হলো দুটো pure state-এর চেয়েই খারাপ |
এটা কোন smell ঠিক করে?
| Smell | এই রিফ্যাক্টরিং কীভাবে সাহায্য করে |
|---|---|
| Switch Statements | নির্ধারক সমাধান — বারবার আসা type-switch dynamic dispatch-এ গলে যায় |
| Shotgun Surgery | একটা ধরন যোগ করার মানে আর প্রতিটা switching method edit করা নয়; একটা নতুন class |
| Primitive Obsession | string/enum type code logic চালানো বন্ধ করে; real class behaviour বহন করে |
| Long Method | Multi-branch switch-ভরা method একটা delegating line-এ সংকুচিত হয় |
| Duplicated Code | method জুড়ে copy-paste হওয়া same case-ladder ঠিক একবারই লেখা হয় — class structure হিসেবে |
একটা revision picture-এ পুরো বিষয়টা:
দ্রুত revision box
+----------------------------------------------------------------+
| REPLACE CONDITIONAL WITH POLYMORPHISM - REVISION CARD |
+----------------------------------------------------------------+
| Problem : same switch on a TYPE CODE repeated across methods |
| new kind => edit every switch (miss one = bug) |
| Solution : one SUBCLASS per kind; each branch becomes an |
| OVERRIDE; a FACTORY maps code -> class ONCE; |
| call sites become plain method calls |
| Result : "ask kind, then branch" -> "tell object, it knows"|
| |
| MECHANICS: factory -> empty subclasses -> move ONE branch |
| -> test -> repeat -> make base method abstract |
| THE ONE SURVIVING SWITCH lives in the factory. That is fine. |
| SKIP IT WHEN: one switch, one place, stable cases |
| WRONG AXIS : kind changes at runtime -> State/Strategy instead |
+----------------------------------------------------------------+অনুশীলন
ধরো একটা courier কোম্পানি তিনটা service type-এর জন্য delivery হিসেব করে। Switch ইতিমধ্যে দুটো method-এ copy-paste হয়ে গেছে:
class Shipment {
constructor(
public service: "standard" | "express" | "same-day",
public weightKg: number,
) {}
price(): number {
switch (this.service) {
case "standard": return 40 + this.weightKg * 10;
case "express": return 90 + this.weightKg * 18;
case "same-day": return 250 + this.weightKg * 30;
default: throw new Error("Unknown service");
}
}
promiseDays(): number {
switch (this.service) { // same ladder again
case "standard": return 5;
case "express": return 2;
case "same-day": return 0;
default: throw new Error("Unknown service");
}
}
}ধাপে ধাপে refactor করো:
- একটা static factory
Shipment.create(service, weightKg)যোগ করো আর সব caller convert করো। Test সবুজ? - তিনটা খালি subclass তৈরি করো —
StandardShipment,ExpressShipment,SameDayShipment— আর factory-কে তাদের দিকে route করো। Behaviour অপরিবর্তিত; test সবুজ? price()এক branch করে সরাও: আগেExpressShipment-এ override করো, base switch থেকে শুধু সেই case মুছো, test করো, তারপর বাকিগুলো।- তিনটা override হলে, base-এ
price()abstract করো।promiseDays()-এর জন্য পুরো প্রক্রিয়া আবার করো। - এখন growth test: marketing "economy" launch করে — ৪০ টাকা + ৬ টাকা/কেজি, ৮ দিনের promise। প্রমাণ করো তুমি একটা class আর একটা factory line যোগ করেছো, আর
promiseDays()ভুলে গেলে compile-ই হতো না। - Bonus চিন্তা: কোম্পানি fragile-item handling যোগ করে যেটা service type থেকে স্বাধীনভাবে পরিবর্তন হয়। তুমি কি
FragileExpressShipment,FragileStandardShipment... তৈরি করবে, নাকি একটা আলাদা handling object compose করবে? একটা বাক্যে বলো, subclass explosion সম্পর্কে যা জানো সেটা ব্যবহার করে। - College bonus: ধাপ ৪-এর পরে
ExpressShipment-এর vtable কাগজে sketch করো। কোন slot-গুলো তার নিজের, আরShipmentreference-এর মাধ্যমেprice()call হলে runtime ঠিক কী করে?
প্রশ্ন ৬-এর উত্তর যদি "compose করো — দুটো স্বাধীন dimension দুটো object হওয়া উচিত, N×M subclass নয়" হয়, তুমি শুধু এই রিফ্যাক্টরিং নয় বরং এর সীমানাও বুঝেছো। এটাই গভীর শিক্ষা। মিসেস ফাতেমা তোমাকে front office-এর জন্য hire করতেন। শাবাশ।
সচরাচর জিজ্ঞাসা
- প্রতিটি switch statement কি খারাপ?
- না। একটা মাত্র switch যদি একটা মাত্র জায়গায় থাকে, ছোট ও স্থির কিছু case নিয়ে, সেটা একদম ঠিক আছে। সমস্যা তখন হয় যখন একই type code নিয়ে একই switch অনেক method-এ বারবার আসে, বা নতুন ধরন বাড়তেই থাকে। Polymorphism তার class hierarchy-র খরচ তখনই উসুল করে যখন conditional নকল হয় বা বাড়তে থাকে।
- রিফ্যাক্টরিং-এর পরে switch কোথায় যায়? পুরোপুরি মুছে যায়?
- একটা switch সাধারণত টিকে থাকে — ইচ্ছা করেই — একটা factory-র ভেতরে। Factory ঠিক একবার incoming type code (database থেকে আসা string, API থেকে আসা enum) কে সঠিক subclass-এ রূপান্তর করে। বাকি সব switch একটা সাধারণ method call-এ পরিণত হয়, যেটা dynamic dispatch নিজেই সামলে নেয়।
- 'Tell, Don't Ask' কী আর এটা এখানে কীভাবে সম্পর্কিত?
- রিফ্যাক্টরিং-এর আগে, কোড object-কে জিজ্ঞেস করে সে কোন ধরনের, তারপর নিজেই সিদ্ধান্ত নেয়: 'তুমি কি day-scholar? তাহলে X করো।' পরে কোড object-কে শুধু বলে তার কাজ করতে — visitor.handle() — আর object নিজেই জানে কীভাবে করতে হবে। সিদ্ধান্তটা যেখানে জ্ঞান আছে সেখানে সরে যায়। এটাই object-oriented design-এর মূল কথা।
- এটা Replace Type Code with Subclasses থেকে আলাদা কীভাবে?
- এরা একে অপরের সঙ্গী। Replace Type Code with Subclasses একটা type-code field থেকে class hierarchy তৈরি করে। Replace Conditional with Polymorphism তারপর প্রতিটি switching method-এর branch গুলো সেই subclass-এ সরিয়ে দেয়। বাস্তবে প্রায়ই দুটো একসাথে করতে হয়: আগে ধরনগুলো তৈরি করো, তারপর behaviour সরাও।
- একটা object-এর ধরন যদি চলার সময় বদলাতে পারে?
- তাহলে object-কেই subclass করা ভুল হবে, কারণ object-এর class তৈরির সময়েই ঠিক হয়ে যায়। বরং composition ব্যবহার করো: পরিবর্তনশীল behaviour একটা swap-যোগ্য State বা Strategy object-এ সরিয়ে দাও। Replace Type Code with State/Strategy পোস্টে ঠিক এই পরিস্থিতি নিয়ে আলোচনা আছে।
আরো দেখো
সম্পর্কিত পাঠ
Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা
Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।
State Pattern: একটা object-এর মেজাজ বদলানোর গল্প
State design pattern শেখো সিলিং ফ্যানের রেগুলেটরের গল্প দিয়ে। সহজ TypeScript আর C# code, state diagram, আর real software-এর উদাহরণ সহ।
Strategy Pattern: সাইকেল, বাস, নাকি অটো — তুমিই ঠিক করো
Strategy design pattern শেখো একটা সহজ স্কুলে যাওয়ার গল্পের মাধ্যমে — TypeScript আর C# কোড, runtime swapping, বাস্তব উদাহরণ, আর প্র্যাকটিস exercise সহ।
Replace Type Code with Subclasses: যখন প্রতিটা ধরন সত্যিই আলাদা আচরণ করে
Replace Type Code with Subclasses refactoring শেখো ডে-স্কলার/বোর্ডার/হোস্টেলার গল্পের মাধ্যমে। TypeScript আর C#-এ switch কীভাবে মুছে যায়, আর Class vs Subclasses vs State/Strategy — কোনটা কখন নেবে সেটাও বুঝবে।