Change Reference to Value: যেকোনো ১০ টাকার নোটই সমান
Change Reference to Value সহজভাবে বোঝানো হয়েছে — একটা shared, mutable reference object-কে কীভাবে content-based equality সহ একটা ছোট immutable value object-এ রূপান্তর করতে হয়, TypeScript আর C# record-এর উদাহরণসহ।
💵 পকেটে দশ টাকার নোট
ধরো সুমাইয়া তার কলেজের গেটের বাইরের দোকান থেকে ১০ টাকায় সিঙ্গারা কিনলো। দোকানদার জামাল ভাইকে একটু কুঁচকানো ১০ টাকার নোট দিলো। বিকেলে চা কিনতে গেলে জামাল ভাই খুচরো হিসেবে একটা ১০ টাকার নোট ফেরত দিলেন — সকালে দেওয়াটার থেকে আলাদা, একটু বেশি সোজা, এক কোণে কারো ফোন নম্বর লেখা।
সুমাইয়া কি অভিযোগ করবে? "ভাই, এটা আমার নোট না! আমারটায় কোণে একটু ভাঁজ ছিল!"
অবশ্যই না। ব্যাপারটা হাস্যকর। একটু ভাবো কেন হাস্যকর। পৃথিবীতে কেউ track করে না তার কাছে কোন নির্দিষ্ট ১০ টাকার নোট আছে। প্রতিটি ১০ টাকার নোটের মান ঠিক ১০ টাকা, আর যেকোনো একটি অন্যটির সাথে অবাধে বদলানো যায়। সিরিয়াল নম্বর আছে — বাংলাদেশ ব্যাংক দরকার হলে নোট track করে — কিন্তু দৈনন্দিন জীবনে সিরিয়াল নম্বরের কোনো মানে নেই। নোট সম্পর্কে যেটা গুরুত্বপূর্ণ সেটা হলো এর মান, এর পরিচয় নয়।
এবার সুমাইয়ার কলেজ ID card-এর কথা ভাবো। একদিন অফিস card গুলিয়ে ফেলে সুমাইয়াকে নাসরিনের ID দিয়ে দেয় — একই কোর্স, একই বছর। এটা কি ঠিক আছে? একদমই না। ID card একজন নির্দিষ্ট মানুষকে represent করে। সুমাইয়া নাসরিনের card দিয়ে পরীক্ষায় বসতে পারবে না, যতই "সমান" দেখতে হোক না কেন। ID card-এর পরিচয় আছে। ১০ টাকার নোটের নেই।
এই একটা পার্থক্য — নোট বনাম ID card — এটাই আজকের refactoring-এর পুরো পাঠ। আমাদের program-এ কিছু object হলো ID card: customer, student, account। ঠিক কোনটা ধরা আছে সেটা track করতে হয়। কিন্তু অনেক object হলো ১০ টাকার নোট: টাকার পরিমাণ, তারিখ, ফোন নম্বর, মানচিত্রের স্থানাঙ্ক। এগুলোর জন্য "কোনটা?" জিজ্ঞেস করাটাই অর্থহীন — শুধু "কত মান?" টাই অর্থবহ।
Change Reference to Value সেই মুহূর্তের জন্য যখন দেখো একটা ১০ টাকার নোটকে ID card-এর মতো treat করা হচ্ছে — একটা tracked instance share করা হচ্ছে, সেটা in-place পরিবর্তন করা হচ্ছে, "এটা কি একই object?" দিয়ে তুলনা করা হচ্ছে — অথচ এই concept-এর কখনোই কোনো পরিচয় ছিল না। সমাধান: এটাকে একটা ছোট, immutable value বানাও। একই contents সহ যেকোনো অন্য value-এর সমান।
🧠 Change Reference to Value কী?
Change Reference to Value হলো Martin Fowler-এর Refactoring বই থেকে একটা refactoring। পরিস্থিতিটা এরকম: একটা object-কে reference হিসেবে manage করা হচ্ছে — registry বা factory দিয়ে তৈরি করা হচ্ছে, holder-দের মধ্যে share করা হচ্ছে, in-place পরিবর্তন করা হচ্ছে, identity দিয়ে তুলনা করা হচ্ছে — অথচ এটা যে concept represent করে তার কোনো আসল পরিচয় নেই। যেমন একটা currency, একটা date range, একটা টাকার পরিমাণ। "THE পাঁচশো টাকা" বলে কিছু নেই। যেকোনো ৫০০ টাকা অন্য যেকোনো ৫০০ টাকার সমান।
এমন একটা concept-কে reference হিসেবে treat করলে অপ্রয়োজনীয় ঝামেলা বেড়ে যায়: instance lookup করার জন্য registry, সতর্ক lifecycle management, আর — সবচেয়ে খারাপ হলো — aliasing-এর বিপদ। একজন holder shared object পরিবর্তন করে, আর অন্য সব holder-এর data চুপচাপ বদলে যায়।
এই কাজের তিনটা অংশ:
- Object-কে immutable করো। Setter সরাও। আগে যেসব method object পরিবর্তন করত সেগুলো এখন নতুন object return করবে — নোটে লেখার বদলে নোট বদলানোর মতো।
- এটাকে value equality দাও। সমান contents সহ দুটো object সমান, শেষ কথা।
equals/==আর hash code override করো যাতে সেগুলো একমত হয়। - Sharing machinery মুছে দাও। Registry, factory cache, lookup — সব গেল। যে কেউ যেখানে খুশি value তৈরি করতে পারবে, কারণ copy নিরীহ।
Refactoring-এর পর, object-টা Martin Fowler-এর অর্থে একটা সত্যিকারের value object হয়: ছোট, immutable, contents দিয়ে তুলনীয়। তার প্রিয় উদাহরণ হলো Money — একটা পরিমাণ আর একটা currency। Value Object bliki-তে তার rule of thumb পরিষ্কার: value গুলো equality method override করে; entity গুলো সাধারণত করে না। Equality আক্ষরিক অর্থে সেই fingerprint যা তোমাকে বলে কোন ধরনের object তুমি দেখছ।
আমাদের একমাত্র guiding প্রশ্নটা মনে করো, Change Value to Reference থেকেও একই:
"এটা কি THE একই জিনিস, নাকি শুধু AN সমান জিনিস?"
যদি "AN সমান জিনিস" যথেষ্ট হয় — যেকোনো সমান নোটই চলবে — তাহলে concept-টা একটা value। এই refactoring code-কে সেই সত্যের সাথে মেলায়।
Fowler-এর practical trigger: একটা reference object "তার lifecycle manage করা justify করার জন্য অনেক ছোট আর খুব কমই পরিবর্তিত হয়।" যদি তুমি একটা তারিখ বা পরিমাণের জন্য registry, identity comparison, আর mutation discipline maintain করো — তাহলে তুমি একটা ১০ টাকার নোটের concept-এর জন্য ID card-এর দাম দিচ্ছ। দেওয়া বন্ধ করো। এটাকে value বানাও।
একটু deep dive করি: এই refactoring যে bug মারে তাকে বলে aliasing। দুটো নাম (variable, field) একটা mutable object-এর সাথে বাঁধা, তাই এক নামের মাধ্যমে write করলে অন্য নামের মাধ্যমে সেটা দেখা যায়। Aliasing সবসময় ভুল নয় — entity গুলো ঠিক এটাই চায়। কিন্তু অনিচ্ছাকৃত aliasing সবচেয়ে কঠিন trace করার মতো bug, কারণ দূষণকারী write হয় অনেক দূরে এমন code-এ যেটা "স্পষ্টতই" অন্য কিছু touch করছে। Functional programming language গুলো সবকিছু immutable করে পুরো সমস্যাটা সমাধান করে। Value object গুলো সেই সমাধান surgically import করে — শুধু যেখানে identity অর্থহীন সেখানে immutability প্রয়োগ করে। একটা immutable object এছাড়াও automatically thread-safe: কোনো write নেই মানে কোনো data race নেই, কোনো lock নেই।
🔔 কখন এটা দরকার?
এই refactoring-এর কথা ভাবো যখন দেখবে:
- Aliasing bug ("spooky action at a distance")। তুমি এক order-এর discount পরিবর্তন করলে সম্পূর্ণ ভিন্ন একটা order-এর total পরিবর্তন হয়ে যাচ্ছে — কারণ দুটো গোপনে একটা mutable object share করছিল। এটাই সবচেয়ে বড় সংকেত।
- Identity-free কিছুর জন্য registry। এমন একটা cache যেটা
CurrencyবাMoneyinstance intern করে "যাতে==কাজ করে" — সেটা ভুল model-কে support করছে। Value equality এই প্রয়োজনীয়তা দূর করে। - Identity ছাড়া concept-এ identity comparison। Code যেটা amount বা date-এর জন্য
a === bবাReferenceEquals(a, b)জিজ্ঞেস করছে, আর যখনই একটা সমান-কিন্তু-আলাদা instance আসে তখন ভেঙে পড়ছে — যেমন JSON deserialization-এর পর। - Distributed বা parallel সমস্যা। Fowler উল্লেখ করেন reference গুলো process জুড়ে বিশেষভাবে awkward। নেটওয়ার্কের উপর দিয়ে pointer share করা যায় না। Value গুলো সুখে travel করে — serialize করো, পাঠাও, recreate করো, তবুও সমান।
- Collection গণ্ডগোল করছে। তুমি
Money-কে map key বা set-এ চাও, কিন্তু value-basedequals/hashCodeছাড়া collection সমান amount গুলোকে ভিন্ন entry হিসেবে treat করে।
এই refactoring Primitive Obsession যে যাত্রা শুরু করে তাও সম্পূর্ণ করে। প্রথমে তুমি একটা raw number-কে Money class-এ wrap করো (Replace Data Value with Object), তারপর এই refactoring সেই class-কে একটা সত্যিকারের value বানায় — immutable, content-equal, freely copyable। একইভাবে, যখন Data Clumps একটা Address বা DateRange-এ bundle হয়, সেই bundle গুলো প্রায় সবসময় value semantics deserve করে।
কখন ব্যবহার করবে না: যখন object-টার সত্যিই identity আর shared changing state আছে — একজন customer, একটা account, একটা live order। এগুলোকে value বানালে update গুলো propagate হওয়া বন্ধ হয়ে যাবে। Concept নির্ধারণ করে, fashion নয়।
সংকেত গুলোর একটা quick triage table:
| সংকেত | এটা কেমন শোনায় | রোগ নির্ণয় |
|---|---|---|
| Spooky action at a distance | "আমি order A পরিবর্তন করলাম আর order B-এর total নড়ে গেল" | Aliased mutable value — এই refactoring প্রয়োগ করো |
| Amount-এর জন্য interning cache | "আমরা Money cache করি যাতে == কাজ করে" | ভুল equality support করছে এমন machinery |
| Deserialization check ভাঙে | "JSON load-এর পরে === fail করতে শুরু করেছে" | Value equality-র জায়গায় identity ব্যবহার হচ্ছে |
| Set-এ duplicate | "একটা set-এ দুটো সমান DateRange" | Content-based equals আর hash নেই |
| Update propagate হওয়া বন্ধ | "এটা immutable করলাম আর screen stale হয়ে গেল" | অনেক বেশি! ওই concept একটা entity — inverse refactoring |
👀 এক নজরে আগে আর পরে
দেখো TypeScript-এ এই রোগটা — একটা shared, mutable Money cache দিয়ে বিতরণ করা হচ্ছে:
// BEFORE — Money treated like an ID card
class Money {
constructor(
public amount: number,
public currency: string,
) {}
addTo(delta: number): void {
this.amount += delta; // mutates in place!
}
}
// A registry "interning" money so identity comparison works:
const cache = new Map<string, Money>();
function money(amount: number, currency: string): Money {
const key = `${amount}-${currency}`;
if (!cache.has(key)) cache.set(key, new Money(amount, currency));
return cache.get(key)!;
}
const samosaPrice = money(10, "INR");
const chaiPrice = money(10, "INR"); // SAME shared object as samosaPrice
samosaPrice.addTo(5); // samosa got costlier...
console.log(chaiPrice.amount); // 15 — chai price changed too! Spooky!
console.log(samosaPrice === chaiPrice); // true, but only by accident of the cacheজামাল ভাই ভয় পেয়ে যেতেন। সিঙ্গারার দাম বাড়ানোতে কোনো কারণে চায়ের দামও বাড়লো, কারণ দুটো দামই গোপনে একই নোট ছিল। এরপর — Money একটা সৎ ১০ টাকার নোট হয়:
// AFTER — Money as a value: immutable, content-equal, freely created
class Money {
constructor(
public readonly amount: number,
public readonly currency: string,
) {
Object.freeze(this); // belt and braces: runtime immutability
}
plus(delta: number): Money {
return new Money(this.amount + delta, this.currency); // a NEW note
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
const samosaPrice = new Money(10, "INR");
const chaiPrice = new Money(10, "INR"); // separate object, and that is FINE
const newSamosaPrice = samosaPrice.plus(5);
console.log(chaiPrice.amount); // 10 — untouched, no spooky action
console.log(samosaPrice.equals(chaiPrice)); // true — AN equal thing
console.log(samosaPrice === chaiPrice); // false — and nobody caresCache মুছে গেল। Mutation চলে গেল — টাকা "পরিবর্তন" মানে নতুন টাকা বানানো। আর equality এখন সেই একমাত্র প্রশ্নই করে যেটা টাকার জন্য কখনো অর্থবহ ছিল: একই পরিমাণ, একই currency?
🪜 ধাপে ধাপে, নিরাপদ উপায়ে
-
নিশ্চিত করো concept-টার কোনো identity নেই। তোমার team-এর সাথে টাকার নোটের প্রশ্নটা করো: যদি দুটো instance-এর সমান field থাকে, এমন কোনো পরিস্থিতি আছে কি যেখানে program-কে সেগুলোকে আলাদা করতে হবে? যদি হ্যাঁ — থামো, এটা entity। যদি না — এগিয়ে যাও।
-
Object-কে immutable করো, একটা দরজা একবারে। Setter সরাও বা private করো। প্রতিটা mutating method-এর জন্য, একটা non-mutating twin যোগ করো যেটা নতুন instance return করে, caller গুলোকে twin-এ migrate করো, তারপর mutator মুছে দাও:
class Money {
// Step 2 (intermediate): both methods exist while callers migrate
addTo(delta: number): void {
this.amount += delta; // old, dying
}
plus(delta: number): Money {
return new Money(this.amount + delta, this.currency); // new, growing
}
}-
Field গুলো readonly চিহ্নিত করো একবার কোনো mutator না থাকলে। Compiler এখন তোমার জন্য immutability prove করে আর যেকোনো straggler-কে error হিসেবে দেখায়।
-
Value equality আর matching hash implement করো। প্রতিটা defining field দুটোতেই অংশ নেয়। TypeScript-এ এটা হলো
equalsmethod (আরtoKey()string যদি map key দরকার হয়, কারণ JS map গুলো object identity দিয়ে তুলনা করে)। C#, Java, Kotlin, আর Python-এ language feature ব্যবহার করো —record,record class,data class,@dataclass(frozen=True)— compiler সঠিক, সবসময়-synchronized equality আর hashing লেখে। -
Registry ভেঙে ফেলো। Constructor public করো, cache আর factory lookup মুছে দাও, caller-দের স্বাধীনভাবে
new Money(10, "INR")লিখতে দাও। Diff দেখো কীভাবে ছোট হয় — মুছে যাওয়া machinery এই refactoring-এর দৃশ্যমান পুরস্কার। -
Identity check গুলোকে value check-এ বদলাও। এই type-এ
===,ReferenceEquals,is(Python) খোঁজো আর প্রতিটিকেequals/==-এ convert করো। তারপর পুরো test suite চালাও। বিশেষ মনোযোগ দিয়ে দেখো এমন code যেটা shared mutation propagate হওয়ার উপর নির্ভর করত — ওই code-এর একটা real entity দরকার, বা একটা explicit update path।
Step 4 সবচেয়ে ধারালো ছুরিটা লুকিয়ে রাখে: equality আর hash code অবশ্যই একমত হতে হবে। যদি দুটো object সমান হয় কিন্তু আলাদাভাবে hash করে, তাহলে dictionary আর set চুপচাপ সেগুলো হারিয়ে ফেলে — item অদৃশ্য হয়, lookup miss করে, কোনো exception throw হয় না। Hand-written equality প্রতিবার field যোগ হলে drift করে। Compiler-generated equality জোরালোভাবে prefer করো (C# records, Python frozen dataclasses, Kotlin data classes)।
আরেকটু গভীরে যাই: equals-hash contract-এর একটা দ্বিতীয়, সূক্ষ্ম clause আছে। Dictionary key হিসেবে ব্যবহৃত object-এর hash dictionary-তে থাকাকালীন কখনো পরিবর্তন হওয়া উচিত না। Hash container গুলো প্রতিটা entry insertion-এর সময় তার hash দিয়ে বেছে নেওয়া bucket-এ store করে। Hash-এ feed করে এমন field পরিবর্তন করলে lookup এখন ভিন্ন bucket compute করে — entry এখনো আছে কিন্তু আর কখনো খুঁজে পাওয়া যাবে না। এ কারণেই "mutable value object" একটা contradiction। Value equality field-based hashing দাবি করে, আর field-based hashing দাবি করে field গুলো কখনো পরিবর্তন না হোক। Immutability এখানে stylistic preference নয় — এটাই value semantics-কে mathematically coherent করে।
🏫 একটা বড় বাস্তব উদাহরণ
ধরো একটা travel-booking app-এ hotel stay-এর জন্য একটা DateRange model করা হয়েছে। কেউ বছর আগে এটাকে shared, mutable reference বানিয়েছিল "memory বাঁচাতে," আর তারপর থেকে bug কামড়াচ্ছে:
// BEFORE — a shared, mutable DateRange
class DateRange {
constructor(
public checkIn: string, // "2026-07-01"
public checkOut: string, // "2026-07-05"
) {}
extendByDays(days: number): void {
const out = new Date(this.checkOut);
out.setDate(out.getDate() + days);
this.checkOut = out.toISOString().slice(0, 10); // mutation!
}
}
class Booking {
constructor(
public readonly hotel: string,
public stay: DateRange,
) {}
}
// The bug: a "copy" that is not a copy
const stay = new DateRange("2026-07-01", "2026-07-05");
const goa = new Booking("Sea Breeze, Goa", stay);
const munnar = new Booking("Hill View, Munnar", stay); // same object reused!
goa.stay.extendByDays(2); // guest extends Goa stay...
console.log(munnar.stay.checkOut); // 2026-07-07 — Munnar booking changed too!একজন guest Goa-তে stay বাড়ালো, আর Munnar-এর booking চুপচাপ দুই দিন বেড়ে গেল। Munnar-এর hotel কেউ ঘুমায়নি এমন রাতের বিল করছে। DateRange-কে value বানানোর পর:
// AFTER — DateRange as an immutable value
class DateRange {
constructor(
public readonly checkIn: string,
public readonly checkOut: string,
) {
if (checkOut <= checkIn) {
throw new Error("Check-out must be after check-in");
}
Object.freeze(this);
}
extendByDays(days: number): DateRange {
const out = new Date(this.checkOut);
out.setDate(out.getDate() + days);
return new DateRange(this.checkIn, out.toISOString().slice(0, 10));
}
nights(): number {
const ms = +new Date(this.checkOut) - +new Date(this.checkIn);
return Math.round(ms / 86_400_000);
}
equals(other: DateRange): boolean {
return this.checkIn === other.checkIn && this.checkOut === other.checkOut;
}
overlaps(other: DateRange): boolean {
return this.checkIn < other.checkOut && other.checkIn < this.checkOut;
}
}
class Booking {
constructor(
public readonly hotel: string,
public readonly stay: DateRange,
) {}
extendStay(days: number): Booking {
return new Booking(this.hotel, this.stay.extendByDays(days));
}
}
const stay = new DateRange("2026-07-01", "2026-07-05");
const goa = new Booking("Sea Breeze, Goa", stay);
const munnar = new Booking("Hill View, Munnar", stay); // sharing is now HARMLESS
const extendedGoa = goa.extendStay(2);
console.log(extendedGoa.stay.checkOut); // 2026-07-07
console.log(munnar.stay.checkOut); // 2026-07-05 — perfectly safeভালো জিনিসগুলোর chain reaction লক্ষ্য করো। দুটো booking-এর মধ্যে একই DateRange share করা আর bug নয়। Immutable value গুলো এক মিলিয়ন holder দ্বারা কোনো risk ছাড়াই share হতে পারে। মানে "memory বাঁচানোর" original লক্ষ্যটা mutable cache-এর চেয়ে value design দ্বারা ভালোভাবে অর্জিত হয় — যে cache bug সৃষ্টি করেছিল। Validation constructor-এ চলে গেল, তাই উল্টো range কখনো exist করতে পারবে না। আর type-টা সত্যিকারের useful behaviour আকর্ষণ করলো (nights, overlaps) কারণ এটা এখন একটা real domain concept।
💜 C#-এ একই refactoring
এখানেই C# records সবচেয়ে উজ্জ্বল হয় — একটা record হলো language-এ built-in একটা value object kit। init-only properties দিয়ে immutability, value equality, matching hash code, ToString, আর with expression দিয়ে non-destructive mutation — সব compiler-generated:
public sealed record DateRange
{
public DateOnly CheckIn { get; }
public DateOnly CheckOut { get; }
public DateRange(DateOnly checkIn, DateOnly checkOut)
{
if (checkOut <= checkIn)
throw new ArgumentException("Check-out must be after check-in");
CheckIn = checkIn;
CheckOut = checkOut;
}
public int Nights => CheckOut.DayNumber - CheckIn.DayNumber;
public DateRange ExtendByDays(int days) =>
new(CheckIn, CheckOut.AddDays(days));
public bool Overlaps(DateRange other) =>
CheckIn < other.CheckOut && other.CheckIn < CheckOut;
}
var a = new DateRange(new(2026, 7, 1), new(2026, 7, 5));
var b = new DateRange(new(2026, 7, 1), new(2026, 7, 5));
Console.WriteLine(a == b); // True — value equality, FREE
Console.WriteLine(ReferenceEquals(a, b)); // False — and irrelevantআর with-expression update সহ positional record হিসেবে Money:
public readonly record struct Money(decimal Amount, string Currency)
{
public Money Plus(decimal delta) => this with { Amount = Amount + delta };
public override string ToString() => $"{Currency} {Amount:F2}";
}
var samosa = new Money(10m, "INR");
var costlier = samosa with { Amount = 15m }; // NEW value, original untouched
Console.WriteLine(samosa); // INR 10.00
Console.WriteLine(costlier); // INR 15.00
Console.WriteLine(samosa == new Money(10m, "INR")); // Trueএটা কেন refactoring-এর সাথে নিখুঁতভাবে মেলে:
recordবনামclassহলো একটা keyword-এ value/reference split। একটা plainclassreference দিয়ে তুলনা করে (ID card); একটাrecordcontents দিয়ে তুলনা করে (১০ টাকার নোট)। Keyword পরিবর্তন করাটাই আক্ষরিক অর্থে Change Reference to Value।readonly record structএমনকি heap allocation-ও সরিয়ে দেয় — value inline থাকে, hot path-এর জন্য আদর্শ।withexpression হলো idiomatic "নোট বদলানোর" operation: সবকিছু copy করো, একটা field পরিবর্তন করো, original অস্পর্শিত।- .NET-এর জন্য Microsoft-এর DDD guidance value object-এর জন্য record recommend করে, EF Core-সহ owned types বা value converter দিয়ে mapping করে — তাই database plain column store করে আর code পুরো value semantics উপভোগ করে।
Python frozen dataclass-এর মাধ্যমে একই kit দেয় — এক decorator-এ immutability, content equality, আর hashing:
from dataclasses import dataclass, replace
from datetime import date
@dataclass(frozen=True)
class DateRange:
check_in: date
check_out: date
def __post_init__(self) -> None:
if self.check_out <= self.check_in:
raise ValueError("Check-out must be after check-in")
def extend_by_days(self, days: int) -> "DateRange":
from datetime import timedelta
return replace(self, check_out=self.check_out + timedelta(days=days))
@property
def nights(self) -> int:
return (self.check_out - self.check_in).days
a = DateRange(date(2026, 7, 1), date(2026, 7, 5))
b = DateRange(date(2026, 7, 1), date(2026, 7, 5))
assert a == b # content equality, generated
assert a is not b # different objects — and nobody cares
assert hash(a) == hash(b) # frozen=True makes hashing safeএকটু লক্ষ্য করো: frozen=True কী করে dataclass-টাকে default-এ hashable করে তোলে। eq=True সহ একটা mutable dataclass-এ __hash__ None-এ set হয় — Python তোমাকে আগে বলা broken-bucket bug থেকে রক্ষা করছে, content-equal mutable object-কে set-এ ঢুকতে দিতে অস্বীকার করছে। Language designer গুলো decorator-এর default-এ সরাসরি equals-hash-immutability triangle encode করেছে। যখন একটা language mutable object hash করতে বাধা দেয়, সেটা pedantic হচ্ছে না; সেটা contract মনে করিয়ে দিচ্ছে।
🛠️ IDE support
| Tool | কাজের moves |
|---|---|
| Visual Studio / Rider (C#) | Quick-action "Convert class to record" equality-এর অংশটা এক click-এ করে; Make readonly / Add init accessor inspection গুলো immutability-র দিকে ঠেলে; setter-এ Find Usages প্রতিটা migration করার mutation খুঁজে দেয়। |
| IntelliJ IDEA (Java) | "Convert class to record" inspection (Java 16+ records); Refactor → Make Static/Immutable helper; record field type mutable হলে warning। |
| VS Code (TypeScript) | Field গুলো readonly চিহ্নিত করো আর compiler প্রতিটা mutation error হিসেবে তালিকাভুক্ত করুক — একটা সম্পূর্ণ migration checklist; ESLint rule যেমন functional/immutable-data type-কে frozen রাখতে পারে। |
| সবগুলো | এই type-এ identity check (ReferenceEquals, ===, is) খোঁজো comparison গুলো খুঁজে পেতে যেগুলো value equality হতে হবে। |
⚖️ সুবিধা আর ঝুঁকি — আর দোলনা
প্রথমে দোলনা — কারণ এই refactoring আর Change Value to Reference সঠিক বিপরীত। একটা concept-কে shared identity-র দিকে ঠেলে; অন্যটা সেটাকে free-floating value-এ টেনে আনে। নির্ধারণকারী প্রশ্ন কখনো বদলায় না: "এটা কি THE একই জিনিস, নাকি শুধু AN সমান জিনিস?"
| Concept সম্পর্কে প্রশ্ন | Reference-এ ঠেলো (inverse article) | Value-এ টানো (এই article) |
|---|---|---|
| বাস্তব পৃথিবীতে কি এর ঠিক একটা আছে? | হ্যাঁ — একজন রাহিম, একজন customer C-1 | না — যেকোনো ১০ টাকা যেকোনো ১০ টাকার সমান |
| সব holder-কে কি update দেখতে হবে? | হ্যাঁ — একটা instance share করো | না — কোনো update নেই, শুধু নতুন value আছে |
| Equality কীভাবে কাজ করা উচিত? | Identity: একই object | Contents: একই field |
| কী machinery দরকার? | Registry/repository, lifecycle, lock | কিছু না — স্বাধীনভাবে তৈরি করো, নির্ভয়ে share করো |
যদি stale copy-র বিরুদ্ধে লড়তে হয়, দোলনাকে reference-এর দিকে কাত করো। যদি aliasing আর registry-র বিরুদ্ধে লড়তে হয়, value-এর দিকে কাত করো। একই সময়ে দুটোর বিরুদ্ধে লড়া সাধারণত মানে একটা concept গোপনে দুটো — একটা entity যেটার ভেতরে value object আছে, যেমন একটা Customer (entity) যেটার মধ্যে একটা Address (value) আছে।
এখন এই direction-এর ledger:
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Aliasing bug অদৃশ্য — কোনো holder অন্যের নিচ থেকে value পরিবর্তন করতে পারে না। | Shared mutation propagate হওয়ার উপর নির্ভরশীল code চুপচাপ "কাজ করা" বন্ধ করে — এটা তোমাকে খুঁজে বের করার আগেই তুমি খুঁজে বের করো। |
| Equality অবশেষে মানুষ যা expect করে তাই: same contents। | Hand-written equals/hash pair drift করে; compiler-generated prefer করো (records)। |
| Immutable value গুলো zero lock-এ thread-safe, আর serialize/deserialize করে meaning না হারিয়ে। | প্রতিটা "পরিবর্তন" একটা নতুন object allocate করে — সাধারণত negligible, hot loop-এ measurable (readonly record struct ব্যবহার করো)। |
| Registry, cache, আর lifecycle machinery মুছে যায় — কম code, কম global। | বড় object copy আর content দিয়ে তুলনা করা costly; বড় aggregate গুলো reference হিসেবে থাকতে পারে। |
| Value গুলো dictionary key আর set member হিসেবে সঠিকভাবে কাজ করে। | ভুলভাবে বেছে নিলে (একটা entity value form-এ জোর করা) inverse article-এর stale-data bug recreate হয়। |
🧹 এটা কোন smell গুলো ঠিক করে?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Primitive Obsession | Cure সম্পূর্ণ করে: wrapped concept একটা true value object হয় — immutable, comparable, freely shareable। |
| Data Clumps | Bundled clump গুলো (Address, DateRange) সেই value semantics পায় যা সেগুলো প্রায় সবসময় deserve করে। |
| Data Class | Getter/setter-এর একটা mutable bag real behaviour আর সৎ equality সহ একটা immutable type হয়। |
| Temporary Field | In-place mutation workflow যেগুলো অর্ধ-updated state রেখে যেত সেগুলো atomic "নতুন value তৈরি করো" steps দিয়ে replace হয়। |
| Shotgun Surgery | Equality, validation, আর derivation logic holder গুলোর মধ্যে ছড়িয়ে থাকার বদলে value type-এ concentrate হয়। |
📦 Quick revision box
+--------------------------------------------------------------+
| CHANGE REFERENCE TO VALUE — REVISION |
+--------------------------------------------------------------+
| Idea : A shared, mutable, identity-tracked object that |
| has NO real identity becomes an immutable value. |
| Key Q : "Is it THE same thing, or just AN equal thing?" |
| AN equal thing -> value (this refactoring) |
| THE same thing -> reference (see the inverse) |
| Mnemonic : Rs.10 note = value; school ID card = entity. |
| Steps : 1. Confirm: no identity, equal fields = equal |
| 2. Remove mutators; add return-new twins |
| 3. Mark all fields readonly/init |
| 4. Value equality + MATCHING hash (use records!) |
| 5. Delete the registry; constructor goes public |
| 6. Replace === / ReferenceEquals with equals |
| Watch out: equal-but-different-hash kills dictionaries; |
| code that depended on shared mutation. |
| C# bonus : record / readonly record struct = the whole |
| refactoring in one keyword. |
| Inverse : Change Value to Reference (the seesaw's far end) |
+--------------------------------------------------------------+✍️ অনুশীলন করো
ধরো একটা ride-sharing app একটা mutable Location object সবখানে share করছে। Riders রিপোর্ট করছে যে তাদের saved "Home" location রহস্যজনকভাবে সরে যাচ্ছে:
class Location {
constructor(
public lat: number,
public lng: number,
public label: string,
) {}
moveTo(lat: number, lng: number): void {
this.lat = lat;
this.lng = lng;
}
}
const locationCache = new Map<string, Location>();
function getLocation(label: string, lat: number, lng: number): Location {
if (!locationCache.has(label)) {
locationCache.set(label, new Location(lat, lng, label));
}
return locationCache.get(label)!;
}
// The bug in the wild:
const home = getLocation("Home", 17.385, 78.4867);
const tripStart = getLocation("Home", 17.385, 78.4867); // same shared object
tripStart.moveTo(17.4474, 78.3762); // driver adjusts pickup point...
console.log(home.lat, home.lng); // Home has MOVED. Rider is not amused.তোমার কাজ:
- টাকার নোটের প্রশ্নটা দিয়ে সিদ্ধান্ত নাও: একটা
Location(স্থানাঙ্কের একটা জোড়া আর একটা label) কি identity আছে, নাকি এটা value? তোমার এক-লাইনের justification একটা comment হিসেবে লেখো। Locationimmutable করো:readonlyfield, constructor validation (latitude -90 আর 90-এর মধ্যে, longitude -180 আর 180-এর মধ্যে), আর একটাwithCoordinates(lat, lng)method যেটা নতুনLocationreturn করে।- তিনটা field-ই compare করে
equals(other)যোগ করো, আর map-এ ব্যবহারের জন্যtoKey()string। locationCacheআরgetLocationসম্পূর্ণ মুছে দাও। Bug scenario ঠিক করো আর একটা test দিয়ে prove করো যে pickup point adjust করলে "Home" অস্পর্শিত থাকে।- C#-এ Bonus:
Location-কেreadonly record structহিসেবে লেখোwith-expression উদাহরণসহ, আর দেখাও যে দুটো সমান location==true compare করে। - Python-এ Bonus:
Location-কে@dataclass(frozen=True)হিসেবে লেখো আর confirm করো যে এটা একটাset-এর ভেতরে নিরাপদে থাকতে পারে। - চিন্তার প্রশ্ন: app-টাতে একটা
Driverobject-ও আছে।Driver-কেও কি একই treatment দেওয়া উচিত? Seesaw দিয়ে উত্তর দাও — আর যদি না হয়, তুমি পরিবর্তে কোন refactoring check করবে সেটার নাম বলো।
তোমার fixed test যদি দেখায় "Home" দাঁড়িয়ে আছে আর pickup point সরছে, তাহলে তুমি জামাল ভাইয়ের জ্ঞান বুঝেছ: নোট বদলাও, কখনো নোটে লিখো না।
সচরাচর জিজ্ঞাসা
- কোনো কিছু reference না value বানাবো সেটা কীভাবে বুঝবো?
- নিজেকে টাকার নোটের প্রশ্নটা করো: তুমি কি চাও ঠিক কোন নোটটা তোমার কাছে আছে, নাকি সেটার মান কত সেটাই যথেষ্ট? কেউ পকেটে কোন নির্দিষ্ট ১০ টাকার নোট আছে সেটা track করে না — যেকোনো সমান নোটই চলবে। টাকা, তারিখ, স্থানাঙ্ক, ফোন নম্বর — এগুলো এভাবেই কাজ করে: কোনো পরিচয় নেই, শুধু মান আছে। উত্তর যদি হয় 'যেকোনো সমান জিনিস চলবে', তাহলে এটাকে value বানাও।
- value object কেন অবশ্যই immutable হতে হবে?
- কারণ value গুলো freely share আর copy হয়। যদি একটা Money object তার amount পরিবর্তন করতে পারত, তাহলে 'তোমার' ৫০ টাকা পরিবর্তন করা মানে হয়তো অন্য কারো ৫০ টাকাও চুপচাপ পরিবর্তন হয়ে যেত — কারণ সেটা আসলে একই object। Immutability এই বিপদ সম্পূর্ণ দূর করে। একটা value 'পরিবর্তন' করতে হলে তুমি নতুন একটা তৈরি করো — যেমন নোট বদলে নেওয়া।
- প্রতিটি পরিবর্তনে নতুন object বানানো কি memory নষ্ট করে না?
- সাধারণত এটা নিয়ে চিন্তার কিছু নেই — ছোট object সস্তা, আর garbage collector ছোট-জীবনের object-এর জন্য optimize করা। সত্যিই performance-critical জায়গায়, C#-তে readonly record struct আছে যেটা stack-এ থাকে, heap allocation শূন্য। চিন্তা করার আগে measure করো।
- equality আর hashing-এর কী হয়?
- সেগুলো অবশ্যই object-এর contents-এর উপর ভিত্তি করে হতে হবে — value নির্ধারণ করে এমন প্রতিটি field-ই Equals আর hash code দুটোতেই অংশ নেয়, আর দুটো অবশ্যই একমত হতে হবে। এটা ভুল হলে dictionary চুপচাপ গণ্ডগোল করে। এ কারণেই C# records আর Python frozen dataclasses-এর মতো language feature এত দরকারি: compiler তোমার জন্য matching equality আর hashing তৈরি করে দেয়।
- এই refactoring কি সত্যিই Change Value to Reference-এর উল্টো?
- হ্যাঁ, ঠিক তাই। Change Value to Reference copied object নিয়ে সেগুলোকে একটা shared, mutable instance-এ ঢোকায় — identity সহ entity-র জন্য। Change Reference to Value একটা shared instance নিয়ে সেটাকে free, immutable copy-তে ভেঙে দেয় — identity ছাড়া concept-এর জন্য। একই দোলনা, বিপরীত দিক। concept-এর প্রকৃতিই ঠিক করে কোন দিকে ঠেলতে হবে।
আরো দেখো
সম্পর্কিত পাঠ
Change Value to Reference: বিশটা ফটোকপি না, একটাই অফিস ফাইল
Change Value to Reference সহজ ভাষায় — একই entity-র ডুপ্লিকেট কপি কেন পুরনো হয়ে যায়, আর registry বা repository দিয়ে একটাই shared instance কীভাবে data consistent রাখে।
Replace Data Value with Object: তোমার Data-কে একটা নিজের ঘর দাও
Replace Data Value with Object সহজভাবে বোঝানো — কীভাবে একটা plain string বা number-কে validation আর behaviour সহ একটা ছোট class-এ রূপান্তর করতে হয়। TypeScript আর C# record-এর উদাহরণ দিয়ে।
Self Encapsulate Field: একজন দারোয়ান তোমার ডেটা পাহারা দিক
Self Encapsulate Field সহজভাবে বোঝানো — একটা class কেন তার নিজের field পড়া ও লেখার জন্য getter এবং setter ব্যবহার করে, নিরাপদ ধাপ, TypeScript ও C# উদাহরণসহ।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।