মূল বিষয়বস্তুতে যান
Clean Code Mastery

Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা

Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।

24 মিনিট আপডেট: June 11, 2026beginner
code smellsoo abusersswitch statementsconditionalspolymorphismtypescriptrefactoring

সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা

ধরো তুমি একটা বড় school-এ গেছ। গেটের সামনে বসে আছেন সালেহা ম্যাডাম — school-এর রিসেপশনিস্ট। বাইশ বছর ধরে কাজ করছেন এখানে। প্রতিটা অভিভাবকের রিকশা আর প্রতিটা দোকানদারের ভ্যান তিনি চেনেন। তাঁর টেবিলে একটা মোটা ভারী নিয়মের খাতা। কেউ এলেই তিনি খাতা খুলে পড়েন:

  • অভিভাবক হলে — নীল slip দাও, class teacher-কে ডাকো, বাচ্চার নাম লেখো।
  • পরিচয়পত্র ভুলে যাওয়া ছাত্র হলে — হলুদ slip দাও, class monitor-কে ডাকো, দেরির নোট করো।
  • বই-এর দোকানদার হলে — সবুজ slip দাও, librarian-কে ডাকো, delivery list চেক করো।
  • coach হলে — সাদা slip দাও, PT স্যারকে ডাকো, পেছনের গেট খুলে দাও।

এটা শুধু entry-র নিয়ম। পাতা উল্টাও — দ্বিতীয় তালিকায় parking-এর জন্য একই চার ধরনের visitor: অভিভাবক বাঁ পাশে park করবেন, দোকানদার storeroom-এর কাছে, coach মাঠের পাশে। আরেকটু উল্টাও — তৃতীয় তালিকায় exit pass-এর জন্যও একই চার ধরনের visitor। একই চারটা case, তিনটা জায়গায় লেখা।

এবার principal ঘোষণা করলেন — school-এ Alumni Cell হবে। পুরনো ছাত্ররা reunion আর career talk-এর জন্য আসবে। মানে নতুন একটা visitor type: alumni। বেচারি সালেহা ম্যাডাম এখন তিনটা chapter-ই ঠিক করতে বসলেন। প্রথম chapter ঠিক করলেন। দ্বিতীয় chapter ঠিক করলেন। চায়ের দোকান থেকে চা এলো, একটু গল্প হলো — তারপর মনে করলেন হয়ে গেছে, বাড়ি গেলেন।

তৃতীয় chapter বাদ পড়ে গেল।

দুই সপ্তাহ পর পুরনো ছাত্র রুবেল এলো reunion-এ। entry slip পেলো। parking পেলো। বিকেল পাঁচটায় বের হতে গেল — exit গেটের দারোয়ান তৃতীয় chapter খুলল, "alumni"-র কোনো নিয়ম নেই, বের হতে দিলো না। ছোট একটা ঝামেলা হলো। principal শুনলেন। সালেহা ম্যাডাম মনে মনে কষ্ট পেলেন — কিন্তু আসলে দোষটা তাঁর না। খাতার design-ই এই ভুলের জন্য set করা ছিল। একটা নতুন idea তিনটা আলাদা জায়গায় লিখতে হলে, একটা না একটা জায়গা miss হবেই।

School-এর computer স্যার তারিক স্যার পুরো ব্যাপারটা দেখে বললেন একটা মজার কথা: "একটা smart design আছে। রিসেপশনিস্ট শুধু একটা প্রশ্ন করবেন — কোন department-এ যাবেন? — তারপর সেই department-এ পাঠিয়ে দেবেন। Library জানে book vendor-কে কীভাবে handle করতে হয়। Sports office জানে coach-দের কীভাবে deal করতে হয়। প্রতিটা department নিজের visitor নিজেই সামলায়। Alumni Cell খুললে, রিসেপশনিস্টের কাজ এতটুকু বদলাবে না।"

সেই মোটা খাতা — যেখানে একই তালিকা তিনবার লেখা — এটাই হলো Switch Statements code smell। আর "সঠিক department-এ পাঠিয়ে দাও" design-টা হলো polymorphism। সালেহা ম্যাডাম, রুবেল আর তারিক স্যারকে মাথায় রাখো — পুরো আলোচনাটা তাদের গল্প, আর আমরা এখন সেই খাতাটা code-এ লিখব।

এই smell টা আসলে কী?

Switch Statements হলো Object-Orientation Abuser smell-গুলোর একটা। এই ধরনের smell তখন আসে যখন code class-এর ভেতরে লেখা, কিন্তু object-oriented programming-এর আসল শক্তিগুলো — inheritance, interface, polymorphism — ব্যবহার করা হচ্ছে না। Object আছে, কিন্তু কাজ করছে না; একটা বিশাল conditional সেই কাজটা তাদের হয়ে করছে।

সহজ কথায় smell টা কী? একটা switch (বা if-else-if ladder — একই জিনিস, ভিন্ন পোশাক) যেটা কোনো type code-এর উপর branch করে: enum, "parent" বা "vendor" এর মতো string, number code, বা runtime type check। একটা এরকম switch সাধারণত ঠিকাছে। Smell টা serious হয় যখন একই switch বারবার অনেক জায়গায় লেখা হয় — একটা switch slip-এর রঙ বেছে নেয়, আরেকটা parking spot বেছে নেয়, তৃতীয়টা exit rule বেছে নেয়, সব একই cases-এর উপর branch করে।

এটা এতটাই গুরুত্বপূর্ণ একটা point যে Martin Fowler তাঁর Refactoring বইয়ের দ্বিতীয় সংস্করণে এই smell-এর নাম "Switch Statements" থেকে "Repeated Switches" বদলে দিলেন। কারণ switch keyword-টা অকারণে বদনাম পাচ্ছিল। অপরাধ একবার switch করা না। অপরাধ হলো একই জিনিসের উপর বারবার switch করা, code-এর ভিন্ন ভিন্ন কোণে — ঠিক যেমন একই visitor-এর তালিকা তিনটা chapter-এ copy করা।

💡

এক লাইনে মূল কথা: Switch Statements smell মানে হলো একই type-based branching codebase জুড়ে copy-paste হয়ে আছে — ফলে নতুন type আসলে প্রতিটা copy খুঁজে edit করতে হয় — অথচ প্রতিটা type নিজেই নিজের behaviour বহন করতে পারত।

এটা কেন "OO abuser"? কারণ object-oriented language-গুলোতে already একটা built-in, automatic switch আছে: এটার নাম polymorphism। তুমি যখন visitor.handleEntry() call করো, language নিজেই visitor-এর আসল type দেখে সঠিক method run করে। তুমি বিনামূল্যে branching পাচ্ছো, compiler check করছে, কোনো case ভুলে যাওয়ার সুযোগ নেই। Type code-এর উপর manual switch লেখা মানে এই বিনামূল্যের উপহার ফিরিয়ে দেওয়া, আর dispatch হাতে হাতে করা — washing machine থাকতে কাপড় ঘাটে ধোয়ার মতো।

একটু deep dive: visitor.handleEntry() call করলে পর্দার আড়ালে কী হয়? C# আর Java-এর মতো language-এ প্রতিটা class একটা hidden method pointer-এর table বহন করে (vtable বলে)। Call টা সেই table থেকে সঠিক method খুঁজে নেয় runtime-এ, constant time-এ। এটা dynamic dispatch — runtime তোমার হয়ে switch করে, automatically class hierarchy-র সাথে sync রেখে। হাতে লেখা switch হলো static branching: caller-এর source code-এ cases জমাট বেঁধে থাকে। আসল পার্থক্য হলো নতুন behaviour কোথায় যায়। Branching-এ নতুন behaviour মানে প্রতিটা caller edit করা। Dynamic dispatch-এ নতুন behaviour মানে একটা নতুন class যোগ করা, আর সব existing call site নিজে থেকেই সেটা ধরে নেয়। এটাই Open/Closed Principle — শুধু slogan না, machinery-তে।

এই smell-এর পুরো ছবি এক map-এ:

চিত্র ১: Switch Statements smell এক নজরে — লক্ষণ, ক্ষতি, সমাধান, আর কখন রাখবে

কীভাবে চিনবে

Code পড়ার সময় এই checklist ব্যবহার করো। যেকোনো একটা মিললেই ভালো করে দেখো:

  • কোনো field, enum, বা string আছে যার একমাত্র কাজ বলা যে object টা কী ধরনের (type, kind, category, role)।
  • সেই kind value-এর উপর একটা switch বা if-else-if ladder branch করছে।
  • সেই enum বা string project-এ search করলে তিন বা তার বেশি জায়গায় পাওয়া যাচ্ছে।
  • গত মাসে নতুন kind যোগ করতে গিয়ে একটা নতুন file বানানোর বদলে বেশ কয়েকটা existing file edit করতে হয়েছিল।
  • default: branch আছে যেটা "unknown type" error throw করে — compiler তোমাকে protect করতে পারছে না, এটার runtime স্বীকারোক্তি।
  • instanceof, typeof, obj.type === "..." দেখছো, বা type check-এর পরে cast — code-এ ছড়িয়ে ছিটিয়ে।
  • একই cases-এর দুটো switch আলাদা হয়ে গেছে — একটায় পাঁচটা case, আরেকটায় চারটা, আর কেউ জানে না এটা bug কিনা।

এক নজরে symptom table:

তুমি যা দেখছোআসলে মানে কী
কোনো class-এ type বা kind fieldBehaviour টা object-এর বাইরে decide হচ্ছে, যে এই field পড়ছে তার কাছে
একই switch cases ৩+ function-এএকটা concept ("visitor type") physically ছড়িয়ে গেছে; update করতে প্রতিটা copy-তে হাত দিতে হবে
default: throw new Error("unknown type")Cases-এর set open-ended আর compiler completeness check করতে পারছে না
Method call-এর আগে instanceof / typeof checkCaller-রা shared interface-কে trust করছে না — সম্ভবত কারণ কোনো interface নেই
নতুন feature = অনেক পুরনো file edit করাপ্রতিটা change-এ Open/Closed Principle ভাঙছে
দুটো switch ladder-এ case-এর সংখ্যা আলাদাCopy-গুলো ইতিমধ্যে diverge করেছে; একটা silently ভুল

একটা কার্যকর trick: enum টা ধরো (যেমন VisitorType) আর project-wide search দাও। Switch ladder কয়টা আছে গোনো। একটা — relax। দুটো — নজর রাখো। তিনটা বা তার বেশি — smell confirmed, refactoring লাভজনক হবে।

তারিক স্যার যখন school-এর visitor management software-এ এই search দিলেন (হ্যাঁ, সেই নিয়মের খাতা অনেক বছর আগে একজন developer code করে দিয়ে গিয়েছিল), দেখলেন একই VisitorType switch চারটা জায়গায় লুকিয়ে আছে:

চিত্র ২: একটা visitor-type switch, school software-এ চারটা আলাদা আস্তানা

চারটা copy। সালেহা ম্যাডামের খাতায় তিনটা chapter ছিল; software-এ চুপচাপ চতুর্থ একটা monthly report module-এ জন্মে গেছে যেটা কেউ মনে রাখেনি। এটাই স্বাভাবিক। Switch-এর copy অন্ধকারে বংশ বৃদ্ধি করে।

কেন এটা সমস্যা

সহজ কথায় cost গুলো গণি — সালেহা ম্যাডামের গল্প ধরে।

Cost 1: প্রতিটা নতুন case মানেই shotgun surgery। Alumni visitor আসলে entry switch, parking switch, exit switch, আর report switch — চারটা জায়গায় edit করতে হবে। একটা idea-র জন্য চারটা edit। একটা miss করলেই bug — রুবেল গেটে আটকে পড়ল। এই "এক change, অনেক edit" ব্যথার নিজের নাম আছে — Shotgun Surgery smell — আর repeated switch হলো এর সবচেয়ে বড় কারণগুলোর একটা।

Cost 2: Compiler তোমাকে বাঁচাতে পারবে না। Polymorphism থাকলে নতুন subclass বানিয়ে abstract method implement করতে ভুলে গেলে code compile-ই হতো না। Scattered switch-এ forgotten case খুশিমনে compile হয়, runtime-এ fail করে — প্রায়ই user-এর সামনে, প্রায়ই সেই default: throw branch দিয়ে। সালেহা ম্যাডামের miss করা chapter রুবেল বিকেল ৫টায় ধরল, দুপুরে কোনো checker ধরেনি।

Cost 3: জ্ঞান data থেকে অনেক দূরে থাকে। "vendor-রা storeroom-এর কাছে park করবে" — এটা vendor সম্পর্কে জ্ঞান, কিন্তু এটা থাকে একটা parking function-এ যে সব visitor type-এর কথা জানে। প্রতিটা switch একটা ছোট gossip center — সবার ব্যাপার জানে। Cohesion কমে; coupling বাড়ে।

Cost 4: Copy-গুলো drift করে। আজ দুটো switch-এ চারটা করে case। আগামী মাসে তাড়াহুড়ো করে কেউ এক switch-এ পঞ্চম case যোগ করল, অন্যটায় ভুলে গেল। এখন codebase নিজের সাথেই চুপচাপ দ্বিমত পোষণ করছে — ঠিক যেমন report module-এর সেই লুকানো চতুর্থ switch।

দেখো পচন কীভাবে ছড়ায়:

চিত্র ৩: একটা innocent switch কীভাবে ধীরে ধীরে codebase নষ্ট করে

নিচের loop টা খেয়াল করো। প্রতিটা hotfix switch-কে বড় করে, যেটা পরের নতুন type-কে আরও কষ্টের করে তোলে। Smell নিজেই নিজেকে খাওয়ায়।

এবার নতুন type আর scattered switch-গুলোর মধ্যে কথোপকথনটা দেখো। এই ঘটনাক্রমই রুবেলকে গেটে আটকে দিল:

চিত্র ৪: Alumni type-কে নিজে নিজে প্রতিটা switch-এ যেতে হলো — একটা visit হলোই না

আর সেই সন্ধ্যায় developer-এর কেমন লাগছিল, দেখো কোথায় মেজাজটা ভেঙে পড়ল:

চিত্র ৫: Switch-ভারী code-এ একটা নতুন visitor type যোগ করার দিন

শেষে, সহজ সংখ্যায় cost। Switch-এর copy যত বেশি, প্রতিটা নতুন type-এর জন্য তত বেশি file ছুঁতে হয়। Polymorphism-এ উত্তর সবসময় একটা:

চিত্র ৬: প্রতিটা নতুন visitor type-এর জন্য কতগুলো file edit করতে হবে, design অনুযায়ী

বাঁ দিকের bars সময়ের সাথে সাথে আরও বাড়ে — প্রতিটা নতুন feature switch copy করলে সেগুলো উঁচু হয়। ডান দিকেরটা চিরকাল একেই থাকে। এই পার্থক্য release-এর পর release compound হতে থাকে।

আসল code-এ উদাহরণ

সালেহা ম্যাডামের নিয়মের খাতাটা code-এ লিখি — যেভাবে school-এর পুরনো developer লিখেছিল। এটা হলো smelly version, আর মনোযোগ দাও কারণ আসল বিপদ হলো একই switch তিনবার আসছে:

// BAD CODE: visitor is just data with a type tag
type VisitorType = "parent" | "student" | "vendor" | "coach";
 
interface Visitor {
  name: string;
  type: VisitorType;
}
 
// Switch copy #1: entry slips
function issueEntrySlip(v: Visitor): string {
  switch (v.type) {
    case "parent":
      return `Blue slip for ${v.name}. Call the class teacher.`;
    case "student":
      return `Yellow slip for ${v.name}. Call the class monitor.`;
    case "vendor":
      return `Green slip for ${v.name}. Call the librarian.`;
    case "coach":
      return `White slip for ${v.name}. Call the PT teacher.`;
    default:
      throw new Error("Unknown visitor type!");
  }
}
 
// Switch copy #2: parking — SAME cases, different file in real life
function assignParking(v: Visitor): string {
  switch (v.type) {
    case "parent":
      return "Park on the left side.";
    case "student":
      return "Cycle stand only.";
    case "vendor":
      return "Park near the store room.";
    case "coach":
      return "Park near the sports ground.";
    default:
      throw new Error("Unknown visitor type!");
  }
}
 
// Switch copy #3: exit passes — the SAME cases yet again
function exitRule(v: Visitor): string {
  switch (v.type) {
    case "parent":
      return "Exit after teacher signs the slip.";
    case "student":
      return "Exit only after last bell.";
    case "vendor":
      return "Exit after store room checks the delivery.";
    case "coach":
      return "Exit through the back gate.";
    default:
      throw new Error("Unknown visitor type!");
  }
}

ধীরে পড়ো। প্রতিটা function আলাদা আলাদাভাবে দেখতে পরিষ্কার মনে হয়। এটাই ফাঁদ! একটা switch-ই evil দেখায় না। Evil হলো repetition-এ: "vendor" string টা এখন তিনটা জায়গায় load-bearing। আগামীকাল "alumni" type যোগ করতে গেলে:

  1. VisitorType union update করো।
  2. issueEntrySlip edit করো। 3. assignParking edit করো। 4. exitRule edit করো।
  3. আশা করো বাকি সব switch খুঁজে পেলে — মনে আছে সেই report module-এর কথা যেটা কেউ মনে রাখেনি।

Real project-এ এই তিনটা function তিনটা আলাদা file-এ থাকে, তিনজন আলাদা developer লিখেছে, তিনটা আলাদা মাসে। তুমি যেমন পাশাপাশি দেখলে, কেউ কখনো এভাবে দেখেনি। রুবেলের বিকেল পাঁচটার গেট নাটক হওয়াটাই স্বাভাবিক।

ধাপে ধাপে ঠিক করা

তারিক স্যার গ্রীষ্মের ছুটিতে school software ঠিক করতে volunteer করলেন। আমরা তাঁর refactoring follow করব আস্তে আস্তে, real project-এর মতো — প্রতিটা step-এর পরেও code কাজ করতে থাকবে।

Step 1: ছড়ানো behaviour এক জায়গায় জড়ো করো। Class introduce করার আগে, আগে তিনটা switch পাশাপাশি নিয়ে এসো যাতে duplication দেখা যায় আর test করা যায়:

// Step 1: at least the rulebook is now ONE object per type, in ONE place
const visitorRules: Record<VisitorType, {
  entrySlip: (name: string) => string;
  parking: string;
  exit: string;
}> = {
  parent: {
    entrySlip: (n) => `Blue slip for ${n}. Call the class teacher.`,
    parking: "Park on the left side.",
    exit: "Exit after teacher signs the slip.",
  },
  student: {
    entrySlip: (n) => `Yellow slip for ${n}. Call the class monitor.`,
    parking: "Cycle stand only.",
    exit: "Exit only after last bell.",
  },
  vendor: {
    entrySlip: (n) => `Green slip for ${n}. Call the librarian.`,
    parking: "Park near the store room.",
    exit: "Exit after store room checks the delivery.",
  },
  coach: {
    entrySlip: (n) => `White slip for ${n}. Call the PT teacher.`,
    parking: "Park near the sports ground.",
    exit: "Exit through the back gate.",
  },
};

এটা already বড় একটা জয়: TypeScript এখন force করছে প্রতিটা visitor type-কে তিনটা behaviour-ই দিতে হবে। কোনো case ভুলে গেলে compile error — runtime surprise না। এই lookup table আগে থেকে থাকলে, "alumni" যোগ করার সাথে সাথেই সালেহা ম্যাডামের miss করা chapter ধরা পড়ত — compiler তিনটা নিয়মই একসাথে চেয়ে বসত। অনেক ছোট program-এর জন্য এখানেই থামা যায়।

Step 2: Type code-কে আসল class দিয়ে replace করো। যখন behaviour সহজ string-এর বাইরে বাড়ে — vendor-দের delivery list দরকার, coach-দের gate key দরকার — প্রতিটা case-কে একটা interface-এর পেছনে নিজের class-এ upgrade করো। এটাই classic refactoring যার নাম Replace Conditional with Polymorphism, আর এটাই তারিক স্যারের "সঠিক department-এ পাঠিয়ে দাও" idea:

// Step 2: each department handles its own visitors
interface SchoolVisitor {
  readonly name: string;
  entrySlip(): string;
  parkingSpot(): string;
  exitRule(): string;
}
 
class ParentVisitor implements SchoolVisitor {
  constructor(readonly name: string, private childClass: string) {}
  entrySlip()   { return `Blue slip for ${this.name}. Call teacher of ${this.childClass}.`; }
  parkingSpot() { return "Park on the left side."; }
  exitRule()    { return "Exit after teacher signs the slip."; }
}
 
class VendorVisitor implements SchoolVisitor {
  constructor(readonly name: string, private deliveryId: string) {}
  entrySlip()   { return `Green slip for ${this.name}. Delivery ${this.deliveryId}.`; }
  parkingSpot() { return "Park near the store room."; }
  exitRule()    { return "Exit after store room checks the delivery."; }
}
 
class CoachVisitor implements SchoolVisitor {
  constructor(readonly name: string) {}
  entrySlip()   { return `White slip for ${this.name}. Call the PT teacher.`; }
  parkingSpot() { return "Park near the sports ground."; }
  exitRule()    { return "Exit through the back gate."; }
}
 
// The receptionist's whole job now:
function receive(visitor: SchoolVisitor) {
  console.log(visitor.entrySlip());
  console.log(visitor.parkingSpot());
  console.log(visitor.exitRule());
}

receive function-টা দেখো। কোনো switch নেই। কোনো default নেই। কোনো throw নেই। এখন alumni যোগ করা মানে শুধু একটা নতুন class AlumniVisitor লেখা — একটা existing line-ও ছুঁতে হবে না। এটাই Open/Closed Principle কাজে লাগছে: নতুন জিনিসের জন্য খোলা, পরিবর্তনের জন্য বন্ধ। নতুন design-এর class diagram:

চিত্র ৭: একটা contract, অনেক department — প্রতিটা visitor type নিজের তিনটা behaviour নিজেই বহন করে

একটু deep dive: এখানে precise trade-off টা কী, যেটা তোমার design patterns professor জানতে চাইবেন। Branching আর polymorphism হলো dual। Switch একটা type-set-এর সব operation function জুড়ে ছড়িয়ে রাখে — তাই নতুন operation যোগ করা সহজ (একটা নতুন function) কিন্তু নতুন type যোগ করা কঠিন (প্রতিটা function edit করো)। Polymorphism একটা type-এর সব operation এক class-এ জড়ো রাখে — তাই নতুন type যোগ করা সহজ (একটা নতুন class) কিন্তু নতুন operation যোগ করা কঠিন (প্রতিটা class-এ হাত দিতে হয়)। এই tension-এর নাম Expression Problem। Practical rule: যদি তোমার type বাড়তে থাকে (নতুন visitor, নতুন payment method), polymorphism বেছে নাও। যদি operation বাড়তে থাকে স্থিতিশীল type-এর উপর (compiler যেটা fixed AST node-এর উপর অনেক pass করে), তাহলে switch — বা Visitor pattern — আসলেই ভালো হতে পারে। তোমার project-এ কোন axis বাড়ছে সেটা বোঝাটাই পুরো খেলা।

Step 3: ঠিক একটা switch রাখো — boundary-তে। Real data আসে flat হয়ে: form, database row, বা type: "vendor" সহ JSON payload। কোথাও না কোথাও সেই flat tag-কে সঠিক object-এ convert করতে হবে। সেই জায়গাটা হলো factory, আর সেখানে একবার switch করা জায়েজ:

// The ONLY switch left in the whole program — and that is fine
function createVisitor(data: { type: VisitorType; name: string; extra: string }): SchoolVisitor {
  switch (data.type) {
    case "parent":  return new ParentVisitor(data.name, data.extra);
    case "vendor":  return new VendorVisitor(data.name, data.extra);
    case "coach":   return new CoachVisitor(data.name);
    case "student": return new StudentVisitor(data.name);
  }
}
চিত্র ৮: আগে আর পরে — তিনটা ছড়ানো switch হলো একটা factory আর polymorphism

একটা healthy project-এ switch-এর জীবনকাহিনী একটা predictable পথ follow করে — আর refactor করা মানে সেই পথে এগিয়ে দেওয়া:

চিত্র ৯: একটা switch-এর life cycle — আর প্রতিটা stage-এ বেরিয়ে আসার সুযোগ

নিচে-বাঁয়ের exit টা লক্ষ্য করো: একটা switch যদি single থাকে সে কখনোই এই journey-তে পড়ে না। Diagram সেটা সৎভাবে দেখাচ্ছে।

এই exact transformation-এ আরও গভীরে যেতে পড়ো Replace Conditional with Polymorphism। আর যখন "type" আসলে runtime-এ বদলানো mode (draft থেকে published থেকে archived), বা cases হলো caller-এর বেছে নেওয়া interchangeable algorithm, তখন cure হলো State আর Strategy pattern — একই idea, দুটো আলাদা পরিস্থিতির জন্য package করা।

⚠️

শেষ switch টা delete করো না! Factory, parser, আর deserializer-কে একবার branch করতেই হবে flat data থেকে rich object বানাতে। লক্ষ্য zero switch না — লক্ষ্য হলো প্রতিটা type code-এর জন্য একটাই switch, boundary-তে, আর business logic-এ কোনোটাই না।

C#-এ একই smell

C#-এ smell টা হুবহু একইরকম দেখায়। এখানে একটা shop billing-এর উদাহরণ, আগে smelly version:

enum ItemKind { Book, Toy, Medicine }
 
class BillingService
{
    public decimal Tax(decimal price, ItemKind kind) => kind switch
    {
        ItemKind.Book     => 0m,
        ItemKind.Toy      => price * 0.12m,
        ItemKind.Medicine => price * 0.05m,
        _ => throw new ArgumentException("unknown kind")
    };
 
    // Same cases again — the duplication is the smell
    public string ShelfLabel(ItemKind kind) => kind switch
    {
        ItemKind.Book     => "Aisle 1 - Books",
        ItemKind.Toy      => "Aisle 4 - Toys",
        ItemKind.Medicine => "Counter only",
        _ => throw new ArgumentException("unknown kind")
    };
}

আর polymorphic cure — প্রতিটা item kind নিজের tax আর shelf জানে এমন class হয়ে গেল:

abstract class Item
{
    public abstract decimal Tax(decimal price);
    public abstract string ShelfLabel { get; }
}
 
sealed class Book : Item
{
    public override decimal Tax(decimal price) => 0m;
    public override string ShelfLabel => "Aisle 1 - Books";
}
 
sealed class Toy : Item
{
    public override decimal Tax(decimal price) => price * 0.12m;
    public override string ShelfLabel => "Aisle 4 - Toys";
}
 
sealed class Medicine : Item
{
    public override decimal Tax(decimal price) => price * 0.05m;
    public override string ShelfLabel => "Counter only";
}

Stationery যোগ করা মানে একটা নতুন class। BillingService কখনো বদলায় না। C# fans-দের জন্য একটা সৎ note: modern C#-এর switch expression closed enum-এর উপর, compiler warning সহ missing cases-এর জন্য, কিছুটা safety ফিরিয়ে দেয়। একটা জায়গায় একটা exhaustive switch expression ভালো design হতে পারে। বিপদ তবুও একই — switch copy-paste হওয়ার সাথে সাথে smell ফিরে আসে।

একটু deep dive: functional language-গুলো এই একই tension handle করে algebraic data type আর exhaustive pattern matching দিয়ে — cases-এর একটা closed set যেখানে compiler prove করে প্রতিটা match প্রতিটা constructor handle করছে। TypeScript-এর discriminated union আর never-typed exhaustiveness check একই proof দেয়। তাই modern চিত্র এটা না যে "switch খারাপ, class ভালো।" বরং: হয় compiler-কে দিয়ে এক জায়গায় closed cases verify করাও (exhaustive match), নয়তো class জুড়ে open cases handle করতে dynamic dispatch-কে দাও (polymorphism)। Smell থাকে unguarded middle-এ: open-ended string tag, কোনো exhaustiveness checking নেই, আর সর্বত্র copy।

Real project-এ এই smell কোথায় লুকায়

এই smell-এর চেহারা চিনে গেলে সর্বত্র দেখবে। Real জায়গাগুলো:

  • Payment processing। switch (paymentMethod) — bKash, card, net banking, wallet, cash on delivery — charge code, refund code, receipt code, আর analytics code-এ বারবার। চারটা switch, একটা concept।
  • Order আর ticket status flow। if (status === "pending") ... else if (status === "shipped") ... — UI rendering, notification, আর permission check-এ ছড়ানো। এটা সাধারণত ছদ্মবেশে state machine — State pattern-এর কাজ।
  • File আর message format handling। File extension বা message type (csv, json, xml) এর উপর reader, validator, আর exporter-এ আলাদা আলাদা branch।
  • Pricing আর discount rule। Customer tier switch (regular, silver, gold) — cart total, shipping cost, আর loyalty point-এ copy করা — textbook Strategy case।
  • UI rendering by type। Frontend component-এ switch (widget.type) rendering ladder — validation আর serialization code-এ একই ladder mirror করছে।
  • Deserialized API data। JSON আসে "type" discriminator field সহ, আর boundary-তে একবার object-এ convert করার বদলে raw tag program-এর গভীরে চলে যায় যেখানে সবাই তার উপর switch করে।

সবার মধ্যে একটা common thread: data আসে flat tag নিয়ে, আর কেউ একবার সেই tag-কে object-এ convert করার দায়িত্ব নেয় না — তাই tag সর্বত্র ঘুরে বেড়ায় আর সবাই তার উপর branch করে। School software-এ "vendor" string গেটের form থেকে চারটা module-এ ঘুরে বেড়ানো একই রোগ।

কখন ignore করা যাবে

সৎভাবে বলি। প্রতিটা switch-এ surgery দরকার না — এটা না মানলে নিজেই over-engineering করছ। তারিক স্যার factory switch convert করেননি, আর তিনি সঠিক ছিলেন।

পরিস্থিতিসিদ্ধান্তকারণ
একটাই সহজ switch, একটা জায়গায়, enum থেকে label-এ map করছেরাখোLabel lookup-এর জন্য class hierarchy switch-এর চেয়ে অনেক ভারী
Factory বা parser-এ একটা switchরাখোBoundary-তে object বানাতে একবার branch করতেই হয়; এটা design কাজ করছে, fail করছে না
Sealed/closed enum-এ exhaustive switch expression, compiler-checked, একটা জায়গায়সাধারণত ঠিকাছেModern compiler missing case-এ warn করে, polymorphism-এর মূল safety benefit ফিরিয়ে দেয়
২ জায়গায় একই switch, years ধরে cases stableনজর রাখোChurn কম মানে cost কম; তৃতীয় copy বা পরের নতুন case এলে refactor করো
৩+ জায়গায় একই switchRefactor করোপ্রতিটা নতুন case এখন multi-file hunt; এটাই আসল smell
Runtime-এ বদলানো status-এর উপর অনেক method-এ switchState-এ refactor করোতোমার কাছে switch costume পরা একটা state machine আছে
JSON থেকে type tag business logic-এর গভীরে চলে গেছেRefactor করোBoundary-তে একবার tag-কে object-এ convert করো; tag-কে travel করতে দিও না

যেকোনো switch দুটো প্রশ্নে এই map-এ রাখতে পারো: copy হয়েছে কি? আর type বাড়তে থাকে কি?

চিত্র ১০: দুটো প্রশ্ন সিদ্ধান্ত দেয় — switch কি copy হয়েছে, আর type কি বাড়তে থাকে?

School rulebook টা উপরে-ডানে গভীরে: চারটা copy, আর principal প্রতি বছর নতুন visitor type বের করছেন। এখনই refactor করো। Factory switch বাঁদিকে নিরাপদে বসে আছে: visitor type বাড়তে থাকলেও, এটা একটাই, compiler check করছে।

ℹ️

Fowler-এর নামবদলা থেকে একটা ভালো rule of thumb: repetition গোনো, switch গোনো না। Branching-এর শূন্য বা এক occurrence — ঠিকাছে। দুটো — সাবধান। তিনটা বা তার বেশি — পরের বার নতুন case যোগ করার সময়ই refactoring নিজেই cost তুলে নেবে।

কোন refactoring সারায়

Refactoringকখন ব্যবহার করবে
Replace Conditional with Polymorphismমূল cure: একই switch repeated, type-এর উপর branch, প্রতিটা type-এ কয়েকটা behaviour
Replace Type Code with SubclassesType creation-এ fixed, কখনো বদলায় না — প্রতিটা code subclass হয়
Replace Type Code with State/Strategy (State, Strategy)"Type" runtime-এ বদলায় (State) বা swappable algorithm (Strategy)
Replace Parameter with Explicit MethodsSwitch doTask("save") এর মতো parameter-এ branch করছে — save(), load() তে ভাগ করো
Introduce Null Objectএকটা branch শুধু "কিছু নেই" case handle করতে আছে
Extract Method + Move MethodFirst aid: প্রতিটা switch-কে নামযুক্ত method-এ টেনে বের করো যাতে duplication দেখা যায়

Quick revision

+----------------------------------------------------------------+
|  SWITCH STATEMENTS (a.k.a. Repeated Switches) — CHEAT SHEET    |
+----------------------------------------------------------------+
|  Story    : Receptionist re-reads the same 4-case rulebook     |
|             in 3 chapters; smart school sends each visitor     |
|             to its own department.                             |
|  Smell    : Same switch on a type code, copy-pasted around.    |
|  Spot it  : Search the enum -> found in 3+ if/switch ladders.  |
|  Danger   : New case = edit every copy; miss one = runtime bug |
|             via "default: throw unknown type".                 |
|  Cure     : Replace Conditional with Polymorphism;             |
|             State (runtime modes), Strategy (algorithms).      |
|  Keep     : ONE switch at the boundary (factory/parser) that   |
|             turns flat data into the right object.             |
|  Mantra   : Count the repetitions, not the switches.           |
+----------------------------------------------------------------+

Practice exercise

তোমার পালা। একটা food delivery app-এ এই code আছে, একই branching তিনটা জায়গায়:

type Plan = "free" | "plus" | "gold";
 
function deliveryFee(plan: Plan, orderTotal: number): number {
  if (plan === "free") return 40;
  if (plan === "plus") return orderTotal > 199 ? 0 : 25;
  return 0; // gold
}
 
function supportPriority(plan: Plan): string {
  if (plan === "free") return "Email only, 48 hours";
  if (plan === "plus") return "Chat, 4 hours";
  return "Phone, 30 minutes"; // gold
}
 
function badge(plan: Plan): string {
  if (plan === "free") return "";
  if (plan === "plus") return "PLUS";
  return "GOLD ★";
}

তিনটা ধাপে refactoring করো:

  1. Step 1: একটা single Record<Plan, ...> object বানাও যেটা তিনটা behaviour ধরে রাখে, আমাদের Step 1-এর মতো। তারপর তিনটা function-কে এক লাইনে rewrite করো যেটা সেখান থেকে পড়ে।
  2. Step 2: Product team এখন একটা নতুন "corporate" plan চাইছে custom per-company fee সহ। Full polymorphism-এ upgrade করো: MembershipPlan interface বানাও deliveryFee(), supportPriority(), আর badge() দিয়ে, আর প্রতিটা plan-এর জন্য একটা class।
  3. Step 3: একটাই allowed switch লেখো — একটা createPlan(tag: string) factory — আর check করো: পঞ্চম plan যোগ করতে কতটা existing file edit করতে হবে? (সঠিক উত্তর হলো factory ছাড়া শূন্য।)
  4. Step 4 (story check): দুটো বাক্যে explain করো কেন তোমার Step 2 design-এ সালেহা ম্যাডামের miss করা chapter হতে পারত না। রুবেলের বিকেল ৫টার গেট নাটকের জায়গায় ঠিক কোন compiler error আসত?

Bonus প্রশ্ন — বন্ধুর সাথে আলোচনা করো: plus plan-এর fee orderTotal-এর উপর নির্ভর করে। Eid-পূজার season-এ fee rule বারবার বদলালে কোন pattern বেশি মানানসই — State নাকি Strategy? (Hint: একটা plan কি নিজে থেকেই অন্য plan-এ transition করে, নাকি business শুধু একটা rule বেছে নেয়?)

সচরাচর জিজ্ঞাসা

প্রতিটা switch statement কি code smell?
না। একটা জায়গায় একটা সহজ switch রাখলে কোনো সমস্যা নেই। smell টা তখন আসে যখন একই type-এর উপর একই switch বারবার copy-paste হয়ে বিভিন্ন জায়গায় ছড়িয়ে পড়ে — তখন নতুন একটা case যোগ করতে গেলে সব copy খুঁজে বের করে edit করতে হয়।
Martin Fowler কেন এই smell-এর নাম বদলে Repeated Switches রাখলেন?
Refactoring বইয়ের দ্বিতীয় সংস্করণে Fowler নাম বদলান কারণ আসল সমস্যা হলো repetition, switch keyword নিজে না। একটা switch ক্ষতিকর না। একই switch codebase-এর বিভিন্ন জায়গায় duplicate হলেই সেটা আসল smell।
Switch statements smell সারাতে মূল refactoring কোনটা?
Replace Conditional with Polymorphism। প্রতিটা case-কে তার নিজের class বানাও একটা shared interface দিয়ে — তাহলে প্রতিটা type নিজের behaviour নিজেই বহন করে, আর caller-দের কাছ থেকে branching উধাও হয়ে যায়।
switch কখন আসলেই সঠিক পছন্দ?
Factory, parser, আর deserializer-এর মতো boundary-তে — যেখানে flat data-কে object-এ convert করতেই হবে। একটা জায়গায় একটাই switch যা সঠিক object তৈরি করে — এটা সেই conversion-এর স্বাভাবিক মূল্য।
if-else ladder আর এই smell-এর সম্পর্ক কী?
একই kind value-এর উপর branch করা if-else-if ladder হলো ভিন্ন পোশাক পরা একই smell। keyword ব্যাপার না — type code-এর উপর বারবার branching করাটাই কষ্টের কারণ।

আরো দেখো

সম্পর্কিত পাঠ

Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি

Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।

আরও পড়ুন

Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number

Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।

আরও পড়ুন

Shotgun Surgery: এক জায়গায় পরিবর্তন, দশ জায়গায় দৌড়াদৌড়ি

Shotgun Surgery code smell শিখবে রুবেলের বাসা বদলের গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript আর C# এর example, Divergent Change এর সাথে পার্থক্য, আর practice সহ।

আরও পড়ুন

Replace Conditional with Polymorphism: প্রতিটি ধরনকে তার নিজের ডেস্ক দাও

Replace Conditional with Polymorphism রিফ্যাক্টরিং শেখো স্কুল রিসেপশনের গল্প দিয়ে — বারবার আসা type switch কীভাবে subclass-এ পরিণত হয়, TypeScript ও C#-এ factory কীভাবে কাজ করে, আর কখন সাধারণ switch রেখে দেওয়াই ভালো সেটাও বুঝবে।

আরও পড়ুন