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

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

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

21 মিনিট আপডেট: June 11, 2026beginner
code smellsprimitive obsessionbloatersrefactoringvalue objectsdomain-driven design

বিপদ লুকিয়ে আছে সেই এক লাইনে

ধরো জামাল ভাই বিয়ের কার্ড পাঠাচ্ছেন। দুইশো জনকে। প্রতিটা খামে ঠিকানা লিখলেন এক লাইনে:

"করিম সাহেব ১৪ মিরপুর রোড পুরনো বাজারের কাছে ঢাকা ১২১৬"

এই এক লাইনে কতগুলো সমস্যা আছে একটু ভাবো। "১৪" কি বাড়ির নম্বর, নাকি রাস্তার নম্বর? "পুরনো বাজার" কি landmark, নাকি এলাকার নাম? আর postal code-টা একটু মনোযোগ দিয়ে দেখো — মাত্র চার ডিজিট। Postal code তো ছয় ডিজিটের হওয়া দরকার। কিন্তু কিছুই তাকে ছয় ডিজিট দিতে বাধ্য করছে না। সব কিছু শুধু কালির একটা দাগ।

বিশটা কার্ড ফিরে আসে। ডাকপিয়ন পড়তে পারেননি। বিশজন অতিথি প্রায় বিয়েই মিস করছিলেন।

জামাল ভাইয়ের ছোট ভাই তারিক একটা courier startup-এ কাজ করে। সে ভাইকে online form দিয়ে কার্ড re-post করতে সাহায্য করল — আলাদা box আছে নাম, বাড়ি নম্বর, রাস্তা, শহর, আর postal code-এর জন্য। Postal code-এর box ঠিক ছয় ডিজিট ছাড়া কিছুই নেয় না। Form-টা ভুলটা ধরল যখন টাইপ করা হচ্ছিল — তিন সপ্তাহ পরে কার্ড ফিরে আসলে না।

বাসায় ফেরার পথে তারিক ভাবতে লাগল। গত মাসে তাদের company-তে একজন customer-কে ৫০০ টাকার বদলে ৫০,০০০ টাকা charge করা হয়েছিল — কেউ পয়সা পাঠিয়েছিল যেখানে code টাকা expect করছিল। তার আগের সপ্তাহে একটা parcel phone number-এর জায়গায় postal code লেখা ছিল বলে ভুল জায়গায় চলে গেছিল। জামাল ভাইয়ের খামের সমস্যা আর company-র bug — আসলে একই রোগ: গুরুত্বপূর্ণ value লেখা হচ্ছে plain, rule-ছাড়া scribble হিসেবে।

একটু ভাবো — একজন দোকানদার তার খাতায় লিখল "দাম ৫০০"। পাঁচশো কী? টাকা? পয়সা? একটু অসাবধান হলেই ৫০০ টাকার বিল হয়ে যায় ৫ টাকা। Plain number কোনো unit বহন করে না, তাই সেটা আমাদের চুপিচুপি ভুল হতে দেয়।

Code-এ এই smell-এর নাম Primitive Obsession — plain string আর number দিয়ে এমন কিছু represent করার অভ্যাস, যার আসলে নিজস্ব type আর নিজস্ব rule থাকা দরকার। এই lesson-এ তারিক office-এ ফিরে সেই রোগ খুঁজে বের করবে আর সারাবে software design-এর একটা সুন্দর idea দিয়ে: value object

এই smell টা আসলে কী

একটু মনে করিয়ে দিই: code smell মানে bug না। Code compile হয়, run হয়, অনেক সময় ঠিকঠাক কাজও করে। Smell হলো আগাম সতর্কবার্তা — design-এ এমন কিছু আছে যা পরে bug ডেকে আনবে আর change করা কঠিন করে দেবে। Primitive Obsession হলো Martin Fowler-এর Refactoring বইয়ের "Bloater" smell-গুলোর একটা — তবে এটা code ফুলিয়ে তোলে চুপিচুপি, validation আর conversion ছড়িয়ে দিয়ে সারা codebase জুড়ে।

Primitive Obsession মানে হলো raw language primitive — string, number, int, boolean, array — দিয়ে এমন domain concept represent করা যার নিজের type দরকার:

  • একটা string যেটা আসলে email address
  • একটা number যেটা আসলে টাকার পরিমাণ
  • একটা string যেটা আসলে postal code
  • দুটো number যেটা আসলে latitude আর longitude

এটাকে obsession বলা হয় কেন? কারণ এটা একটা অভ্যাস যেটা আমরা ছাড়তে পারি না। Primitive সবসময় হাতের কাছে থাকে। Email store করতে হবে? string তো সাথে সাথে কাজ করে — কোনো design লাগে না। তাই primitive-টা চলে যায়। পরের developer সেটা copy করে। "Email address" concept-টা কখনো code-এ নাম পায় না, যদিও পুরো business সেটার উপর নির্ভর করছে। তারিক তাদের company-র codebase-এ ঠিক এটাই পায়: "parcel" শব্দটা প্রতিটা meeting-এ আছে, কিন্তু codebase শুধু string আর number চেনে।

💡

Domain-driven design থেকে একটা দারুণ কথা: make invalid states unrepresentable। মানে হলো, postal code যদি তার নিজের type হয় আর তৈরির সময়েই ছয় ডিজিট check করে, তাহলে পাঁচ ডিজিটের postal code program-এ exist করতেই পারবে না। Bug ধরা পড়বে না — bug হওয়াটাই impossible হয়ে যাবে।

এই রোগের cure-এর সুন্দর নাম: value object — একটা ছোট type যেটা তার value দিয়ে define হয়, তৈরির সময়েই নিজেকে validate করে, আর নিজের behavior বহন করে। ৫০ টাকার দুটো Money object সমান, ঠিক যেমন দুটো পঞ্চাশ টাকার নোট বদলযোগ্য। বিখ্যাত domain-driven-hexagon project guide এই কারণেই value object-কে ভালো backend design-এর মূল building block হিসেবে দেখায়।

একটু deeper জানতে চাইলে: Eric Evans-এর Domain-Driven Design বইয়ে value object হলো domain model-এর তিনটা building block-এর একটা — entity আর aggregate-এর পাশে। মূল পার্থক্য: value object-এর কোনো identity নেই (শুধু value-based equality আছে), এটা immutable, আর তৈরির সময়েই নিজের invariant enforce করে। Functional programmer-রা এই idea-কে চেনে "parse, don't validate" নামে — untyped input-কে একবার boundary-তে proof-carrying type-এ convert করো, বারবার check করার দরকার নেই।

কীভাবে চিনবে

তারিক এই checklist দিয়ে তাদের codebase audit করল। তুমিও তোমার codebase-এ চালাও:

  • string email, string phone, number amount, string status — primitive-রা domain concept-এর নামের পোশাক পরেছে।
  • একই validation বারবার: যতগুলো function phone number নেয়, সবাই আলাদা করে length check করছে।
  • Magic string বা number type code হিসেবে: if (user.plan === "PREMIUM") কুড়িটা file-এ ছড়িয়ে আছে।
  • একসাথে ঘোরা primitive pair: amount আর currencyCode সবসময় একসাথে, latitude আর longitude সবসময় একসাথে।
  • Positional meaning-সহ array: point[0] হলো x আর point[1] হলো y — কিন্তু কেউ তোমাকে উল্টো পড়তে বাধা দিচ্ছে না।
  • Mixed unit বা swapped value থেকে bug: পয়সা গেছে যেখানে টাকা expected ছিল, বা একই type-এর দুটো parameter চুপিচুপি বদলে গেছে।
লক্ষণকী বলছে
string email, string pin, number priceDomain concept তোমার মাথায় আছে, type system-এ নেই
পাঁচটা file-এ একই regex checkValidation-এর কোনো বাড়ি নেই, তাই copy-paste হচ্ছে আর আলাদা হয়ে যাচ্ছে
if (type === "GOLD") সর্বত্রType code চিৎকার করছে, তাকে real type বা enum বানাও
amount + currency সবসময় পাশাপাশিএকটা Money value object জন্ম নিতে চাইছে
data[3] convention-এ "discount"Positional array একটা structured record লুকিয়ে রেখেছে
পয়সা/টাকা বা cm/inch mix-up bugUnit-ছাড়া number physics-কে চুপিচুপি ভুল হতে দেয়

তারিক তাদের company-র audit-এ দেখল postal code length check পাঁচটা file-এ copy করা — আর ইতিমধ্যে সেগুলো মিলছে না: চারটায় length === 6, একটায় শূন্য দিয়ে শুরু হওয়া code-ও reject করছে। কোনটা ঠিক? কেউ মনে রাখতে পারছে না।

কেন সমস্যা

১. Validation duplicate হয় — তারপর drift করে। ছয় ডিজিটের postal code rule প্রতিটা entry point-এ check করতে হয়, কারণ plain string কিছুই guarantee করে না। দেরি না হতেই একটা copy update হবে, বাকি চারটা হবে না। তারিক এখন এটাই দেখছে।

২. Compiler তোমাকে protect করতে পারে না। sendCard(pinCode, phone) আর sendCard(phone, pinCode) — দুটোই string, compiler-এর কাছে একই দেখাচ্ছে। আলাদা type হলে swap করা compile error হত — program run করার আগেই bug ধরা পড়ত।

৩. Domain language হারিয়ে যায়। Business টাকা, ঠিকানা, postal code নিয়ে কথা বলে; code বলে string আর number। নতুন developer-দের file-by-file পড়ে meaning বের করতে হয়।

৪. Behaviour-এর কোনো বাড়ি নেই। "দুটো টাকার পরিমাণ safely যোগ করো" — এটা কোথায় থাকবে? Primitive দিয়ে উত্তর হয় "project-এর সতেরোটা helper function-এ ছড়িয়ে।" Money type থাকলে উত্তর হয় "Money-তে।"

৫. Invalid value স্বাধীনভাবে ঘুরে বেড়ায়। Negative quantity, ভুল format-এর email, পাঁচ ডিজিটের postal code — primitive সব গলাধকরণ করে আর system-এর গভীরে পাঠিয়ে দেয়, যেখানে এগুলো অনেক দূরে গিয়ে ফাটে।

আর এটা শুধু classroom theory না। ১৯৯৯ সালে NASA-র Mars Climate Orbiter — ৩০ কোটি ডলারের একটা মহাকাশযান — হারিয়ে যায়। একটা ground software thruster data পাঠাচ্ছিল pound-second-এ আর navigation software expect করছিল newton-second। দুই পাশেই ছিল plain number — কোনো unit attached নেই। তাই কেউ mismatch বুঝতে পারেনি। Spacecraft মঙ্গলের atmosphere-এ ঢুকে গেল ভুল angle-এ আর ধ্বংস হয়ে গেল। Unit-সহ একটা type থাকলে mix-up হতেই দিত না। এটা planetary scale-এ Primitive Obsession।

চিত্র ১: একটা innocent primitive কীভাবে system-wide সমস্যায় পরিণত হয়

তারিক তিন মাসের production bug categorize করার পরে তার manager সাথে সাথে রাজি হয়ে গেলেন। অর্ধেকের বেশি bug আসছে rule-ছাড়া primitive থেকে:

চিত্র ২: তিন মাসের production bug, root cause অনুযায়ী — primitive-ই আধিপত্য করছে

একটা চুপচাপ আরেকটা cost আছে: codebase বাড়ার সাথে সাথে scattered check-ও বাড়তে থাকে:

চিত্র ৩: Codebase বাড়ার সাথে একই validation rule-এর কতগুলো copy হয় — type না থাকলে check গুণে গুণে বাড়ে

PinCode type থাকলে সেই line সবসময় একে flat থাকত।

Deeper জানতে চাইলে: Drift সমস্যাটা হলো Single Source of Truth principle-এর লঙ্ঘন। Type-system researcher-রা এর cure-কে বলেন "newtypes" বা "branded types": zero-cost wrapper যার কাজ শুধু একই shape-এর দুটো value-কে incompatible করে রাখা। TypeScript branded intersection type দিয়ে এটা simulate করে, Haskell আর Rust natively support করে, C# পায় record struct দিয়ে। Runtime representation প্রায়ই একই থাকে — safety টা purely compile-time, মানে এটা free।

পয়সার bug-এর গল্প — live demo

কিছু fix করার আগে তারিক তার team-কে দেখাল গত মাসের ৫০,০০০ টাকার disaster আসলে কোথায় হয়েছিল:

চিত্র ৪: পয়সার bug-এর anatomy — একটা unitless number তিনটা layer পার হয়ে গিয়ে ফাটল

চারটা layer সেই number handle করল। কেউই জানতে পারল না এটা পয়সা — কারণ number-এ কোনো unit থাকে না। Bug ফাটল customer-এর bank account-এ — source থেকে সবচেয়ে দূরে। তারিকের সেই সপ্তাহটা দেখতে ছিল এরকম:

চিত্র ৫: একটা unitless number system-এর মধ্যে তাড়া করতে তারিকের পুরো সপ্তাহ লাগল

সবচেয়ে দুঃখের row-টা দেখো: fix নিজেই সহজ ছিল, কিন্তু তারিককে চারটা জায়গায় guard check paste করতে হলো — পরের drift bug-এর বীজ বপন করে। Primitive patch করা রোগ সারায় না; বরং ভদ্রভাবে ছড়িয়ে দেয়।

কোন value-এর type দরকার

"তাহলে কি project-এর প্রতিটা string wrap করতে হবে?" — তারিকের junior teammate জিজ্ঞেস করল। না! সব কিছু wrap করা নিজেই একটা সমস্যা। তারিক whiteboard-এ দুটো axis আঁকল: value-এর কি rule আছে, আর সেটা system-এর কত দূরে দূরে যায়?

চিত্র ৬: Wrap decision — যেগুলোর rule আছে আর সব জায়গায় যায়, সেগুলোকে আগে type বানাও

Money আর postal code "Wrap it now" quadrant-এ গভীরে: অনেক rule, সব module-এ যাচ্ছে। Loop counter-এর না rule আছে, না reach — ওটাকে ছেড়ে দাও।

সত্যিকারের code example

তারিক তাদের codebase-এ যা পেল সেটা দেখো — TypeScript-এ, courier app সব কিছু primitive হিসেবে store করছে:

// Everything is a primitive. What could go wrong?
function createParcel(
  recipientName: string,
  address: string,          // one long line, like the envelope
  pinCode: string,          // hopefully six digits?
  codAmount: number,        // rupees? paise? who knows
  phone: string,
): Parcel {
  // every function must re-validate everything
  if (pinCode.length !== 6) throw new Error("Bad PIN");
  if (codAmount < 0) throw new Error("Bad amount");
  return { recipientName, address, pinCode, codAmount, phone };
}
 
function printLabel(p: Parcel): string {
  return p.recipientName + ", " + p.address + " - " + p.pinCode;
}
 
function chargeCod(p: Parcel, deliveryFee: number): number {
  // is deliveryFee in rupees or paise? the last bug was exactly this
  return p.codAmount + deliveryFee;
}
 
// Call site - spot the two bugs the compiler happily accepts:
const parcel = createParcel(
  "Rohan Mehta",
  "14 Lajpat Ngr near old tank Delhi",
  "98113",                 // five digits - runtime error at best
  50000,                   // meant Rs. 500.00 entered as paise
  "110024",                // phone and PIN swapped? both are strings!
);

এক এক করে সমস্যাগুলো দেখো:

  • Address একটা string, তাই app কখনো reliably city extract করতে পারবে না sorting-এর জন্য, বা check করতে পারবে না postal code আছে কিনা। ঠিক যেমন postman-কে জামাল ভাইয়ের খাম পড়ে অনুমান করতে হচ্ছিল।
  • Postal code check আছে createParcel-এর ভেতরে — কিন্তু updateAddress, importParcels, আর API endpoint সবাই আলাদা করে repeat করছে। একটা rule-এর পাঁচটা copy, ইতিমধ্যে drift শুরু করেছে।
  • codAmount bare number। একজন developer ভাবছে টাকা, আরেকজন পয়সা। Customer একশো গুণ বেশি charge হয়।
  • Compiler দেখতে পাচ্ছে না "110024" phone-এর জায়গায় বসানো হয়েছে। দুটোই string; দুটোই fit করে।
🚨

একই primitive type-এর দুটো parameter পাশাপাশি থাকলে সেটা transposition trap। Compiler কখনো swap ধরবে না। আলাদা type হলে এই runtime mystery compile-time error হয়ে যায়।

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

তারিকের main tool হলো Replace Data Value with Object: bare primitive-কে একটা ছোট class-এ promote করো যে নিজের rule নিজে ধরে রাখে।

ধাপ ১: Postal code-কে তার নিজের type দাও। Rule একটাই জায়গায় চলে আসে — constructor-এ — আর invalid postal code তৈরি করাই impossible হয়ে যায়।

class PinCode {
  private constructor(readonly value: string) {}
 
  static of(raw: string): PinCode {
    const cleaned = raw.trim();
    if (!/^[1-9][0-9]{5}$/.test(cleaned)) {
      throw new Error("PIN code must be exactly 6 digits: " + raw);
    }
    return new PinCode(cleaned);
  }
}

এই মুহূর্ত থেকে, যে কোনো function PinCode পেলে সে জানে এটা valid। আর কোথাও re-check করতে হবে না, কখনো না। তারিকদের পাঁচটা drifting copy এখন এই একটা constructor-এ মিলে গেল — আর "leading zero" নিয়ে বিতর্কটাও একটা code review-এ শেষ হলো।

ধাপ ২: টাকাকে এমন একটা type দাও যে তার unit জানে। আমরা internally পয়সায় store করব (whole number দিয়ে decimal rounding ঝামেলা এড়ানো যায়) কিন্তু edge-এ টাকায় কথা বলব।

class Money {
  private constructor(readonly paise: number) {}
 
  static fromRupees(rupees: number): Money {
    if (rupees < 0) throw new Error("Money cannot be negative");
    return new Money(Math.round(rupees * 100));
  }
 
  add(other: Money): Money {
    return new Money(this.paise + other.paise);
  }
 
  toString(): string {
    return "Rs. " + (this.paise / 100).toFixed(2);
  }
}

টাকা/পয়সার confusion এখন structurally impossible: raw পয়সা টাকা-expecting code-এ দেওয়ার কোনো উপায় নেই, কারণ দুই পাশেই শুধু Money exchange হচ্ছে। ৫০,০০০ টাকার bug আর হতেই পারবে না — কেউ careful বলে না, বরং carelessness-টাই compile হয় না।

ধাপ ৩: এক লাইনের address-কে structured type বানাও, আর সব একসাথে। Address আর scribbled envelope line না, সেটা এখন proper form। Street + city + postal code এক type-এ group করাটা Introduce Parameter Object-এর preview — আর আমাদের Data Clumps lesson-এরও।

class Address {
  constructor(
    readonly houseNo: string,
    readonly street: string,
    readonly city: string,
    readonly pin: PinCode,
  ) {}
 
  label(): string {
    return `${this.houseNo}, ${this.street}, ${this.city} - ${this.pin.value}`;
  }
}
 
function createParcel(
  recipientName: string,
  address: Address,
  codAmount: Money,
  phone: PhoneNumber,
): Parcel {
  // nothing to validate here - every input validated itself at birth
  return { recipientName, address, codAmount, phone };
}
 
function chargeCod(p: Parcel, deliveryFee: Money): Money {
  return p.codAmount.add(deliveryFee); // units can never mix
}

আগে আর পরে compare করো। createParcel-এ কোনো validation নেই — careless হওয়ার জন্য না, বরণ invalid input এখন পৌঁছাতেই পারে না। Phone/postal code swap এখন compile error। Label print করার logic আছে Address-এ, যেখানে থাকার কথা। প্রতিটা rule ঠিক একটাই জায়গায়।

Team demo-তে তারিক যে diagram দেখাল:

চিত্র ৭: Refactor করা parcel — প্রতিটা concept নিজেই self-validating value object

আরো দুটো tool kit complete করে। যখন "PREMIUM"-এর মতো magic string behavior select করছে, তখন Replace Type Code with Class ব্যবহার করো। যখন point[0], point[1]-এর মতো positional array record হওয়ার ভান করছে, তখন Replace Array with Object

চিত্র ৮: Validation সর্বত্র ছড়িয়ে বনাম value object-এ একটা জায়গায়

Deeper জানতে চাইলে: Value object immutable হওয়া উচিত: তৈরি হওয়ার পরে আর change হয় না। Immutability-ই value-based equality কে safe করে। আর add-এর মতো method নতুন instance return করে, যেটা ঠিক উপরের Money.add যেভাবে করে।

এই smell-এর জীবনচক্র

Primitive Obsession প্রতিটা codebase-এ একটা চেনা arc follow করে। বিপজ্জনক অংশটা মাঝখানে, যখন primitive-টা "convention" হয়ে যায়:

চিত্র ৯: একটা primitive-এর জীবনচক্র — quick convenience থেকে codebase convention, আর ফেরার পথ

Trap loop-টা দেখো: Bitten থেকে আবার Obsessed। আরো একটা scattered check দিয়ে symptom patch করা দায়িত্বশীল মনে হয়, কিন্তু সেটা obsession আরো গভীর করে। বের হওয়ার পথ হলো boundary rewrite: raw input-কে edge-এ একবার value object-এ parse করো, আর শুধু typed value-ই ভেতরে যাক।

C#-এ একই smell

C# record type দিয়ে value object খুব সংক্ষেপে লেখা যায়। এটা একটা school fee system-এর smelly version:

public void RecordFeePayment(string studentId, decimal amount, string currency)
{
    if (amount <= 0) throw new ArgumentException("Bad amount");
    if (currency != "INR") throw new ArgumentException("Only INR supported");
    // ... save payment
}

আর এটা clean version, যেখানে Rupees নিজেকে নিজে guard করে:

public readonly record struct Rupees
{
    public decimal Amount { get; }
 
    public Rupees(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
        Amount = amount;
    }
 
    public static Rupees operator +(Rupees a, Rupees b) => new(a.Amount + b.Amount);
    public override string ToString() => $"Rs. {Amount:N2}";
}
 
public void RecordFeePayment(StudentId studentId, Rupees amount)
{
    // nothing to check - a Rupees value is valid by construction
    // ... save payment
}

record struct value-based equality free-তে দেয় — দুটো Rupees(500) value সমান, ঠিক যেমন দুটো পাঁচশো টাকার নোট। StudentId type-টাও দেখো: কোনো format rule না থাকলেও ID-র নিজের wrapper আছে, শুধু OrderId-এর সাথে confuse না হওয়ার জন্য।

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

  • Raw string বা int হিসেবে ID। userId, orderId, productId সব string — যতদিন না কেউ order ID দিয়ে user lookup করে। Strongly-typed ID wrapper DDD-style codebase-এ এর famous cure।
  • double বা float হিসেবে টাকা। দ্বিগুণ বিপজ্জনক: currency নেই আর binary floating point 0.1 exactly represent করতে পারে না, তাই লম্বা calculation-এ পয়সা leak করে। Finance code এই দুটো কারণেই decimal-based Money type ব্যবহার করে।
  • String হিসেবে status আর type code। "ACTIVE", "PENDING", "premium" বনাম "PREMIUM" — একটা casing mistake আর comparison চুপচাপ fail করে।
  • Number হিসেবে date আর duration। timeout = 30 — second না millisecond? এই একটা প্রশ্নে পুরো bug category লুকিয়ে আছে; আধুনিক API Duration/TimeSpan type pass করে।
  • API boundary আর DTO। JSON থেকে data আসে string আর number হিসেবে — edge-এ ঠিকই আছে, কিন্তু সেই raw value business logic-এ ঢুকে পড়লে smelly। Well-structured project (domain-driven-hexagon guide দেখো) boundary-তেই primitive-কে value object-এ convert করে।
  • Scientific আর engineering software। Mars Climate Orbiter-এর গল্পটা canonical warning: unit-ছাড়া number team boundary cross করলে মহাকাশযান ধ্বংস হয়।
ℹ️

তোমার type checker হলো সবচেয়ে সস্তা test suite যেটা তুমি কখনো পাবে। Value object যতগুলো swap, unit mix-up, আর invalid value ঠেকায় — সেগুলো unit test যেটা তোমাকে কখনো লিখতে, run করতে, বা maintain করতে হবে না।

কখন ignore করা যায়

পরিস্থিতিIgnore করবে?কারণ
Loop counter, array index, temporary flagহ্যাঁকোনো rule নেই, domain meaning নেই — wrap করলে শুধু ceremony বাড়বে
অনেক module জুড়ে validation-সহ valueনাRule একটা জায়গায় আনলে অনেকবার payoff পাওয়া যায়
ছোট script বা throwaway prototypeহ্যাঁDesign এত দিন টিকবে না যে payoff পাওয়া যাবে
Long-lived system-এ টাকা, ID, email, phoneনাএগুলো classic, সবচেয়ে বেশি payoff-এর candidate
Performance-critical inner loop, measure করার পরেমাঝে মাঝেWrapper allocation খুব কম ক্ষেত্রে matter করে — কিন্তু struct/record সাধারণত free
একটা function-এ, একবার, কোনো rule ছাড়া valueহ্যাঁInvariant নেই এমন value-এর wrapper কিছুই protect করে না

Honest rule: primitive wrap করো যখন value-এর enforce করার rule আছে, নিজস্ব behaviour আছে, বা নাম দেওয়ার মতো meaning আছে। Payoff বাড়ে সাথে সাথে value কতটা দূরে দূরে যায় — ঠিক চিত্র ৬-এর quadrant-এ যেমন দেখানো।

কোন refactoring দিয়ে সারাবে

Refactoringকখন ব্যবহার করবে
Replace Data Value with Objectমূল cure — bare primitive-কে value object-এ promote করো যে নিজের validation আর behaviour ধরে রাখে
Introduce Parameter Objectসবসময় একসাথে চলা primitive (amount + currency) এক type হয়
Replace Type Code with ClassCategory select করা magic string বা int real type হয়
Replace Array with ObjectPositional array (p[0] হলো x, p[1] হলো y) named field-সহ object হয়
Extract Classএকটা class-এ থাকা related primitive-গুলো Address-এর মতো structured component হয়

এক নজরে পুরো smell

তারিকের final whiteboard sketch — team-এর অর্ধেক সেটার ছবি তুলে রেখেছিল:

চিত্র ১০: Primitive Obsession এক নজরে — লক্ষণ, কারণ, খরচ, আর সমাধান

Quick revision

+------------------------------------------------------------------+
|              PRIMITIVE OBSESSION - CHEAT SHEET                   |
+------------------------------------------------------------------+
| What     : Raw strings/numbers playing the role of rich          |
|            concepts (the address scribbled on one line)          |
| Family   : Bloaters                                              |
| Spot it  : string email, repeated validation, magic type         |
|            codes, unit mix-ups, positional arrays                |
| Costs    : Duplicated rules, no compiler safety, lost            |
|            domain language, invalid states travel freely         |
| Main fix : Replace Data Value with Object (value objects)        |
| Helpers  : Introduce Parameter Object, Replace Type Code,        |
|            Replace Array with Object                             |
| Ignore   : Loop counters, throwaway scripts, rule-free values    |
| Mantra   : "Make invalid states unrepresentable."                |
+------------------------------------------------------------------+

Practice করো

তারিকের juniors-দের জন্য homework — আর তোমার জন্যও। এই ticket booking function primitive-এ ডুবে আছে। এটাকে rescue করো!

function bookTicket(
  passengerName: string,
  age: number,
  from: string,        // station code like "NDLS"
  to: string,          // station code like "BCT"
  farePaise: number,   // careful - paise, not rupees!
  mobile: string,
): string {
  if (age < 0 || age > 120) throw new Error("Bad age");
  if (from.length !== 4 || to.length !== 4) throw new Error("Bad station code");
  if (mobile.length !== 10) throw new Error("Bad mobile");
  if (farePaise < 0) throw new Error("Bad fare");
 
  const discounted = age >= 60 ? farePaise * 0.6 : farePaise;
  return passengerName + ": " + from + " -> " + to + ", Rs." + discounted / 100;
}
 
// Spot the danger at this call site:
bookTicket("Meera", 65, "BCT", "NDLS", 145000, "9876543210");
// Did the caller mean Rs. 1450 or Rs. 14.50? And are from/to in the right order?

তোমার কাজ:

১. Value object বানাও: Age, StationCode, Money (একটা fromRupees factory সহ), আর MobileNumber। প্রতিটা তার constructor-এ নিজেকে validate করবে। ২. এই type গুলো accept করার জন্য bookTicket rewrite করো। ভেতরে কতটুকু validation line বাকি থাকে? ৩. Senior citizen discount-টা Money type-এ বা Fare type-এ withDiscount(percent) method হিসেবে নিয়ে যাও। ৪. Bonus চিন্তা: from আর to দুটোই StationCode — type alone এদের swap ঠেকাতে পারবে না। কী করতে পারো? Hint: named field-সহ একটা Route object — যেটা আসলে একটা ছোট্ট Data Clump fix। সেটাই আমাদের পরের পরের lesson-এর topic।

তোমার final bookTicket-এ যদি শূন্যটা validation line থাকে আর সেটা plain English-এর মতো পড়া যায়, তাহলে তুমি obsession থেকে বের হয়ে এসেছ — আর জামাল ভাইয়ের ফিরে আসা বিশটা কার্ডের মতো না, তোমার parcel সবসময় ঠিক জায়গায় পৌঁছাবে।

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

'Primitive' মানে আসলে কী?
Primitive মানে হলো language-এর basic built-in type — string, number, boolean, int, decimal, এসবের array, এই রকম। এগুলো হলো programming-এর কাঁচামাল। সমস্যা হয় যখন আমরা এই কাঁচামাল দিয়েই সব কাজ সারি — যেটার আসলে নিজস্ব রূপ থাকা দরকার ছিল, যেমন email address, টাকার পরিমাণ, বা phone number।
তাহলে কি প্রতিটা value-কে class-এ wrap করতে হবে?
না! Loop counter-এ int ঠিকই আছে। Wrap করো শুধু তখন, যখন কোনো value-এর নিজস্ব rule (validation) আছে, নিজস্ব behavior (operation) আছে, বা domain-এ তার আলাদা meaning আছে। Money amount যেটা pricing, tax, billing সব জায়গায় ঘুরছে — সেটার নিজের type দরকার। কিন্তু একটা temporary index-এর দরকার নেই।
'Value object' কী জিনিস?
Value object হলো একটা ছোট type, যেটা তার value দিয়ে define হয় — কোনো identity দিয়ে নয়। ৫০ টাকার দুটো Money object সমান আর বদলযোগ্য, ঠিক যেমন দুটো পঞ্চাশ টাকার নোট। Value object তৈরির সময়েই নিজেকে validate করে নেয়, তাই invalid value কখনো exist করতেই পারে না।
Primitive wrap করলে কি program slow হয়?
খরচটা এতটাই কম যে বেশিরভাগ সময় বোঝাই যায় না। অনেক language-এ সস্তা wrapper আছে — C#-এ struct আর record, TypeScript-এ type আর class। টাকা আর পয়সা মিলিয়ে ফেলার bug ঠেকাতে যে খরচ হয়, সেটা কয়েক nanosecond-এর চেয়ে অনেক বেশি।
এই smell-এর সাথে জড়িত বাস্তব বিপর্যয়ের কোনো উদাহরণ আছে?
১৯৯৯ সালে NASA-র Mars Climate Orbiter মহাকাশযান হারিয়ে যায়, কারণ একটা software pound-second-এ thruster data পাঠাচ্ছিল আর অন্যটা newton-second আশা করছিল। দুটোই plain number — কোনো unit নেই। Unit-সহ type থাকলে ভুলটা ধরা পড়ত। Mission-এর খরচ ছিল ৩০ কোটি ডলারের বেশি।

আরো দেখো

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

Data Clumps: যে বন্ধুরা সবসময় একসাথে ঘোরে

শিক্ষার্থীদের জন্য Data Clumps code smell — শেখো কীভাবে সবসময় একসাথে চলা value-এর গ্রুপ চেনা যায় আর সেগুলোকে একটা class-এ bundle করা যায়, ঠিক যেমন একটা student ID card।

আরও পড়ুন

Long Parameter List: দশটা নির্দেশনার চায়ের অর্ডার

Long Parameter List কোড স্মেল সহজ ভাষায় — কেন বেশি argument-এর method বাগ তৈরি করে, আর কীভাবে parameter object দিয়ে call ছোট, পরিষ্কার আর নিরাপদ করা যায়।

আরও পড়ুন

Large Class: যে স্কুলের ব্যাগে সব কিছু থাকে

Large Class code smell কী সেটা বুঝো — কেন god class বড় হয়, low cohesion কীভাবে চেনা যায়, আর Extract Class দিয়ে কীভাবে ছোট ছোট focused class-এ ভাগ করা যায়।

আরও পড়ুন

Replace Data Value with Object: তোমার Data-কে একটা নিজের ঘর দাও

Replace Data Value with Object সহজভাবে বোঝানো — কীভাবে একটা plain string বা number-কে validation আর behaviour সহ একটা ছোট class-এ রূপান্তর করতে হয়। TypeScript আর C# record-এর উদাহরণ দিয়ে।

আরও পড়ুন