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

Replace Array with Object: প্রতিটি জায়গাকে একটা নাম দাও

Replace Array with Object সহজ ভাষায় — কেন row[0], row[1], row[2]-এর মতো গোপন position-ওয়ালা array bug তৈরি করে, আর কীভাবে named field সহ একটা class কোডকে সৎ আর নিরাপদ করে তোলে।

22 মিনিট আপডেট: June 11, 2026beginner
refactoringreplace array with objectorganizing dataprimitive obsessionnamed fieldstypescriptcsharp

ফ্রিজে রহস্যময় লিস্ট

ধরো, মিরপুরের একটা বাসায় ফ্রিজের গায়ে একটা ছোট whiteboard আছে। সোমবার সকালে নাসরিন ভাবি সেখানে লেখা দেখলেন:

জামাল, ৭, নীল

তিনি থামলেন। ৭ মানে কী? জামালের class? তার roll number? টিফিনে কটা রুটি দিতে হবে? আর নীল — এটা কি sports day-তে তার house colour, নাকি ইস্ত্রি করার uniform-এর রঙ, নাকি গত সপ্তাহে হারিয়ে যাওয়া water bottle-এর রঙ?

তিনি জামালের বাবা সালাম ভাইকে ফোন করলেন। সালাম ভাই তখন রিকশায়। ট্র্যাফিকের ভেতর চিৎকার করে বললেন, "আরে আমি লিখেছিলাম! প্রথমটা নাম, দ্বিতীয়টা class, তৃতীয়টা sports house। স্কুল ফোন করেছিল — বার্ষিক দিনের form-এর জন্য।" ঠিক আছে — কিন্তু সেই নিয়মটা শুধু সালাম ভাইয়ের মাথায়। whiteboard নিজে কিছুই বলছে না।

নাসরিন ভাবি সঠিকভাবে form পূরণ করলেন কারণ তিনি জিজ্ঞেস করেছিলেন। কিন্তু বুধবার জামালের দাদি রাহেলা বেগম টিফিন গোছাতে গিয়ে একই board পড়লেন। তিনি সালাম ভাইয়ের গোপন নিয়ম জানেন না। তাঁর কাছে "জামাল, ৭, নীল" মানে স্পষ্টতই: জামাল ৭টা রুটি চায়, নীল টিফিন বাক্সে। তাই জামাল দুপুরে ব্যাগ খুলে দেখে সাতটা রুটি টাওয়ারের মতো সাজানো। বন্ধুরা এক সপ্তাহ হাসে। তারা তাকে "সাত রুটি জামাল" ডাকা শুরু করে।

ব্যাপারটা হলো, এই গল্পে কেউই অসাবধান ছিল না। দাদি মনোযোগ দিয়ে board পড়েছেন। সালাম ভাই সত্যিকারের তথ্যই লিখেছেন। data পুরো সময় সঠিক ছিল। কিন্তু format ব্যর্থ হয়েছিল — প্রতিটা item-এর অর্থ ছিল একজন মানুষের স্মৃতিতে, board-এ নয়।

এখন কল্পনা করো ফ্রিজে একটা proper label card লাগানো:

নাম:    জামাল
ক্লাস:  ৭
হাউস:   নীল

একই তথ্য। শূন্য বিভ্রান্তি। যে কেউ — নাসরিন ভাবি, দাদি, বেড়াতে আসা খালা — প্রথম চেষ্টায় সঠিকভাবে পড়তে পারবেন। কারণ প্রতিটা মান তার নিজের নাম বহন করছে। কোনো গোপন নিয়ম জানার নেই, তাই ভুলে যাওয়ার কিছু নেই।

আমাদের program-গুলোও প্রতিদিন এই দুটো পছন্দের মুখোমুখি হয়। হয় mixed information একটা array-এ রাখি যেখানে position 0 গোপনে "name" মানে আর position 1 গোপনে "class" মানে। অথবা একটা object-এ রাখি যেখানে প্রতিটা মান একটা proper label-এর পেছনে বসে। যে refactoring আমাদের cryptic whiteboard থেকে label card-এ নিয়ে যায় তাকে বলে Replace Array with Object

চিত্র ১: ফ্রিজের লিস্টের যাত্রা — একই data বিভ্রান্তি থেকে স্পষ্টতায় যায়

Replace Array with Object কী?

Replace Array with Object হলো সেই array-গুলোর জন্য একটা refactoring যেগুলো আসলে list নয়। এটা তখন লাগে যখন কোনো array-কে একটা form-এর মতো ব্যবহার করা হচ্ছে — slot 0-এ এক ধরনের তথ্য, slot 1-এ ভিন্ন ধরন, slot 2-এ আরেকটা। আমরা এই ধরনের array-কে একটা object (সাধারণত একটা ছোট class) দিয়ে বদলে দিই যেখানে প্রতিটা slot-এর জন্য একটা named field থাকে।

একটু ভাবো, array আসলে কী বলতে চায়। একটা array ঘোষণা করে: "আমি একই ধরনের জিনিসের একটা sequence।" marks-এর list। customers-এর queue। তাপমাত্রার সারি। প্রতিটা element একই ধরনের জিনিস, আর position শুধু বলে কোনটা, কী নয়।

কিন্তু কখনো কখনো programmer-রা array-কে সস্তা record হিসেবে অপব্যবহার করে:

  • row[0] হলো team-এর নাম
  • row[1] হলো জয়ের সংখ্যা
  • row[2] হলো হারের সংখ্যা

এখানে position অর্থ বহন করছে। array নিজের প্রকৃতি সম্পর্কে মিথ্যা বলছে — দেখতে একই ধরনের item-এর list মনে হচ্ছে, কিন্তু আসলে তিনটা ভিন্ন field সহ একটা structure। Martin Fowler তাঁর Refactoring বইয়ে এই refactoring catalog করেছেন। Refactoring Guru সুন্দরভাবে বলেছেন — এটা ডাকঘরের বাক্স ব্যবহার করার মতো, box 1-এ username আর box 14-এ address রাখার মতো। কেউ ভুল box-এ কোনো মান রাখলে জিনিসগুলো বিভ্রান্তিকরভাবে ব্যর্থ হবে।

চিত্র ২: দুটো পথ — গোপন-position array অনুমান করতে বাধ্য করে; named object অনুমান সম্পূর্ণ দূর করে
💡

দুই সেকেন্ডে একটা দ্রুত test করো: জিজ্ঞেস করো, "আমি যদি এই array shuffle করি, তাহলেও কি এর মানে থাকবে?" marks-এর list shuffle করলেও টিকে যায় — এটা সত্যিকারের list। কিন্তু ["জামাল", "", "নীল"] shuffle করলে ["নীল", "জামাল", ""] হয় — সম্পূর্ণ অর্থহীন। Shuffle করলে যদি অর্থ নষ্ট হয়, তাহলে array আসলে ছদ্মবেশী record, আর এটা object হতে চায়।

Refactoring-এর পর data নিজেই নিজেকে বর্ণনা করে। team.wins-কে team.losses ভেবে ভুল করা যায় না। Compiler জানে wins একটা number আর name একটা string। আর নতুন object winRate() method-এর মতো behaviour-এর জন্য প্রাকৃতিক বাড়ি হয়ে ওঠে।

কলেজ কর্নার: type-theory-র ভাষায়, এই refactoring তোমাকে positional encoding থেকে nominal encoding-এ নিয়ে যায়। string-এর একটা array শুধু বলে "এখানে n টা string আছে" — এর type role সম্পর্কে শূন্য তথ্য বহন করে। একটা class প্রতিটা role-কে type-এরই অংশ হিসেবে ঘোষণা করে, তাই compiler প্রতিটা ব্যবহারের জায়গায় "wins সবসময় একটা number" এর মতো তথ্য prove করতে পারে। একটা মাঝামাঝি পথ আছে: TypeScript-এ [string, number, number]-এর মতো tuple type per-position type checking দেয় কিন্তু তবুও কোনো নাম নেই। Tuple হলো position সহ structural typing; class আর interface তোমাকে named structure দেয়। বেশিরভাগ software engineering course-এ শেখানো rule of thumb এখানে প্রযোজ্য: কোনো data যত দূরে তৈরি হওয়ার জায়গা থেকে ভ্রমণ করে, তার অর্থ তত বেশি এর সাথে যেতে হবে — আর নামই হলো অর্থ বহন করার উপায়।

কখন এটার দরকার হয়?

তোমার codebase-এ এই লক্ষণগুলো দেখো:

  1. Position ব্যাখ্যা করে comment। যদি দেখো // row[1] = wins, row[2] = losses, code নিজের অপরাধ স্বীকার করছে। সৎ code-এর decoder ring লাগে না।
  2. সব কিছু একটা type-এ জোর করে ঢোকানো। বেশিরভাগ typed language-এ array এক ধরনের element ধরে। তাই class number 7 string "7" হিসেবে save হয়, আর প্রতিটা পাঠককে মনে রাখতে হয় Number(...)-দিয়ে ফিরিয়ে আনতে। ভুলে যাওয়া conversion গোপন bug তৈরি করে — JavaScript-এ "7" + 1 হলো "71", 8 নয়।
  3. Index bug। কেউ row[1] পড়ে যখন মানে করেছিল row[2]। কেউ মাঝখানে নতুন field insert করে আর তার পরের সব index shift হয়ে যায়। Compiler এর কিছুই ধরতে পারে না কারণ সব slot তার কাছে একই দেখায়।
  4. Primitive Obsession smell। এই refactoring হলো Primitive Obsession-এর standard cure-গুলোর একটা — সমৃদ্ধ domain concept-এর জন্য raw built-in type (এখানে, একটা raw array) ব্যবহার করার অভ্যাস যা নিজের shape পাওয়ার যোগ্য।
  5. Data যেগুলো একসাথে চলে। একই তিনটা মান সবসময় একসাথে তৈরি হয়, একসাথে pass হয়, আর একসাথে পড়া হয় — তারা একটা concept। Concept-গুলো নাম পাওয়ার যোগ্য।

আর একটা স্পষ্ট লক্ষণ যে তোমার এটা দরকার নেই: array-তে অনেক একই জিনিস আছে। পঞ্চাশটা marks-এর list array হিসেবেই থাকুক। Replace Array with Object শুধু ছদ্মবেশী array-গুলোর জন্য।

নিচের quadrant decide করার সময় একটা কার্যকর mental map। দুটো প্রশ্ন: slot-গুলো কি ভিন্ন জিনিস মানে, আর data কতদূর ভ্রমণ করে?

চিত্র ৩: কোনো array object হওয়ার যোগ্য কিনা তা নির্ধারণ করা

দেখো, ফ্রিজের list বিপদের কোণে — mixed মানে, পুরো পরিবার ব্যবহার করে। তাই এটাকে label card হতে হবে। marks-এর list অনেক দূরেও যায়, কিন্তু প্রতিটা slot একই জিনিস মানে, তাই সুস্থ array হিসেবেই থাকে। একটা ছোট function-এ ব্যবহৃত mixed pair tuple হিসেবে টিকতে পারে। মোটকথা, যেখানে মানে mixed আর data ভ্রমণ করে সেখানে refactor করো।

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

ফ্রিজ-whiteboard সমস্যাটা code হিসেবে লেখা। আগে — একটা school প্রতিটা student record-কে plain string array হিসেবে রাখছে:

// BEFORE: position is a secret rule
// record[0] = name, record[1] = class, record[2] = sports house
const record: string[] = ["Ravi", "7", "Blue"];
 
function makeIdCard(record: string[]): string {
  // Hope everyone remembers the secret order...
  return `${record[0]} | Class ${record[1]} | ${record[2]} House`;
}
 
function isSenior(record: string[]): boolean {
  return Number(record[1]) >= 6;   // must remember: [1] is class, and it's a string!
}

পরে — named, properly typed field সহ object হিসেবে একই data:

// AFTER: every value carries its own label
class StudentRecord {
  constructor(
    public readonly name: string,
    public readonly studentClass: number,
    public readonly house: string,
  ) {}
 
  isSenior(): boolean {
    return this.studentClass >= 6;
  }
 
  idCardLine(): string {
    return `${this.name} | Class ${this.studentClass} | ${this.house} House`;
  }
}
 
const ravi = new StudentRecord("Ravi", 7, "Blue");
ravi.isSenior();      // true — no Number() conversion, no index guessing
ravi.idCardLine();    // "Ravi | Class 7 | Blue House"

তিনটা জিনিস লক্ষ্য করো। Class এখন সত্যিকারের number, number পোশাক পরা string নয়। Senior check object-এর ভেতরে গেছে, যে data ব্যবহার করে তার পাশে। আর কাউকে কোথাও মনে রাখতে হবে না position 1 মানে কী, কারণ position 1 আর নেই।

চিত্র ৪: ফলাফলের আকৃতি — একটা ছোট class যেখানে প্রতিটা slot named, typed member হয়ে গেছে

দুটো storage style পাশাপাশি তুলনা করো:

প্রশ্নছদ্মবেশী arrayNamed object
Slot 1 মানে কী?শুধু সালাম ভাই জানেনField-এর নামই studentClass
Class number-এর type কী?Number সাজা stringCompiler-এর check করা real number
Compiler কি mix-up ধরতে পারে?না — সব slot এক দেখায়হ্যাঁ — name আর wins ভিন্ন field ও type
Calculation কোথায় থাকে?বাইরের function-এ ছড়িয়েযে data ব্যবহার করে তার পাশে method হিসেবে
নতুন field যোগ হলে কী হয়?এর পরের সব index গোপনে shift হয়পুরানো field-গুলো নাম ধরে রাখে; কিছু shift হয় না

ধাপে ধাপে, নিরাপদ উপায়ে

Refactoring ব্যস্ত রাস্তা পার হওয়ার মতো — কখনো দৌড়িও না, একবারে একটা নিরাপদ পদক্ষেপ নাও, আর প্রতিটা পদক্ষেপের পরে দুই দিক দেখো (test চালাও)। Fowler যে mechanics বর্ণনা করেছেন তার উপর ভিত্তি করে নিরাপদ পথটা এখানে।

পদক্ষেপ ১: নতুন class তৈরি করো, শুরুতে খালি।

class StudentRecord {
  // fields will arrive one by one
}

পদক্ষেপ ২: সবচেয়ে সহজ slot বেছে নাও আর তার জন্য named field যোগ করো। সাধারণত slot 0। পুরানো array থেকে object তৈরির উপায় সহ field যোগ করো, যাতে পুরানো আর নতুন কিছুক্ষণ পাশাপাশি থাকতে পারে:

class StudentRecord {
  constructor(public readonly name: string) {}
 
  static fromArray(record: string[]): StudentRecord {
    return new StudentRecord(record[0]);
  }
}

পদক্ষেপ ৩: সেই একটা slot-এর read-গুলো caller-এ caller-এ বদলাও। যেখানেই code record[0] বলে, student.name ব্যবহার করতে বদলে দাও। প্রতিটা পরিবর্তনের পরে compile করো আর test চালাও। Program প্রতিটা মুহূর্তে কাজ করে — এটাই baby step-এ এগোনোর মূল কথা।

পদক্ষেপ ৪: বাকি প্রতিটা slot-এর জন্য repeat করো। studentClass যোগ করো (fromArray-এর ভেতরে "7"-কে number 7-এ convert করো, যাতে conversion ঠিক এক জায়গায় হয়), তারপর house। Caller-গুলো একে একে update করো।

class StudentRecord {
  constructor(
    public readonly name: string,
    public readonly studentClass: number,
    public readonly house: string,
  ) {}
 
  static fromArray(record: string[]): StudentRecord {
    return new StudentRecord(record[0], Number(record[1]), record[2]);
  }
}

পদক্ষেপ ৫: Array form মুছে দাও। যখন কোনো code আর কোনো index পড়ে না, creation site-গুলো বদলাও সরাসরি StudentRecord বানাতে আর fromArray মুছে দাও যদি বাইরে কেউ না লাগে। Whiteboard মুছে গেছে; শুধু label card থাকে।

পদক্ষেপ ৬: নতুন class-এ behaviour নিয়ে যাও। Record নিয়ে কিছু compute করে এমন function-গুলো খোঁজো — isSenior, idCardLine — আর সেগুলোকে method হিসেবে ভেতরে নিয়ে যাও। এই পদক্ষেপটাই একটা mere data bundle-কে real object-এ পরিণত করে।

চিত্র ৫: State machine হিসেবে migration — program প্রতিটা state-এ compile ও run করে
⚠️

এই refactoring-এর সবচেয়ে বিপজ্জনক মুহূর্ত হলো type conversion। Array-তে class ছিল string "7"; object-এ এটা number 7 হয়। কোনো caller যদি গোপনে string behaviour-এর উপর নির্ভর করে থাকে (যেমন message-এ "7" জোড়া লাগানো), সে ভিন্নভাবে আচরণ করতে পারে। ঠিক এক জায়গায় conversion করো, migrate করা প্রতিটা caller-এর পরে test চালাও, আর কখনো একসাথে সব caller বদলিও না।

Caller আর data-এর মধ্যে কথোপকথনের পার্থক্য, আগে আর পরে। আগে, caller একটা নীরব array-কে জিজ্ঞাসাবাদ করে আর নিজেই সব অর্থ সরবরাহ করে। পরে, object নাম ধরে উত্তর দেয়:

চিত্র ৬: আগে, caller অর্থ বহন করে; পরে, অর্থ data-র সাথে ভ্রমণ করে

কলেজ কর্নার: লক্ষ্য করো যে পদক্ষেপ ১–৪ দুটো representation একসাথে জীবিত রাখে, fromArray factory দিয়ে সংযুক্ত। এটা একটা সাধারণ migration pattern যাকে adapter at the boundary বলে — database schema migration আর API version upgrade-এর জন্যও একই কৌশল। পুরানো format আর নতুন format একসাথে থাকে; একবারে একটা করে caller নতুনে যায়; bridge সবশেষে মুছে যায়। কিছু ভুল হলে তোমার কাছে একটা ছোট, revertible পরিবর্তন আছে — চল্লিশটা file-এর catastrophe-র বদলে। Professional team-গুলো এই giant-leap alternative-কে "big bang migration" বলে, আর তারা এটা ঠিক সেই tone-এ বলে যেটায় দাদি "সাত রুটি" বলেন।

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

ধরো, রহিমের বড় বোন সুমাইয়া computer engineering পড়ে আর স্থানীয় একটা cricket academy-র জন্য software লেখে। বাড়িতে বেড়াতে এসে সাত রুটির গল্প শুনে হাসে — তারপর চুপ হয়ে যায়। কারণ সে সবে বুঝলো তার league table code-এ একদম একই রোগ আছে। পুরানো code প্রতিটা team-কে array হিসেবে রাখে, ঠিক spreadsheet row-এর মতো:

// BEFORE
// row[0] = team name, row[1] = wins, row[2] = losses, row[3] = points (all strings!)
const table: string[][] = [
  ["Nagpur Strikers", "12", "4", "24"],
  ["Wardha Warriors", "9", "7", "18"],
  ["Amravati Aces", "9", "7", "19"],   // bug: points should be 18, who will notice?
];
 
function printTable(table: string[][]): void {
  for (const row of table) {
    const rate = Number(row[1]) / (Number(row[1]) + Number(row[2]));
    console.log(`${row[0]}: ${row[3]} pts (win rate ${rate.toFixed(2)})`);
  }
}
 
function topTeam(table: string[][]): string {
  let best = table[0];
  for (const row of table) {
    if (Number(row[3]) > Number(best[3])) best = row;
  }
  return best[0];
}

প্রতিটা function একই Number(...) conversion বারবার করছে। প্রতিটা function আবার column order মনে রাখছে। Amravati row-তে bug (জয়ের সাথে মিলছে না এমন point) চুপচাপ বসে আছে কারণ structure কোথাও বলে না "point মানে wins গুণ দুই"। গত মৌসুমে ঠিক এই ধরনের row ভুল team-কে playoffs-এ পাঠিয়েছিল। Coach তারিককে দুই রাগান্বিত team captain-এর কাছে ক্ষমা চাইতে হয়েছিল। কেউ বলতেই পারছিল না কখন ভুল সংখ্যাটা type হয়েছিল।

Replace Array with Object-এর পরে:

// AFTER
class TeamStanding {
  constructor(
    public readonly name: string,
    public readonly wins: number,
    public readonly losses: number,
  ) {
    if (wins < 0 || losses < 0) {
      throw new Error("Wins and losses cannot be negative");
    }
  }
 
  get points(): number {
    return this.wins * 2;          // derived, so it can NEVER disagree with wins
  }
 
  get winRate(): number {
    const played = this.wins + this.losses;
    return played === 0 ? 0 : this.wins / played;
  }
 
  summary(): string {
    return `${this.name}: ${this.points} pts (win rate ${this.winRate.toFixed(2)})`;
  }
}
 
const table: TeamStanding[] = [
  new TeamStanding("Nagpur Strikers", 12, 4),
  new TeamStanding("Wardha Warriors", 9, 7),
  new TeamStanding("Amravati Aces", 9, 7),   // points are computed: bug impossible
];
 
function printTable(table: TeamStanding[]): void {
  for (const team of table) console.log(team.summary());
}
 
function topTeam(table: TeamStanding[]): string {
  return table.reduce((best, t) => (t.points > best.points ? t : best)).name;
}

কী উন্নতি হলো সেটা মনোযোগ দিয়ে দেখো:

  • বাইরের array array-ই থাকলো — আর সঠিকভাবেই থাকলো, কারণ এটা একই ধরনের জিনিসের (team) সত্যিকারের list। শুধু ভেতরের ছদ্মবেশী array object হলো।
  • Points column সম্পূর্ণ গেল। এটা derived data ছিল, আর getter হিসেবে এটা কখনো wins থেকে আলাদা হতে পারবে না। Amravati bug শুধু fix হয়নি — এটা এখন অপ্রতিনিধিত্বযোগ্য।
  • সব Number(...) noise গেল। তৈরির সময় থেকেই type সঠিক।
  • Constructor এখন validate করে। একটা array slot -5 রাখলে খুশিমনে রাখতো; object রাখতে অস্বীকার করে।

কলেজ কর্নার: "bug অপ্রতিনিধিত্বযোগ্য" হলো মূল কথা। points column ছিল wins-এ ইতিমধ্যে থাকা তথ্যের একটা denormalised copy — আর যেকোনো নকল তথ্য তার উৎস থেকে দূরে সরে যেতে পারে। Points-কে derived getter বানিয়ে, সুমাইয়া একটা invariant enforce করলো: প্রতিটা মুহূর্তে, প্রতিটা team-এর জন্য, point সমান wins গুণ দুই। যে invariant তুমি compute করো সেটা লঙ্ঘন করা যায় না; যে invariant তুমি শুধু document করো সেটা deadline-এর আগের শুক্রবার সন্ধ্যায় লঙ্ঘন হয়। এই ধারণা — "illegal state অপ্রতিনিধিত্বযোগ্য করে তোলো" — functional programming জগৎ থেকে এসেছে কিন্তু তুমি যে কোনো language-এ কাজ করো না কেন, সেখানেই প্রযোজ্য।

Rewrite-এর পরে সুমাইয়া একটা term ধরে academy bug report track করলো। সে coach তারিককে যে chart দেখালো:

চিত্র ৭: Academy-তে index ও column mix-up bug, refactoring-এর আগে ও পরে

মাসে ছয়টা bug শুনতে নাটকীয়, কিন্তু প্রতিটাই ছিল ছোট আর অপমানজনক: নতুন report-এ একটা swapped column, ভুলে যাওয়া Number(...)-এর কারণে "9" + "7" ছাপা হচ্ছে 97, মাঝখানে নতুন column insert করায় পরের সব index shift। ছয়টাই একই পরিবারের, আর পুরো পরিবার একসাথে বিলুপ্ত হলো। কারণ তাদের যে রোগ ছিল — positional meaning — সেটা আর নেই।

Academy committee যখন সুমাইয়াকে জিজ্ঞেস করলো পুরানো table কেন বারবার ভাঙছিল, সে code স্পর্শ করা সবাইকে একটা দ্রুত survey করলো: "row[1] মানে কী ছিল বলে মনে করেছিলে?" উত্তরগুলো সব ব্যাখ্যা করে:

চিত্র ৮: বিভিন্ন developer slot 1 মানে কী ভেবেছিল — সবাই নিশ্চিত ছিল, বেশিরভাগ ভুল ছিল

চল্লিশ শতাংশ সঠিক ছিল। বাকিরা সাত রুটি নিয়ে দাদির মতো — সতর্ক মানুষ, এমন একটা format দ্বারা বিভ্রান্ত যা নিজেকে ব্যাখ্যা করতে অস্বীকার করে।

Python-এ এক নজর

একই refactoring প্রতিটা language-এ আছে। Python-এ "before" সাধারণত position অনুযায়ী unpack করা list বা tuple-এর মতো দেখায়, আর "after" হলো dataclass — Python-এর named, typed field declare করার lightweight উপায়:

# BEFORE: positional bundle, all meaning is in your memory
row = ["Nagpur Strikers", "12", "4"]
rate = int(row[1]) / (int(row[1]) + int(row[2]))
 
# AFTER: a dataclass with named, typed fields
from dataclasses import dataclass
 
@dataclass(frozen=True)
class TeamStanding:
    name: str
    wins: int
    losses: int
 
    @property
    def points(self) -> int:
        return self.wins * 2
 
    @property
    def win_rate(self) -> float:
        played = self.wins + self.losses
        return 0.0 if played == 0 else self.wins / played
 
team = TeamStanding(name="Nagpur Strikers", wins=12, losses=4)
print(f"{team.name}: {team.points} pts ({team.win_rate:.2f})")

দুটো জিনিস লক্ষ্য করার মতো। frozen=True object-টাকে immutable করে, TypeScript-এর readonly field-এর মতো — একবার তৈরি হলে, standing চুপচাপ edit করা যাবে না। আর construction site-এ keyword argument (wins=12, losses=4) call-টাকে ফ্রিজের label card-এর মতো পড়ায়। Python programmer-রা যারা এটা এড়িয়ে tuple pass করে ঘুরে, তারা শেষমেশ তাদের নিজস্ব সাত রুটির সকাল দেখে।

C#-তে একই refactoring

C# আমাদের ফলাফলের জন্য বেশ কিছু সুন্দর shape দেয়। সবচেয়ে modern আর compact হলো record, যা value equality আর পড়ার মতো printing বিনামূল্যে দেয়:

// BEFORE: the impostor array
// row[0] = name, row[1] = wins, row[2] = losses
string[] row = { "Nagpur Strikers", "12", "4" };
double rate = double.Parse(row[1]) / (double.Parse(row[1]) + double.Parse(row[2]));
// AFTER: a record with named, typed members
public record TeamStanding(string Name, int Wins, int Losses)
{
    public int Points => Wins * 2;
 
    public double WinRate =>
        (Wins + Losses) == 0 ? 0 : (double)Wins / (Wins + Losses);
 
    public string Summary() => $"{Name}: {Points} pts (win rate {WinRate:F2})";
}
 
var team = new TeamStanding("Nagpur Strikers", 12, 4);
Console.WriteLine(team.Summary());   // Nagpur Strikers: 24 pts (win rate 0.75)

C#-specific কিছু বিষয় যা তাড়াতাড়ি নিজেকে শেখানো উচিত:

  • int Wins একটা real integer। String-parsing ritual (double.Parse(row[1])) একবারই হয়, সেই boundary-তে যেখানে data প্রবেশ করে — যেমন একটা FromCsvRow factory method-এ — আর কখনো না।
  • Record-এর positional constructor-এর এখনও একটা order আছে (Name, তারপর Wins, তারপর Losses), কিন্তু compiler type check করে। আর তুমি named argument ব্যবহার করতে পারো — new TeamStanding(Name: "Aces", Wins: 9, Losses: 7) — call site-গুলোকে ফ্রিজের label card-এর মতো পড়াতে।
  • Points হলো expression-bodied property: demand-এ computed derived data, TypeScript getter-এর মতোই।
  • Data তৈরির পরে বদলাতে হলে (live match চলাকালীন mutable wins count), বাইরের কাউকে field set করতে দেওয়ার বদলে RecordWin()-এর মতো method সহ class prefer করো — যেটা পরের lesson, Encapsulate Field-এর বিষয়।

IDE support

Mainstream IDE-তে কোনো single one-click "Replace Array with Object" button নেই কারণ IDE অনুমান করতে পারে না প্রতিটা slot মানে কী — শুধু তুমি জানো slot 1 হলো "wins"। কিন্তু IDE প্রায় প্রতিটা individual step automate করে:

  • Visual Studio / Rider / IntelliJ IDEA: generate constructor আর generate properties action দিয়ে দ্রুত নতুন class তৈরি করো। Rider ও IntelliJ-এ, এখনো-বিদ্যমান-নয় এমন class-এর ব্যবহারে Alt+Enter চাপলে তা তৈরি করার offer আসে।
  • Find Usages (Visual Studio-তে Shift+F12, JetBrains IDE-তে Alt+F7) প্রতিটা জায়গা list করে যেখানে array index করা হচ্ছে — একে একে migrate করার একটা precise checklist।
  • Rename refactoring (VS Code-এ F2, JetBrains IDE-তে Shift+F6) পরে আরও ভালো নাম বেছে নিলে নতুন field-গুলো নিরাপদে rename করে — যেটা সম্ভব ছিল না যখন "field" মানে ছিল শুধু number 1।
  • TypeScript-এর compiler তোমার refactoring partner: কোনো function-এর parameter string[] থেকে StudentRecord-এ বদলাও আর compiler সঙ্গে সঙ্গে প্রতিটা caller list করবে যারা এখনও array pass করছে। একে একে fix করো, আর error list খালি হলে migration সম্পূর্ণ।

মনে রাখার কথা: IDE mechanical typing করে; তুমি অর্থ সরবরাহ করো।

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

সুবিধাঝুঁকি / খরচ
প্রতিটা মান একটা meaningful নাম পায় — row[1]-এর বদলে team.winsলিখতে ও maintain করতে একটা নতুন class
প্রতিটা field তার নিজস্ব সঠিক type পায়; number সাজা string আর নাI/O boundary-তে (CSV, JSON, network) positional form-এ যাওয়া-আসার জন্য mapping code লিখতে হবে
পুরো পরিবারের bug বিলুপ্ত হয়: ভুল index, off-by-one, transposed columnসত্যিকারের homogeneous list-এ ভুল — genuine similar item-এর list-এ প্রয়োগ করো না
Object behaviour-এর বাড়ি হয় (winRate(), validation, derived value)এক function-এ ব্যবহৃত ছোট throwaway pair-এর জন্য tuple full class-এর চেয়ে হালকা হতে পারে
Compiler ও IDE field access check, rename ও navigate করতে পারেMigration-এ type conversion তাড়াহুড়ো করলে গোপনে আচরণ বদলে যেতে পারে

কোন smell এটা সারায়?

SmellReplace Array with Object কীভাবে সাহায্য করে
Primitive ObsessionStructured value-এর জায়গায় থাকা raw array proper type হয় named, validated field সহ
Data Classআংশিকভাবে — নতুন object data হিসেবে শুরু হয়, কিন্তু পদক্ষেপ ৬ (behaviour ভেতরে নেওয়া) এটাকে hollow data bag হতে বাধা দেয়
Duplicate Codeঅনেক function-এ বারবার index-decoding ও string-to-number conversion এক জায়গায় collapse হয়
CommentsDecoder-ring comment (// [1] = wins) অপ্রয়োজনীয় হয়ে মুছে যায়

আমরা যা cover করলাম সব একটা mental map-এ। শুধু এই picture মনে রাখলে পুরো lesson মনে রাখা হলো:

চিত্র ৯: পুরো refactoring একটা map-এ

দ্রুত revision বাক্স

+================================================================+
|          REPLACE ARRAY WITH OBJECT — REVISION CARD             |
+================================================================+
| SMELL SIGN : row[0], row[1], row[2] mean DIFFERENT things      |
| TEST       : "Would shuffling destroy the meaning?"            |
|              YES -> it is a record in disguise -> refactor     |
|              NO  -> true list of similar items -> leave it     |
+----------------------------------------------------------------+
| THE MOVE   : 1. Create empty class                             |
|              2. Add one named, typed field per slot            |
|              3. Migrate readers slot by slot (test each step)  |
|              4. Delete the array form                          |
|              5. Move related behaviour into the class          |
+----------------------------------------------------------------+
| REMEMBER   : Convert types at ONE boundary only                |
|              Derived data (points) -> computed getter          |
|              Fridge whiteboard -> label card                   |
+================================================================+

Practice করো

ধরো একটা school canteen প্রতিদিনের বিক্রি এভাবে track করছে:

// sale[0] = item name, sale[1] = price in rupees (string),
// sale[2] = quantity sold (string), sale[3] = "veg" or "nonveg"
const sales: string[][] = [
  ["Samosa", "15", "120", "veg"],
  ["Vada Pav", "20", "95", "veg"],
  ["Egg Roll", "40", "30", "nonveg"],
];
 
function totalRevenue(sales: string[][]): number {
  let total = 0;
  for (const sale of sales) {
    total += Number(sale[1]) * Number(sale[2]);
  }
  return total;
}
 
function vegItems(sales: string[][]): string[] {
  return sales.filter((s) => s[3] === "veg").map((s) => s[0]);
}

তোমার কাজ:

  1. Replace Array with Object প্রয়োগ করো। pricequantity অবশ্যই number হবে; food type-এর জন্য union type "veg" | "nonveg" বিবেচনা করো — এমন properly named ও typed field সহ CanteenSale class তৈরি করো।
  2. totalRevenuevegItems ধাপে ধাপে migrate করো — প্রতিটা পরিবর্তনের পরে program compile রাখো।
  3. CanteenSale-এ একটা revenue getter যোগ করো যাতে multiplication data-র সাথে থাকে।
  4. Constructor-এ validation যোগ করো: price ও quantity negative হওয়া যাবে না।
  5. Bonus: fromCsvRow(row: string[]) factory লেখো যাতে real CSV file-এর positional data ঠিক এক জায়গায় convert হয়।
  6. Reflection: বাইরের sales array — এটাও কি object হওয়া উচিত? কেন বা কেন না? Hint: shuffle test প্রয়োগ করো।

আর শেষে একটা কথা মনে রেখো। Whiteboard format খারাপ ছিল না — এটা ছিল সস্তা। সালাম ভাই label না লিখে দশ সেকেন্ড বাঁচিয়েছিলেন, আর পরিবার সেই দশ সেকেন্ডের মূল্য চুকিয়েছিল ফোন call, সাত রুটি, আর class 8 পর্যন্ত ধাওয়া করা একটা ডাকনাম দিয়ে। গোপন position-ওয়ালা array ঠিক এই trade-ই করে: লেখার সময় কয়েক সেকেন্ড বাঁচায়, সুদসহ ফেরত দেয় প্রতিটা পাঠককে, চিরকালের জন্য। Label লেখো। ভবিষ্যতের পাঠক — তিন মাস পরের তুমিসহ — হলো তোমার ফ্রিজ পড়া পরিবার।

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

প্রতিটা array কি খারাপ? সব array-কে object দিয়ে বদলে দেব?
না, একদমই না! Array তখনই পারফেক্ট যখন প্রতিটা slot-এ একই ধরনের জিনিস থাকে — marks-এর list, student নামের list, তাপমাত্রার series। সমস্যা হয় তখনই যখন slot 0 মানে এক জিনিস (নাম) আর slot 1 মানে সম্পূর্ণ আলাদা জিনিস (class number)। সেই array আসলে একটা record সাজছে, আর শুধু সেই ধরনের array-এর জন্যই এই refactoring দরকার।
এটা Replace Data Value with Object-এর থেকে আলাদা কীভাবে?
Replace Data Value with Object একটা single plain value (যেমন phone number ধরে রাখা একটা string)-কে proper type-এ upgrade করে। Replace Array with Object পুরো একটা mixed value-এর bundle-কে upgrade করে যেগুলো array slot-এ ঢুকিয়ে দেওয়া হয়েছিল। দুটোর চেতনা একই — data-কে একটা real shape দাও — কিন্তু একটা single value-এ কাজ করে আর অপরটা positional bundle-এ।
আমি কি TypeScript tuple যেমন [string, number] ব্যবহার করতে পারি class-এর বদলে?
typed tuple plain array-এর চেয়ে ভালো কারণ compiler প্রতিটা position-এ type check করে। কিন্তু position-গুলোর এখনও কোনো নাম নেই — পাঠকদের এখনও মনে রাখতে হবে index 1 মানে কী। এক জায়গায় ব্যবহার হওয়া ছোট data-এর জন্য tuple ঠিক আছে। কিন্তু program জুড়ে ঘুরে বেড়ানো data-এর জন্য object বা class-এ named field অনেক বেশি পরিষ্কার।
CSV file বা API থেকে আসা data যদি সত্যিই positional হয়?
সীমানায় convert করো। যখনই একটা CSV row তোমার program-এ ঢোকে, একটা mapping function-এ তাকে proper object-এ রূপান্তর করো। বাকি code তখন named field নিয়ে কাজ করবে। আবার data বের করার সময় একটা function object-কে positional form-এ convert করবে। শুধু edge-এ রাখো cryptic format-টাকে।
object বানানোর পর আমার class-এ শুধু data আছে, কোনো method নেই। এটা কি ঠিক আছে?
এটা একটা ভালো প্রথম পদক্ষেপ, কিন্তু সাবধান থাকো — শুধু data সহ একটা class হলো Data Class smell। এর আশেপাশের code দেখো। object-এর field ব্যবহার করে যেকোনো calculation (যেমন marks থেকে percentage বের করা) সম্ভবত object-এর ভেতরে method হিসেবে থাকা উচিত। ভেতরে নিয়ে যাও, তখন object সত্যিকারের কাজের হবে।

আরো দেখো

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

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

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

আরও পড়ুন

Data Class: নিয়মহীন রেজিস্টার — যে কেউ যা খুশি লিখে যায়

Data Class smell শেখো একটা society register-এর গল্পের মাধ্যমে। দেখো কেন behavior ছাড়া data encapsulation ভেঙে পড়ে, আর কখন DTO আর record একদম ঠিকঠাক।

আরও পড়ুন

Encapsulate Field: Object যেন নিজের ডেটা নিজে পাহারা দেয়

Encapsulate Field কী সেটা সহজ ভাষায় — কেন public field যেকোনো কোডকে object-এর ডেটা নষ্ট করতে দেয়, আর কীভাবে private field সাথে getter-setter দিয়ে object নিজেই সব নিয়ন্ত্রণ করে।

আরও পড়ুন

Encapsulate Collection: লাইভ লিস্ট বাইরে দেওয়া বন্ধ করো

Encapsulate Collection সহজ ভাষায় — কেন live array বা list return করলে যেকেউ তোমার object নষ্ট করে দিতে পারে, আর কীভাবে read-only view আর add/remove method দিয়ে নিয়ন্ত্রণ ফিরিয়ে আনা যায়।

আরও পড়ুন