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

Replace Type Code with Class: ম্যাজিক নম্বরকে একটা আসল পরিচয় দাও

Replace Type Code with Class রিফ্যাক্টরিং শেখো একটা স্কুলের গল্প দিয়ে — TypeScript আর C#-এ before/after দেখো, আর কখন Class, Subclasses বা State/Strategy বেছে নেবে সেটা একটা সহজ decision table দিয়ে বুঝে নাও।

22 মিনিট আপডেট: June 11, 2026beginner
refactoringtype codeenumssmart enumprimitive obsessiontypescriptcsharp

হাউস নম্বর ২-এর রহস্য

ধরো তুমি একটা স্কুলের নতুন অফিস কেরানি। তোমার সামনে একটা ফর্ম পড়ে আছে। তাতে লেখা "House: 2"।

তুমি থামলে। ২ মানে কী? নীল? সবুজ? স্কুলে চারটা হাউস আছে — লাল, নীল, সবুজ, হলুদ। কিন্তু কে কোনটা সেটা কোথাও লেখা নেই। পুরনো কেরানি সালাম সাহেব সব মাথায় রাখতেন। তিনি অবসর নিয়েছেন, মাথাটাও নিয়ে গেছেন।

তুমি তারেক ভাইকে জিজ্ঞেস করলে — বিশ বছর ধরে স্কুলে আছেন। তিনি বললেন সবুজ, পুরো আত্মবিশ্বাসে। পিটি টিচার নাসরিন ম্যাডামকে জিজ্ঞেস করলে। তিনি বললেন নীল, সমান আত্মবিশ্বাসে। দুইজন সিনিয়র মানুষ, দুটো উল্টো উত্তর।

আর খারাপ ঘটনা আগেই ঘটেছে। গত বছর একটা ফর্ম এসেছিল "House: 7" লেখা। কোনো হাউস ৭ নেই। কিন্তু ফর্মটা গৃহীত হলো, কম্পিউটারে টাইপ হলো, বার্ষিক রিপোর্টে ছাপা হলো। রুবেল নামের এক ছাত্র পুরো এক বছর এমন একটা হাউসে থাকল যেটার অস্তিত্বই নেই। আরেকটায় "House: 0" — কারণ অভিভাবক ঘরটা খালি রেখেছিলেন, আর কম্পিউটার সেটাকে শূন্য ধরে নিয়েছিল। শূন্য-হাউস রুবেল আর সাত-হাউস রুবেল — দুইজনেই database-এ শান্তিতে বাস করছিল। database-এর কাছে ওরা শুধু নম্বর, আর নম্বর কখনো অভিযোগ করে না।

তুমি পাশের স্কুলে গেলে দেখতে তারা কীভাবে সামলায়। সেখানে প্রতিটা ছাত্র একটা হাউস ব্যাজ পরে — ছোট সেলাই করা ঢাল, মোটা অক্ষরে "BLUE HOUSE" লেখা। "House 7" লেখা নকল ব্যাজ বানানো সম্ভব না কারণ ব্যাজ বানানোর লোকটার কাছে শুধু চারটা আসল design আছে। ব্যাজের মানে বুঝতে হয় না কারণ মানেটা ব্যাজের উপরেই লেখা। ভুল value তৈরি করাই অসম্ভব।

ফর্মের উপর সেই নম্বর ২ — প্রোগ্রামারদের ভাষায় এটা type code। সঠিক ব্যাজ হলো একটা class। আজকের refactoring, Replace Type Code with Class, হলো ঠিক এই কাজটা — রহস্যময় নম্বরকে একটা ব্যাজ দিয়ে বদলে দাও।

চিত্র ১: পুরনো ভর্তি ফর্মের হিসাব — সব জায়গায় magic number, কোথাও মানে নেই

কোডে হাত দেওয়ার আগে তোমার যাত্রাটা একবার দেখো — এটাই প্রতিটা developer-এর যাত্রা যে কখনো খালি নম্বরে ভরা codebase পেয়েছে।

চিত্র ২: নম্বর অনুমান করা থেকে ব্যাজে বিশ্বাস করা পর্যন্ত যাত্রা

Replace Type Code with Class কী?

Type code হলো একটা primitive value — কোনো int, string, বা magic constant — যেটা বোঝায় "এই জিনিসটা কোন ধরনের"। রক্তের গ্রুপ 0, 1, 2, 3। অ্যাকাউন্ট tier "GOLD"। হাউস 2। Value টা শুধু একটা label, কিন্তু রাখা হয়েছে খালি primitive হিসেবে।

এটা খারাপ কেন? কারণ primitive যেকোনো কিছু মেনে নেয়। ফিল্ড number হলে 7, -3, আর 42 সবই ঢুকতে পারে — এমনকি যখন শুধু 1 থেকে 4 মানে হয়। compiler সাহায্য করতে পারে না, কারণ তার কাছে হাউস নম্বর আর জুতার সাইজ একই জিনিস। Validation copy-paste হয় বিভিন্ন জায়গায়, একদিন কেউ একটা জায়গায় ভুলে যায় — ঠিক এভাবেই হাউস ৭ বার্ষিক রিপোর্টে ঢুকেছিল। আর 2-এর মানে শুধু মানুষের স্মৃতিতে থাকে, যেটা সালাম সাহেবের সাথে অবসরে চলে যায়।

Replace Type Code with Class বলে: type code-এর জন্য একটা ছোট, নির্দিষ্ট class বানাও। সেই class:

  1. Value set বন্ধ করে। শুধু আগে থেকে নির্ধারিত instance-গুলো — House.RED, House.BLUE, House.GREEN, House.YELLOW — থাকতে পারবে। Constructor private, তাই কেউ নকল House(7) বানাতে পারবে না।
  2. প্রতিটা value-এর নাম দেয়। আর অনুমান না, আর তারেক ভাইকে জিজ্ঞেস না। House.BLUE পড়লেই বোঝা যায়।
  3. Validation এক জায়গায় রাখে। Form বা database থেকে কাঁচা code parse করা একটা method-এ, একটাই জায়গায় হয়।
  4. Signature-এ দেখা যায়। House নেয় এমন function-এ ভুলে জুতার সাইজ দেওয়া যাবে না। Compiler এখন দরজায় পাহারা দেয়।

এই refactoring Martin Fowler-এর Refactoring বইয়ের classic catalog থেকে, আর এটা তিনটা refactoring-এর পরিবারের সবচেয়ে মৃদু সদস্য। এর ভাই-বোন হলো Replace Type Code with Subclasses আর Replace Type Code with State/Strategy। কিছুক্ষণ পরে শিখব কীভাবে তিনটার মধ্যে বেছে নিতে হয়।

💡

এক লাইনে সারাংশ: Replace Type Code with Class একটা "magic" primitive label-কে নামকরা, আগে থেকে নির্ধারিত instance সহ একটা ছোট class-এ পরিণত করে — যাতে illegal value লেখাই অসম্ভব হয়ে যায়।

এগোনোর আগে একটা গুরুত্বপূর্ণ কথা। এই refactoring তখনই সঠিক যখন type code কোনো behavior বহন করে না। আমাদের স্কুলে প্রতিটা হাউস software-এ একইভাবে কাজ করে — হাউস শুধু label। কোনো method কোথাও বলে না "নীল হাউস হলে marks আলাদাভাবে গণনা করো"। যখনই code অনুযায়ী behavior আলাদা হতে শুরু করে, তখন ভাই-বোনের একটা দরকার হয়।

কলেজ কর্নার: আমরা আসলে এখানে domain-driven design-এর একটু ছোঁয়া দিচ্ছি। House class টা একটা value object — একটা immutable type যেটা সম্পূর্ণভাবে তার value দিয়ে সংজ্ঞায়িত। গভীর নীতিটা হলো make illegal states unrepresentable: runtime-এ হাউস ৭ আসার পরে reject করার বদলে, এমন একটা type design করো যেখানে হাউস ৭-এর বানানই সম্ভব না। Type system শুধু typo ধরার জন্য না — এটা business rule encode করার টুল, যাতে ভুল compile time-এ ধরা পড়ে, সবচেয়ে সস্তা মুহূর্তে।

কখন এটা দরকার?

কোডে এই লক্ষণগুলো খোঁজো:

  • একটা field primitive কিন্তু মানে একটা category। bloodGroup: number, tier: string, house: number। Type বলছে "যেকোনো নম্বর", কিন্তু মানে বলছে "চারটার মধ্যে একটা"। এই অমিলটাই classic Primitive Obsession smell।
  • Magic constant আশেপাশে ঘুরছে। const RED = 1; const BLUE = 2; ফাইলের শুরুতে, আর প্রার্থনা যে কেউ সরাসরি 2 hard-code করবে না। কেউ না কেউ সবসময় সরাসরি 2 hard-code করে।
  • একই validation অনেক জায়গায়। if (house < 1 || house > 4) throw ... তিনটা service-এ copy-paste, আর চতুর্থটায় নেই — যেটা অবশ্যই সেই জায়গা যেখান দিয়ে খারাপ form ঢুকেছে।
  • ভুল value আসলেই ঢুকে গেছে। Database-এ house = 7 row আছে, বা tier = "GLOD" typo, আর কেউ জানে না কীভাবে হলো।
  • Function call পড়া যাচ্ছে না। registerStudent("রুবেল", 2) — ২ কী? আরেকটা file খুলতে হবে, বা টিমের সালাম সাহেবকে জিজ্ঞেস করতে হবে।

এটা সঠিক refactoring হলে দেখা উচিত না — code-এর উপর ভিত্তি করে behavior ভাগ হওয়া। যদি switch (house) ladder দেখো যেগুলো প্রতিটা হাউসে program-কে ভিন্ন কাজ করায়, সেটা Switch Statements smell, আর সাধারণ value class এটা সারাবে না। সেটার জন্য polymorphic ভাই-বোন দরকার।

একটা সৎ কথা: value set ছোট, স্থিতিশীল, আর আচরণ-মুক্ত হলে build-in enum যথেষ্ট হতে পারে। Enum হলো বাজার থেকে কেনা ready-made ব্যাজ, আর হাতে বানানো class হলো extra pocket সহ দর্জির বানানো ব্যাজ।

এক নজরে আগে এবং পরে

স্কুলের student record — refactoring-এর আগে। হাউসগুলো খালি নম্বর, আশা বেশি, নিরাপত্তা শূন্য।

// BEFORE: a type code — any number can sneak in
const HOUSE_RED = 1;
const HOUSE_BLUE = 2;
const HOUSE_GREEN = 3;
const HOUSE_YELLOW = 4;
 
class Student {
  constructor(
    public name: string,
    public house: number, // hopes and prayers only
  ) {}
}
 
const asha = new Student("Asha", 2); // is 2 Blue? Green? Who knows.
const broken = new Student("Ravi", 7); // compiles happily. House 7!

আর পরে। হাউস একটা proper class হয়ে গেল বন্ধ instance set সহ।

// AFTER: a class — only the four real houses can ever exist
class House {
  static readonly RED = new House("RED", "Red House");
  static readonly BLUE = new House("BLUE", "Blue House");
  static readonly GREEN = new House("GREEN", "Green House");
  static readonly YELLOW = new House("YELLOW", "Yellow House");
 
  private constructor(
    readonly code: string,
    readonly displayName: string,
  ) {}
 
  toString(): string {
    return this.displayName;
  }
}
 
class Student {
  constructor(
    public name: string,
    public house: House, // the compiler now stands guard
  ) {}
}
 
const asha = new Student("Asha", House.BLUE); // reads like English
// new Student("Ravi", 7)  -> compile error. House 7 cannot exist.

তিনটা জিনিস দেখো। Constructor private, তাই পুরো universe-এ মাত্র চারটা static readonly House object আছে — ঠিক ব্যাজ বানানোর লোকটার মতো যার কাছে মাত্র চারটা design। Field-এর type number থেকে House হয়ে গেছে, তাই ভুল value compile time-এ ধরা পড়ে, Sports Day-তে না। আর House.BLUE নিজের display name বহন করে — ব্যাজে মানে লেখা থাকায় তারেক ভাইকে আর জিজ্ঞেস করতে হয় না।

চিত্র ৩: আগে যেকোনো নম্বর house field-এ ঢুকত; পরে শুধু চারটা sealed instance আছে

Class structure টা নিজেই ছোট — এটাই এর আকর্ষণ। একটা ছোট class, চারটা sealed instance, একটা association।

চিত্র ৪: House value class — নামকরা instance-এর বন্ধ set যেটা Student point করে

তিনটার মধ্যে কোনটা বেছে নেব?

এটাই পুরো topic-এর মূল কথা। তাই এই section টা মনোযোগ দিয়ে পড়ো। তিনটা type-code refactoring আছে, আর সবাই গুলিয়ে ফেলে। ভালো খবর হলো: মাত্র দুটো প্রশ্ন করলেই বেছে নেওয়া সহজ।

প্রশ্ন ১: Code-এর উপর ভিত্তি করে কি behavior আলাদা হয়? মানে — কোনো method কি value অনুযায়ী ভিন্ন কাজ করে? (switch (type) বা if (type === ...) ladder খোঁজো।)

প্রশ্ন ২: Runtime-এ type কি বদলাতে পারে? মানে — একই object কি তার জীবনে এক type থেকে আরেকটায় যেতে পারে?

Code-এর উপর ভিত্তি করে behavior আলাদা?Runtime-এ type বদলায়?এই refactoring নাওদৈনন্দিন উদাহরণ
না — শুধু labelযেকোনো হোকReplace Type Code with Class (বা plain enum)স্কুল হাউস ব্যাজ — লাল, নীল, সবুজ, হলুদ; সবার behavior একই
হ্যাঁনা — object-এর সারাজীবন নির্ধারিতReplace Type Code with SubclassesDay-scholar বনাম boarder — আলাদা fee আর সময়সূচি, কিন্তু student record কখনো type পালটায় না
হ্যাঁহ্যাঁ — একই object type পালটায়Replace Type Code with State/StrategyPrepaid থেকে postpaid হওয়া SIM — একই নম্বর, নতুন behavior
চিত্র ৫: Decision flowchart — দুটো প্রশ্ন প্রতিবার সঠিক refactoring বেছে দেয়

আমাদের স্কুল হাউস প্রথম সারিতে পড়ে। হাউস শুধু label। কোনো method নীল হাউসের জন্য marks আলাদাভাবে গণনা করে না। তাই একটা value class (বা enum) যথেষ্ট — এখানে subclasses আনা চার রঙের ব্যাজ সামলাতে চারজন আলাদা principal নিয়োগের মতো হবে।

পছন্দটাকে দুটো axis-এর map হিসেবেও ভাবতে পারো। Behavior কতটা আলাদা, আর type কতটা পরিবর্তনশীল?

চিত্র ৬: একটা map-এ তিনটা refactoring — হাউস ব্যাজ শান্ত নিচে-বাম কোণে বসে

এই table আর map মনে রেখো। তিনটা ভাই-বোনের post-এই এগুলো আছে, তাই যেটাতেই পড়ো, সবসময় জানবে কোন tool মানানসই। উপরে-বাম "সাবধান" কোণটা বিরল কিন্তু বাস্তব: behavior সব জায়গায় একই কিন্তু label বদলাতেই থাকে — সাধারণত মানে label আসলে type code-ই না, বরং সাধারণ mutable data।

কলেজ কর্নার: TypeScript-এ প্রায়ই class-এর আগে union type দেখবে: type House = "RED" | "BLUE" | "GREEN" | "YELLOW"। Union value set বন্ধ করে আর চমৎকার exhaustiveness checking দেয় — never-typed default arm সহ union-এর উপর switch compile ব্যর্থ হয় যখন নতুন member আসে। Class জেতে যখন value-গুলো data (display name, color) আর behavior helper বহন করতে হয়, আর যখন একটাই parsing point চাও। অনেক mature codebase দুটোই ব্যবহার করে: JSON boundary-তে union, domain-এর ভেতরে value class।

ধাপে ধাপে, নিরাপদ পথে

Refactoring কখনো এক বিশাল লাফে করো না। ছোট ছোট পদক্ষেপে এগোও, প্রতিটার পরে compile করো আর test চালাও।

ধাপ ১: Value class বানাও। Private constructor, প্রতিটা valid value-র জন্য একটা static readonly instance, আর underlying code-এর জন্য একটা field।

class House {
  static readonly RED = new House(1, "Red House");
  static readonly BLUE = new House(2, "Blue House");
  static readonly GREEN = new House(3, "Green House");
  static readonly YELLOW = new House(4, "Yellow House");
 
  private constructor(
    readonly legacyCode: number, // keep the old number for boundaries
    readonly displayName: string,
  ) {}
}

ধাপ ২: Conversion helper যোগ করো। একটা method পুরনো primitive parse করে class-এ রূপান্তর করে; একটা property এটা ফিরিয়ে দেয়। সব validation এখন একটাই জায়গায়।

class House {
  // ...instances as above...
 
  static fromCode(code: number): House {
    const found = [House.RED, House.BLUE, House.GREEN, House.YELLOW]
      .find((h) => h.legacyCode === code);
    if (!found) {
      throw new Error(`Unknown house code: ${code}`);
    }
    return found;
  }
}

ধাপ ৩: পুরনো field-এর পাশে নতুন field যোগ করো। এটা মধ্যবর্তী পর্যায়। Student কিছুক্ষণ পুরনো houseCode: number রাখে, আর নতুন house: House পায়। পুরনো caller-রা কাজ করতে থাকে; কিছু ভাঙে না।

class Student {
  house: House; // new
 
  constructor(
    public name: string,
    public houseCode: number, // old — still alive during migration
  ) {
    this.house = House.fromCode(houseCode);
  }
}

ধাপ ৪: Caller-গুলো একে একে সরাও। যেখানে যেখানে code কাঁচা নম্বর পড়ে বা লেখে, সেগুলো class ব্যবহার করতে বদলাও। student.houseCode === 2 হয়ে যাবে student.house === House.BLUE। প্রতিটা caller-এর পরে compile করো আর test করো।

ধাপ ৫: পুরনো field আর magic constant মুছে দাও। যখন search-এ দেখা যায় কেউ আর houseCode বা HOUSE_BLUE = 2 ছোঁয় না, তখন মুছে দাও। Primitive শুধু I/O boundary-তে বেঁচে থাকে — database column, API JSON — যেখানে House.fromCode আর house.legacyCode boundary-তে অনুবাদ করে।

Migration টা নিজেই একটা ছোট state machine। তোমার codebase তিনটা স্পষ্ট পর্যায়ের মধ্য দিয়ে যায়।

চিত্র ৭: Migration-এর তিনটা পর্যায় — প্রথম থেকে শেষে এক লাফে যাওয়া চলবে না
⚠️

ধাপ ৩-এ পাশাপাশি রাখার stage টা এড়িও না। প্রথম দিনেই পুরনো নম্বর field মুছলে একসাথে সব caller ভেঙে যাবে, আর তুমি আতঙ্কে পঞ্চাশটা compile error ঠিক করছ। কিছুক্ষণ পুরনো আর নতুন একসাথে রাখলে শান্তিতে migrate করা যায়, একটা একটা caller, মাঝখানে green test রেখে। Equality-র বিষয়েও সতর্ক থাকো: class instance পুনরায় তৈরি হতে পারলে (যেমন JSON deserialization-এর পরে) object reference-এর বদলে code দিয়ে তুলনা করো।

একটা বড় বাস্তব উদাহরণ

উদাহরণটাকে আরও বাস্তব করি। এই স্কুলের নতুন software ভর্তি সামলায়। Refactoring-এর আগে code টা এরকম ছিল — আর production-এ সত্যিকার একটা bug ছিল:

// BEFORE: numbers everywhere, validation nowhere
function registerStudent(name: string, house: number, classLevel: number) {
  // someone forgot validation here
  db.save({ name, house, classLevel });
}
 
function sportsDayList(students: { name: string; house: number }[]) {
  // 2... was that Blue or Green? The author guessed. The author guessed WRONG.
  return students.filter((s) => s.house === 2); // meant Green, got Blue
}
 
registerStudent("Ravi", 7, 6); // house 7 saved into the database. Oops.

দুটো classic বিপর্যয়: ভুল value 7 save হয়ে গেছে কারণ অনেক entry point-এর একটায় validation ভুলে গেছে, আর একজন developer 2-এর মানে অনুমান করেছিল আর ভুলভাবে। Refactoring-এর পরে দুটো bug-ই অসম্ভব:

// AFTER: the House class guards the gates
class House {
  static readonly RED = new House(1, "Red House", "#d32f2f");
  static readonly BLUE = new House(2, "Blue House", "#1976d2");
  static readonly GREEN = new House(3, "Green House", "#388e3c");
  static readonly YELLOW = new House(4, "Yellow House", "#fbc02d");
 
  static readonly ALL = [House.RED, House.BLUE, House.GREEN, House.YELLOW];
 
  private constructor(
    readonly legacyCode: number,
    readonly displayName: string,
    readonly bannerColor: string, // data per value lives ON the value
  ) {}
 
  static fromCode(code: number): House {
    const found = House.ALL.find((h) => h.legacyCode === code);
    if (!found) throw new Error(`Unknown house code: ${code}`);
    return found;
  }
}
 
function registerStudent(name: string, house: House, classLevel: number) {
  db.save({ name, house: house.legacyCode, classLevel }); // primitive only at the edge
}
 
function sportsDayList(students: { name: string; house: House }[]) {
  return students.filter((s) => s.house === House.GREEN); // no guessing possible
}
 
// registerStudent("Ravi", 7, 6)        -> compile error
registerStudent("Ravi", House.fromCode(formValue), 6); // bad form data fails LOUDLY, in one place

Class টা চুপচাপ আরও কিছু দিয়েছে। প্রতিটা house তার bannerColor বহন করে, তাই Sports Day page আলাদা lookup table ছাড়াই banner রাঙাতে পারে। House.ALL dropdown menu-র জন্য ready তালিকা দেয়। আর কাঁচা নম্বর 7 শুধু fromCode-এ attack করতে পারে, যেখানে সাথে সাথে স্পষ্ট message দিয়ে ধরা পড়ে।

নতুন gate-এর মধ্য দিয়ে দুটো path trace করো — ভালো code আর খারাপ code — দেখো কোনটা কোথায় শেষ হয়।

চিত্র ৮: Boundary কাজে লেগেছে — code ২ আসল House হয়, code ৭ দরজায় উচ্চস্বরে আটকে যায়

মনে রাখার pattern: ভেতরে rich type, boundary-তেই শুধু primitive। Form, database, আর JSON নম্বরে কথা বলে; তোমার program House-এ কথা বলে। Boundary একটাই পাতলা রেখা, একটা জায়গায় রক্ষিত — শতটা ছড়িয়ে-ছিটিয়ে থাকা checkpost-এর বদলে।

C# আর Python-এ একই refactoring

C# হালকা থেকে সমৃদ্ধ পর্যন্ত বিকল্পের একটা সিঁড়ি দেয়।

বিকল্প ১: একটা plain enum ছোট, স্থিতিশীল, আচরণ-মুক্ত set-এর জন্য প্রায়ই এটাই যথেষ্ট:

public enum House
{
    Red = 1,
    Blue = 2,
    Green = 3,
    Yellow = 4
}
 
public class Student
{
    public required string Name { get; init; }
    public required House House { get; init; }
}

এটাই value-গুলোর নাম দেয় আর signature-এ দেখা যায়। কিন্তু C# enum-এর দুর্বলতা জানো: এটা আসলে গোপনে একটা integer, আর compiler (House)7-কে অভিযোগ ছাড়া মেনে নেয়। Enum display name বা banner color-এর মতো extra data-ও বহন করতে পারে না। তাই boundary-তে Enum.IsDefined দিয়ে validate করো, আর যদি আরও বেশি দরকার হয়, সিঁড়ি বাড়াও।

বিকল্প ২: হাতে বানানো value class — TypeScript version-এর মতোই আকৃতি, private constructor আর static readonly instance সহ:

public sealed class House
{
    public static readonly House Red = new(1, "Red House");
    public static readonly House Blue = new(2, "Blue House");
    public static readonly House Green = new(3, "Green House");
    public static readonly House Yellow = new(4, "Yellow House");
 
    public static IReadOnlyList<House> All { get; } = [Red, Blue, Green, Yellow];
 
    public int Code { get; }
    public string DisplayName { get; }
 
    private House(int code, string displayName)
        => (Code, DisplayName) = (code, displayName);
 
    public static House FromCode(int code) =>
        All.FirstOrDefault(h => h.Code == code)
        ?? throw new ArgumentException($"Unknown house code: {code}");
 
    public override string ToString() => DisplayName;
}

এখন (House)7 অসম্ভব — কোনো cast নেই, কোনো নকল instance নেই, কোনো পেছনের দরজা নেই। প্রতিটা value data বহন করে, আর parsing একটা জায়গায়।

বিকল্প ৩: একটা smart enum library। C# community এই pattern টা এতটাই পছন্দ করেছে যে Steve "Ardalis" Smith এটাকে Ardalis.SmartEnum NuGet library হিসেবে package করেছেন। একটা base class থেকে inherit করো আর বিনামূল্যে পাও value-equality, FromName, FromValue, সব instance-এর তালিকা, এমনকি JSON আর EF Core integration package:

using Ardalis.SmartEnum;
 
public sealed class House : SmartEnum<House>
{
    public static readonly House Red = new(nameof(Red), 1, "Red House");
    public static readonly House Blue = new(nameof(Blue), 2, "Blue House");
    public static readonly House Green = new(nameof(Green), 3, "Green House");
    public static readonly House Yellow = new(nameof(Yellow), 4, "Yellow House");
 
    public string DisplayName { get; }
 
    private House(string name, int value, string displayName)
        : base(name, value) => DisplayName = displayName;
}
 
// House.FromValue(2)        -> House.Blue
// House.FromName("Green")   -> House.Green
// House.List                -> all four houses, ready for a dropdown

এই ধারণাকে মাঝে মাঝে enumeration class বলা হয়, আর Microsoft-এর নিজস্ব microservices guidance ঠিক আমাদের কারণে এটা সুপারিশ করে: যখন একটা enum validation, data, বা behavior চাইতে শুরু করে, সেটাকে class-এ উন্নীত করো।

Python-এ? Python-এর enum.Enum আমাদের হাতে বানানো class-এর কাছাকাছি C# enum-এর চেয়ে — member-রা আসল singleton object, set বন্ধ, আর data আর helper যোগ করা যায়:

from enum import Enum
 
class House(Enum):
    RED = (1, "Red House")
    BLUE = (2, "Blue House")
    GREEN = (3, "Green House")
    YELLOW = (4, "Yellow House")
 
    def __init__(self, code: int, display_name: str):
        self.code = code
        self.display_name = display_name
 
    @classmethod
    def from_code(cls, code: int) -> "House":
        for house in cls:
            if house.code == code:
                return house
        raise ValueError(f"Unknown house code: {code}")
 
# House.from_code(2)  -> House.BLUE
# House.from_code(7)  -> ValueError, loudly, in one place

কলেজ কর্নার: ভাষা জুড়ে spectrum টা লক্ষ্য করো। C# enum একটা integer-এর উপর পাতলা রং — (House)7 পার হয়ে যায়, তাই exhaustive switch expression-এ এখনো একটা discard arm দরকার। Java enum আর Python Enum হলো সত্যিকার class, sealed instance set সহ, যে কারণে Java-র switch compiler exhaustiveness check করতে পারে। TypeScript union never trick-এর মাধ্যমে exhaustiveness পায়। Design brain-এর জন্য শিক্ষা: "enum" অনেক ভিন্ন safety level-এর একটা শব্দ, তাই সবসময় জিজ্ঞেস করো set-এর বাইরে একটা value তৈরি করা যায়? যদি হ্যাঁ, তাহলে এখনো একটা পাহারাদার parsing point দরকার। কোনোদিন একটা হাউসে প্রকৃতপক্ষে per-value behavior দরকার হলে — সাবধান! Decision table আবার চেক করো। হালকা per-value data আর helper এখানে ভালোভাবে মানায়; প্রতিটা type-এ সত্যিকার ভিন্ন behavior Subclasses বা State/Strategy-এ যাওয়ার সংকেত।

সুবিধা আর ঝুঁকি

সুবিধাঝুঁকি / খরচ
Illegal value অপ্রকাশযোগ্য হয়ে যায় — House(7) থাকতে পারে নালিখতে আর রক্ষণাবেক্ষণ করতে একটা নতুন class, আর I/O boundary-তে mapping code
প্রতিটা value-এর readable নাম আছে; code ইংরেজির মতো পড়া যায়ছোট, স্থিতিশীল set-এর জন্য অতিরিক্ত — plain enum হালকা হতে পারে
Validation একটা জায়গায় (fromCode), প্রতিটা entry point-এ ছড়িয়ে নাSerialization-এ যত্ন দরকার: JSON আর database এখনো primitive-এ কথা বলে
Compiler signature check করে — house code class level-এর জায়গায় দেওয়া যাবে নাInstance পুনরায় তৈরি হলে reference equality সমস্যা করতে পারে; code দিয়ে তুলনা করো
Per-value data (display name, color) value-এর উপরেই থাকেBehavioral switch সরায় না — behavior আলাদা হলে ভুল tool
পরে helper বাড়ানোর জন্য পরিষ্কার seam রেখে যায়Team-কে private-constructor idiom শিখতে হবে

পেঅফটা তাত্ত্বিক না। এই স্কুলের developer যখন রেকর্ড দেখলেন, নম্বর যুগ আর ব্যাজ যুগের পার্থক্য দেখে নম্বর যুগ বিব্রত হয়ে যায়।

চিত্র ৯: Database-এ পৌঁছানো ভুল house entry, value class-এর আগে আর পরে

বছরে বারোটা ভুল entry ছোট শোনায় যতক্ষণ না মনে করো প্রতিটা একজন রুবেল যে Sports Day-তে ভুল line-এ দাঁড়িয়ে, বার্ষিক রিপোর্টে একটা ভুল সারি, আর trace করতে একটা বিকেলের অফিস সময় নষ্ট। Refactoring-এর পরে সংখ্যা শূন্য — মানুষ সতর্ক হয়েছে বলে না, বরং অসাবধানতা compile করা বন্ধ হয়েছে বলে।

কোন smell-গুলো সারায়?

Smellএই refactoring কীভাবে সাহায্য করে
Primitive Obsessionসরাসরি সারায় — type-এর ভান করা primitive আসল type হয়ে যায়
Magic number আর stringপ্রতিটা রহস্যময় value নামকরা, sealed instance পায়
Duplicate validationসব parsing আর checking একটা fromCode method-এ চলে আসে
Switch Statementsশুধু চাপ কমায় — বন্ধ, নামকরা set-এর উপর switch নিরাপদ, কিন্তু behavioral switch সরাতে ভাই-বোন refactoring দরকার
Value পরিবর্তনে Shotgun surgeryএকটা value যোগ বা নামান্তর একটা class-এই হয়, বিশটা file-এ না

পুরো পরিবার এক ছবিতে

Revision card-এর আগে, এখানে একটাই mental map-এ পুরো type-code পরিবার। এই তিনটা post থেকে যদি একটাই ছবি মনে রাখো, এটাই মনে রেখো।

চিত্র ১০: Type-code পরিবারের map — পরিস্থিতি মেলাও সঠিক শাখায়

দ্রুত revision box

+----------------------------------------------------------------+
|        REPLACE TYPE CODE WITH CLASS - REVISION CARD            |
+----------------------------------------------------------------+
| Problem  : a primitive (int/string) secretly means a category  |
|            -> any value sneaks in, meaning lives in memory     |
| Solution : small class, PRIVATE constructor,                   |
|            static readonly instance per legal value            |
| Result   : illegal values cannot even be written               |
|                                                                |
| WHICH OF THE THREE?                                            |
|   no behaviour varies            -> CLASS / ENUM   (this one)  |
|   behaviour varies, type fixed   -> SUBCLASSES                 |
|   behaviour varies + type changes-> STATE / STRATEGY           |
|                                                                |
| Remember : rich type inside, primitive only at the boundary    |
| C# bonus : plain enum -> hand-rolled class -> Ardalis.SmartEnum|
+----------------------------------------------------------------+

অনুশীলন করো

তোমার পালা। ধরো একটা hospital app রক্তের গ্রুপ নম্বর দিয়ে রাখে: 0 = O, 1 = A, 2 = B, 3 = AB। নিচের code ইতিমধ্যে রক্তের গ্রুপ 9 সহ একজন রোগী গ্রহণ করেছে।

const O = 0, A = 1, B = 2, AB = 3;
 
class Patient {
  constructor(
    public name: string,
    public bloodGroup: number, // danger!
  ) {}
}
 
const p = new Patient("Meena", 9); // compiles. 9 is not a blood group.

নিজে refactoring করো, ধাপে ধাপে:

  1. একটা BloodGroup class বানাও private constructor আর চারটা static readonly instance সহ: O, A, B, AB। প্রতিটায় একটা code: number আর একটা label: string দাও (যেমন "AB")।
  2. BloodGroup.fromCode(code: number) যোগ করো যেটা ০–৩-এর বাইরে যেকোনো কিছুর জন্য স্পষ্ট error দেয়, আর একটা BloodGroup.ALL তালিকা।
  3. Patient.bloodGroup-এর type BloodGroup-এ পরিবর্তন করো, caller migrate করার সময় কিছুক্ষণ পুরনো numeric field রেখে — চিত্র ৭-এর তিনটা migration পর্যায় মনে করো।
  4. জয় প্রমাণ করো: দেখাও যে new Patient("সুমাইয়া", 9) আর compile হয় না, আর BloodGroup.fromCode(9) স্পষ্ট message দিয়ে উচ্চস্বরে ব্যর্থ হয়।
  5. বোনাস চিন্তা: hospital এখন একটা নিয়ম চায় — "গ্রুপ O সবাইকে রক্ত দিতে পারে"। এটা কি value-এর উপর data (এখানে ঠিক আছে) নাকি type অনুযায়ী behavior আলাদা (decision table আবার দেখার ইঙ্গিত)? তোমার উত্তর একটা বাক্যে সমর্থন করো।

যদি কোনো বন্ধুকে বোঝাতে পারো কেন ধাপ ৪-এর compile error একটা উপহার, তাহলে এই refactoring সম্পূর্ণ বুঝেছ। আর যখন তোমার type code সত্যিকার অর্থে প্রতিটা value-তে ভিন্নভাবে আচরণ করতে শুরু করে, সরাসরি পরবর্তী post-এ চলো: Replace Type Code with Subclasses, যেখানে তিন ধরনের ছাত্র তিন ধরনের fee দেয়।

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

type code আসলে কী জিনিস?
type code হলো একটা সাধারণ নম্বর বা string যেটা আসলে একটা ক্যাটাগরি বোঝায় — যেমন ২ মানে নীল হাউস, বা 'GOLD' মানে গোল্ড অ্যাকাউন্ট। কম্পিউটার শুধু একটা primitive value দেখে, তাই ভুল value আটকাতে বা মানে বোঝাতে পারে না।
কখন Replace Type Code with Class ব্যবহার করব, Subclasses বা State/Strategy-এর বদলে?
যখন code টা শুধু একটা label আর এর উপর ভিত্তি করে কোনো behavior বদলায় না — তখন এটা ব্যবহার করো। যদি behavior আলাদা হয় কিন্তু type সারাজীবন একই থাকে, তাহলে Subclasses। আর যদি type রানটাইমে বদলাতে পারে, তাহলে State/Strategy।
পুরো class-এর বদলে কি একটা সাধারণ enum যথেষ্ট?
বেশিরভাগ সময় হ্যাঁ। ছোট, স্থিতিশীল value set-এর জন্য যেখানে extra data বা rules নেই, language enum একদম ঠিকঠাক। হাতে বানানো class বা smart enum তখনই দরকার যখন validation, display name-এর মতো extra field, বা প্রতিটা value-তে helper method চাই।
এই refactoring কি switch statement সরিয়ে দেয়?
নিজে থেকে না। এটা value set বন্ধ করে আর value-গুলোর নাম দেয়, যা switch-কে নিরাপদ করে। কিন্তু method-গুলো যদি এখনো type দেখে ভিন্নভাবে কাজ করে, তাহলে সেই switch সরাতে Replace Type Code with Subclasses বা State/Strategy লাগবে।
database বা API-তে যেখানে শুধু নম্বর আছে সেখানে কী করব?
boundary-তে ছোট conversion helper রাখো। একটা fromCode method কাঁচা নম্বর বা string পার্স করে class instance-এ রূপান্তর করে, আর একটা code property সেটাকে ফিরিয়ে দেয়। program-এর ভেতরে সবাই rich type ব্যবহার করে; শুধু boundary primitive ছোঁয়।

আরো দেখো

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

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

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

আরও পড়ুন

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

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

আরও পড়ুন

Replace Type Code with Subclasses: যখন প্রতিটা ধরন সত্যিই আলাদা আচরণ করে

Replace Type Code with Subclasses refactoring শেখো ডে-স্কলার/বোর্ডার/হোস্টেলার গল্পের মাধ্যমে। TypeScript আর C#-এ switch কীভাবে মুছে যায়, আর Class vs Subclasses vs State/Strategy — কোনটা কখন নেবে সেটাও বুঝবে।

আরও পড়ুন

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।

আরও পড়ুন