Replace Data Value with Object: তোমার Data-কে একটা নিজের ঘর দাও
Replace Data Value with Object সহজভাবে বোঝানো — কীভাবে একটা plain string বা number-কে validation আর behaviour সহ একটা ছোট class-এ রূপান্তর করতে হয়। TypeScript আর C# record-এর উদাহরণ দিয়ে।
নোটবুকের পেছনের পাতার নম্বর
ধরো রুবেল মিরপুরে একটা ছোট মোবাইল রিচার্জের দোকান চালায়। দোকানটা একটা টেইলারিং শপ আর একটা চায়ের দোকানের মাঝখানে গেঁথে আছে। ব্যবসা মোটামুটি চলছে, কিন্তু রুবেলের "customer database" হলো একটা পুরনো অঙ্কের খাতার পেছনের পাতা। পরে রিচার্জ করতে চাইলে সে পেনসিলে নম্বর লিখে রাখে: "সুমাইয়া আপা ০১৮২৩০...", "করিম ভাই ০১৮২২ কিছু একটা", "মোটা গোঁফওয়ালা আঙ্কেল ০১৮২৩০৫৪৬৭"।
সমস্যাটা দেখছো? একটা নম্বরে মাত্র নয়টা digit — সেটায় রিচার্জ হবে না, আর রুবেল জানতে পারবে তখনই যখন রাগী customer ফিরে আসবে। একটায় মাঝখানে ফাঁক আছে — সেটা কি space নাকি missing digit? আর "মোটা গোঁফওয়ালা আঙ্কেল" — তিনজন গোঁফওয়ালা করিম ভাই-এর মধ্যে কোনজন? প্রতিবার ডায়াল করার আগে রুবেলকে পেনসিলের দাগ দেখতে হয়, আঙুলে digit গুনতে হয়, আর আশা করতে হয় সব ঠিক আছে।
একদিন বিকেলে সবচেয়ে খারাপটা ঘটে। রুবেল ভুল নম্বরে ৩৯৯ টাকার রিচার্জ করে ফেলে। অচেনা কেউ বিনামূল্যে একমাসের data পেয়ে যায়, আর সুমাইয়া আপা কিছুই পান না। রুবেল টাকাও হারায়, তর্কেও হারে।
রুবেলের মেয়ে ফাতেমা, college-এর ছুটিতে বাড়ি এসে পুরো ব্যাপারটা দেখে চুপচাপ বাবার ফোন হাতে নেয়। সে একটা ঠিকঠাক contact card system সেট আপ করে দেয়। এখন প্রতিটা contact-এ আছে নাম, একটা 11-digit নম্বর যেটা একটা digit কম থাকলে ফোন save করতেই দেবে না, আর সুন্দর করে দেখায় +880 1823 054678। রুবেল যখন সুমাইয়া আপার জন্য "০১৮২৩০৫৪৬৭" save করতে যায়, ফোন সাথে সাথে বলে: একটা digit কম। ভুলটা ধরা পড়ে দরজাতেই, ক্ষতি হওয়ার আগে।
আর ফাতেমা রাতের খাবারে যেটা বোঝায় সেটা হলো: checking টা হয় একবারই, save করার সময়। তারপর থেকে ফোনের প্রতিটা অংশ — dialer, WhatsApp, SMS — শুধু contact card-কে trust করে। আর কেউ digit গোনে না। Dialer re-check করে না। WhatsApp re-check করে না। খারাপ নম্বর ঢুকতেই পারে না, কারণ খারাপ নম্বর contact card হতেই পারে না।
এটাই পুরো refactoring একটা গল্পে। Plain digits হিসেবে লেখা phone number হলো data value — উন্মুক্ত, unchecked, এর মানে শুধু রুবেলের মাথায় আছে। Contact card হলো object — সে নিজের rules আর নিজের behaviour নিয়ে যেখানে যায় সেখানে যায়।
Replace Data Value with Object বলছে: যখন একটা plain string বা number rules আর behaviour জমাতে শুরু করে, তখন scribble করা বন্ধ করো। Contact card বানাও।
Replace Data Value with Object আসলে কী?
Replace Data Value with Object হলো Martin Fowler-এর Refactoring বইয়ের একটা refactoring। দ্বিতীয় edition-এ এর নাম Replace Primitive with Object — একই ব্যাপার। কাজটা হলো: একটা field যেটা bare primitive — string, number, decimal — সেটাকে একটা নিজস্ব ছোট class-এ promote করো। Class টা ভেতরে raw value রাখবে, তৈরির সময় validate করবে, আর সেই value-সংক্রান্ত operations offer করবে।
এটা এত গুরুত্বপূর্ণ কেন? কারণ data behaviour আকর্ষণ করে। একটা field খুব কমই simple থাকে:
- Phone number হিসেবে রাখা string-এর দরকার হয় একটা 11-digit check। তারপর একটা formatting rule। তারপর receipts-এর জন্য masking rule (
0182XXXXX78)। priceনামের number-এর দরকার হয় rounding rules। তারপর currency। তারপর একটা rule যে এটা কখনো negative হতে পারবে না।
Value যদি primitive হিসেবেই থাকে, তাহলে এই সব logic কোথায় যাবে? এটা ছড়িয়ে পড়বে প্রতিটা class-এ যারা value-টা ব্যবহার করে। Order phone validate করে, Invoice আবার একটু আলাদাভাবে validate করে, SmsService তৃতীয়বার format করে। Phone number-এর নিজস্ব logic অন্যের বাড়িতে গিয়ে বাস করছে, duplicate হচ্ছে, আর ধীরে ধীরে আলাদা হয়ে যাচ্ছে। একদিন একটা copy 12 digit allow করবে, আরেকটা space allow করবে, আর রিচার্জ চলে যাবে ভুল জায়গায়।
এই refactoring সেই logic-কে তার একমাত্র নিজের ঘর দেয়। আর তুমি যে class বানাবে সেটা সাধারণত একটা value object — ছোট, immutable, তার contents দিয়ে compare করা হয়। একই নম্বর সহ দুটো contact card একে অপরের বদলে ব্যবহার করা যায়, ঠিক যেমন দুটো ১০ টাকার নোট। এটাই value চিন্তার মূল প্রশ্ন, আর পরের দুটো refactoring-এও এটা আসবে:
"এটা কি THE same জিনিস, নাকি শুধু AN equal জিনিস?"
Phone number-এর জন্য "an equal জিনিস" যথেষ্ট — যে object-ই 01823054678 রাখুক না কেন, সে same। এটাই value equality। Customer-এর সাথে তুলনা করো, যেখানে কোন customer সেটা গুরুত্বপূর্ণ — সেটা identity। এই নিয়ে আরো পড়ো Change Value to Reference-এ।
Fowler এটাকে বইয়ের সবচেয়ে মূল্যবান refactoring-গুলোর একটা বলেন, আর তার প্রিয় value object-এর উদাহরণ হলো Money — একটা amount আর একটা currency, contents দিয়ে equal, immutable। শুধু একটা কথা মনে রাখো: একটা value-এ rules আসলে, সেই rules-এর একটা ছাদ দরকার। ছাদটা হলো একটা ছোট class।
একটু গভীরে যাই। "খারাপ নম্বর contact card হতে পারে না" — এর formal নাম হলো making illegal states unrepresentable। Primitive দিয়ে string type অসংখ্য invalid phone number allow করে, তাই প্রতিটা consumer-কে নিজেকে রক্ষা করতে হয়। Scattered defensive check মানেই সেই duplication যেটা এই refactoring সরায়। Value object-এ constructor-ই একমাত্র দরজা, আর সেটা পাহারায় থাকে। তাই একটা PhoneNumber instance দেখলেই বোঝা যায় — validation ইতিমধ্যে পাস হয়েছে। Type theorist-রা এটাকে বলে "parse, don't validate": অবিশ্বস্ত input-কে boundary-তে একবারেই richer type-এ convert করো, তারপর সেই type-ই সব জায়গায় guarantee বহন করে নিয়ে যাক।
কখন এটা দরকার?
এই refactoring সরাসরি Primitive Obsession-এর ওষুধ — raw string আর number দিয়ে rich domain idea represent করার smell। এই চিহ্নগুলো দেখলে কাজে লাগাও:
- Repeated validation. একই "এটা কি valid phone number?" check তিনটা file-এ আছে। প্রতিটা copy আলাদা হয়ে যেতে পারে। একদিন একটা copy 12 digit allow করবে আর bug hunt শুরু হবে।
- Value-এর behaviour ভুল class-এ আছে।
Order.customerInitials(),Invoice.maskPhone(),Report.formatAmount()— host class গুলো নিজের না হওয়া কাজ করছে। - Primitive parameter গুলিয়ে যাচ্ছে।
sendSms(phone: string, message: string)— ভুল order-এ দিলে compiler কিছু বলবে না।sendSms(phone: PhoneNumber, message: string)এই ভুল impossible করে দেয়। - Unit বা format সহ value — money, percentage, দূরত্ব, string হিসেবে date, PIN code। একটা bare
500জানে না এটা টাকা নাকি পয়সা। - কয়েকটা primitive সবসময় একসাথে চলে — রাস্তা, শহর, ZIP — এটাই Data Clumps smell। একটা clump-কে একটা object-এ bundle করাও এই refactoring-এর কাজ।
কখন করবে না? যখন value-এর কোনো rule নেই, কোনো behaviour নেই। একটা loop index, temporary count, internal flag — এগুলো wrap করলে একটা class বাড়বে, লাভ কিছু হবে না। আর মনে রেখো boundary-এর খরচ: নতুন type-কে JSON আর database-এর জন্য map করতে হবে। Value-টাকে সেই খরচের যোগ্য হতে হবে সে যে bug গুলো ঠেকাবে তার মাধ্যমে।
কোন value নিজের class পাওয়ার যোগ্য? এই table দেখো:
| Candidate | Rules আছে? | Behaviour আছে? | সিদ্ধান্ত |
|---|---|---|---|
| Phone number | ✅ 11 digit, নির্দিষ্ট শুরু | ✅ format, mask | Promote করো |
| Money amount | ✅ negative হবে না, পয়সা rounding | ✅ add, percent, display | Promote করো |
| Email address | ✅ shape check | ✅ domain part, normalize | Promote করো |
Loop counter i | ❌ | ❌ | সংখ্যাই থাকুক |
Boolean flag isOpen | ❌ | ❌ | একাই থাকুক |
| PIN code | ✅ 6 digit | ✅ city lookup | Promote করো |
Before আর After — এক নজরে
রুবেলের দোকান, TypeScript-এ। Before — phone number একটা naked string, আর Customer নিজেই phone-এর কাজ করছে:
// BEFORE — a scribble in a notebook
class Customer {
constructor(
public name: string,
public phone: string, // any string at all!
) {}
isPhoneValid(): boolean {
return /^[6-9]\d{9}$/.test(this.phone); // phone logic inside Customer
}
maskedPhone(): string {
return this.phone.slice(0, 2) + "XXXXX" + this.phone.slice(7); // and again
}
}
const c = new Customer("Sunita", "982305467"); // 9 digits — accepted silently!After — phone number নিজেই নিজেকে check করা একটা contact card হয়ে গেছে:
// AFTER — a proper contact card
class PhoneNumber {
private readonly digits: string;
constructor(raw: string) {
const cleaned = raw.replace(/[\s-]/g, "");
if (!/^[6-9]\d{9}$/.test(cleaned)) {
throw new Error(`Invalid Indian mobile number: ${raw}`);
}
this.digits = cleaned;
}
toString(): string {
return `+91 ${this.digits.slice(0, 5)} ${this.digits.slice(5)}`;
}
masked(): string {
return this.digits.slice(0, 2) + "XXXXX" + this.digits.slice(7);
}
equals(other: PhoneNumber): boolean {
return this.digits === other.digits; // value equality — AN equal thing
}
}
class Customer {
constructor(
public readonly name: string,
public readonly phone: PhoneNumber,
) {}
}
const phone = new PhoneNumber("982305467"); // throws AT ONCE — bug caught at the doorতিনটা উন্নতি দেখো। নয়-digit bug এখন তৈরির মুহূর্তেই ধরা পড়ে, রিচার্জের সময় না। Masking আর formatting logic চলে গেছে সেই type-এ যার এটা নিজের। আর Customer ছোট হয়েছে — সে আর অন্যের কাজ করছে না।
ধাপে ধাপে, নিরাপদ উপায়ে
ছোট ছোট step, প্রতিটা step-এর পরে test green। Fowler যে order recommend করেন:
-
আগে field-কে self-encapsulate করো। যদি অনেক method সরাসরি
this.phoneছোঁয়, তাদের একটা getter আর setter দিয়ে route করো (দেখো Self Encapsulate Field)। এখন আসছে পরিবর্তন একটাই জায়গায় লাগবে। -
নতুন class তৈরি করো শুধু raw value নিয়ে। এখনো behaviour না। Constructor-এ primitive store করো, আর একটা getter দাও।
class PhoneNumber {
constructor(private readonly raw: string) {}
get value(): string {
return this.raw;
}
}-
Host-এর field type বদলাও নতুন class-এ। Host-এর constructor আর setter update করো incoming primitive wrap করতে:
this.phone = new PhoneNumber(raw)। Host-এর getter এখনো primitive return করুক (return this.phone.value), যাতে কোনো caller এখনো বদলাতে না হয়। Compile করো, test করো। -
নতুন class-এর constructor-এ validation যোগ করো। এখন invalid value দরজাতেই মরবে। Test run করো — কোনো test fail করলে মানে তুমি এমন জায়গা আবিষ্কার করেছো যেখানে invalid data তৈরি হচ্ছিল।
-
Behaviour সরাও, একটা method একবারে।
CustomerথেকেmaskedPhone()নাও,PhoneNumber-এmasked()হিসেবে তৈরি করো, পুরনো method-টাকে delegate করাও, test করো, তারপর পুরনো method মুছে দাও। Codebase-এ phone logic-এর প্রতিটা টুকরার জন্য এটা করো। -
Caller-গুলো upgrade করো object ব্যবহার করতে। ধীরে ধীরে signature বদলাও
phone: stringথেকেphone: PhoneNumber-এ, compiler প্রতিটা জায়গা দেখিয়ে দেবে। সবশেষে equality আর immutability ঠিক করো — সাধারণতreadonlyfield আর একটাequalsmethod, সেটাকে true value object বানাতে।
সবচেয়ে ঝুঁকির মুহূর্ত হলো step 4। Validation যোগ করা technically একটা behaviour change — আগে যা slip করত এখন সেটা throw করবে। Deploy করার আগে তোমার database আর logs-এ existing invalid value খোঁজো। অনেক team প্রথমে "lenient mode" রাখে যেটা throw না করে log করে এক সপ্তাহ, তারপর dirty data ঠিক করে, তারপর constructor strict করে।
একটা বড় বাস্তব উদাহরণ
ধরো একটা school-এর fee system-এ টাকার পরিমাণ bare number হিসেবে রাখা আছে। Accountant তারিক ভাই দুবার মার খেয়েছেন। একবার late-fee পয়সা rounding ভুল হয়ে তিনশো receipt এক পয়সা করে কম-বেশি হয়ে গেছে, auditor খুশি হননি। আর একবার ভুলে refund করতে গিয়ে fee negative হয়ে গেছে — student-এর বাবা bill পেয়েছেন যেখানে লেখা school তাদের টাকা দেবে। Money-এর একটা class দরকার:
// BEFORE — money as a naked number
class FeeAccount {
private balance = 0; // rupees? paise? who knows
charge(amount: number): void {
this.balance = this.balance + amount;
}
payOnline(amount: number): void {
const fee = amount * 0.02; // gateway fee, float maths — 0.1 + 0.2 problems!
this.balance = this.balance - amount + fee;
}
}Money-কে value object-এ promote করার পরে — পয়সা integer হিসেবে store করা হচ্ছে, float-এর ঝামেলা নেই, আর nonsense refuse করছে:
// AFTER — Money owns its rules
class Money {
private constructor(private readonly paise: number) {
if (!Number.isInteger(paise)) throw new Error("Paise must be whole");
if (paise < 0) throw new Error("Money cannot be negative");
}
static fromRupees(rupees: number): Money {
return new Money(Math.round(rupees * 100));
}
add(other: Money): Money {
return new Money(this.paise + other.paise); // returns NEW value
}
subtract(other: Money): Money {
return new Money(this.paise - other.paise); // negative? constructor catches it
}
percent(rate: number): Money {
return new Money(Math.round((this.paise * rate) / 100));
}
equals(other: Money): boolean {
return this.paise === other.paise;
}
toString(): string {
return `Rs. ${(this.paise / 100).toFixed(2)}`;
}
}
class FeeAccount {
private balance = Money.fromRupees(0);
charge(amount: Money): void {
this.balance = this.balance.add(amount);
}
payOnline(amount: Money): void {
const gatewayFee = amount.percent(2);
this.balance = this.balance.subtract(amount).add(gatewayFee);
}
}পুরনো প্রতিটা bug-এর এখন দরজায় পাহারাদার আছে। Float rounding? গেছে — পয়সা integer। Careless refund-এ negative balance? Constructor throw করবে। টাকা-বনাম-পয়সা confusion? Type বলছে Money, আর ঢোকার একমাত্র রাস্তা fromRupees। আর দেখো style-টা: add আর subtract পুরনো object বদলায় না, নতুন Money object return করে। এই immutability-ই value object-কে নিরাপদ করে — এটা সেই contact card যেটায় কেউ scribble করতে পারে না।
একটু গভীরে ভাবো: পয়সার জন্য integer কেন, number rupee কেন না? কারণ IEEE-754 floating point বেশিরভাগ decimal fraction exactly represent করতে পারে না — 0.1 + 0.2 হয় 0.30000000000000004। Money-র জন্য professional solution: সবচেয়ে ছোট currency unit integer-এ রাখো, অথবা decimal type ব্যবহার করো (C#-এ decimal, Java-তে BigDecimal, Python-এ Decimal)। মূল শিক্ষা: representation choice নিজেই একটা rule, আর value object হলো সেই rule রাখার সঠিক জায়গা।
C#-এ একই refactoring
C# এখানে দারুণ — কারণ record গুলো value object-এর জন্যই বানানো। একটা record দেয় value equality, GetHashCode, init দিয়ে immutability, readable ToString, আর with দিয়ে non-destructive update — সব compiler generate করে:
public sealed record PhoneNumber
{
public string Digits { get; }
public PhoneNumber(string raw)
{
var cleaned = raw.Replace(" ", "").Replace("-", "");
if (!Regex.IsMatch(cleaned, @"^[6-9]\d{9}$"))
throw new ArgumentException($"Invalid Indian mobile number: {raw}");
Digits = cleaned;
}
public string Masked => $"{Digits[..2]}XXXXX{Digits[7..]}";
public override string ToString() => $"+91 {Digits[..5]} {Digits[5..]}";
}
// Value equality comes FREE with records:
var a = new PhoneNumber("98230 54678");
var b = new PhoneNumber("9823054678");
Console.WriteLine(a == b); // True — equal by contents
Console.WriteLine(a.Equals(b)); // Trueআর Money compact readonly record struct হিসেবে — heap allocation নেই, full value semantics:
public readonly record struct Money(long Paise)
{
public static Money FromRupees(decimal rupees) =>
new(checked((long)Math.Round(rupees * 100)));
public Money Add(Money other) => new(Paise + other.Paise);
public Money Percent(decimal rate) =>
new((long)Math.Round(Paise * rate / 100));
public override string ToString() => $"Rs. {Paise / 100m:F2}";
}পুরনো উপায়ের সাথে তুলনা করো — হাতে Equals, GetHashCode, operator == লিখে প্রতিটা field change-এর সাথে sync রাখা। Record এই পুরো category-র boilerplate bug সরিয়ে দিয়েছে। Microsoft-এর নিজের .NET DDD guidance এরকমই value object recommend করে। EF Core-এ value converter বা owned entity type দিয়ে map করতে পারো — database দেখবে simple column কিন্তু তোমার code দেখবে rich type।
একটু গভীরে: structural equality বনাম reference equality হলো এখানকার precise vocabulary। C# class default হলো reference equality — দুটো object equal শুধু যদি তারা একই heap object হয়। record (আর struct) implement করে structural equality — সব field equal হলেই equal। Value object-এর দরকার structural equality, আর GetHashCode অবশ্যই একই field থেকে বের হতে হবে। কারণ hash-based collection আগে hash দিয়ে bucket করে, তারপর Equals call করে। দুটো structurally equal object আলাদা hash করলে একটা HashSet দুটোই রাখতে পারে আর Dictionary lookup miss করতে পারে — চুপচাপ। এই Equals-hashCode contract হলো সবচেয়ে common hand-rolled value object bug, আর এটাই সবচেয়ে বড় argument compiler-কে দিয়ে দুটোই generate করানোর।
Python-এর version, সম্পূর্ণতার জন্য — @dataclass(frozen=True) হলো Python-এর record:
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
paise: int
def __post_init__(self) -> None:
if self.paise < 0:
raise ValueError("Money cannot be negative")
@staticmethod
def from_rupees(rupees: float) -> "Money":
return Money(round(rupees * 100))
def add(self, other: "Money") -> "Money":
return Money(self.paise + other.paise)
def __str__(self) -> str:
return f"Rs. {self.paise / 100:.2f}"
# frozen=True gives immutability; dataclass gives __eq__ by contents;
# eq=True with frozen=True also makes it hashable — dict-key safe.
assert Money.from_rupees(15) == Money.from_rupees(15) # AN equal thingIDE কীভাবে সাহায্য করে
"Replace Data Value with Object" নামে একটা single button নেই, কারণ এই refactoring ছোট ছোট moves-এর একটা recipe। IDE প্রতিটা উপাদান automate করে:
| Tool | কাজের moves |
|---|---|
| Visual Studio / Rider (C#) | Extract Class আর Encapsulate Field শুরু করতে; Change Signature string থেকে PhoneNumber-এ সব caller-এ বদলাতে; class থেকে record-এ convert করার quick-fix। |
| IntelliJ IDEA (Java) | Refactor → Extract Delegate একটা field আর তার method নতুন class-এ নিয়ে যায়; Type Migration field-এর type বদলে fallout chase করে। |
| VS Code (TypeScript) | Rename Symbol পুরনো field নিরাপদে rename করতে; type change-এর পরে compiler error-গুলোই precise to-do list হিসেবে কাজ করে। |
| সব IDE | Find Usages primitive field-এ শুরু করার আগে পুরো blast radius দেখায়। |
TypeScript-এ একটা কার্যকর trick: আগে field-এর type বদলাও আর red squiggle দেখাতে দাও। Compiler তোমার checklist হয়ে যাবে — প্রতিটা error মানে একটা caller এখনো raw string দিচ্ছে।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Validation একবারই চলে, তৈরির সময় — invalid value হওয়াই অসম্ভব। | একটা নতুন type লিখতে হবে, test করতে হবে, JSON/database boundary-তে map করতে হবে। |
| Behaviour তার data-এর সাথে থাকে; host class-গুলোতে duplication মিলিয়ে যায়। | Trivial value over-wrap করলে noise বাড়বে, safety না। |
Domain vocabulary পায়: PhoneNumber, Money, PinCode — anonymous string না। | Existing dirty data strict validation আসলে ঝামেলা করতে পারে — আগে migrate করো। |
Compiler parameter mix-up থামায় (Money যেখানে PhoneNumber দরকার সেখানে দেওয়া যাবে না)। | খুব hot loop-এ সামান্য allocation cost (C# readonly record struct এটা solve করে)। |
Full value-object design-এর natural first step — equality, immutability, with-update। | Sharing নিয়ে ভাবতে হবে: update সব জায়গায় দেখাতে হলে Change Value to Reference লাগবে পরে। |
দুটো chart দিয়ে trade-off সৎভাবে দেখো।
এই quadrant chart পরের দুটো lesson-এর ইঙ্গিত দিচ্ছে। নিচে-বাঁয়ে থাকা concept গুলো entity — সেগুলো shared reference-এর পেছনে থাকে। উপরে-ডানে থাকা concept গুলো value — সেগুলো freely copy হওয়া উচিত। এই দুই কোণের মাঝে যাতায়াত মানেই Change Value to Reference আর Change Reference to Value।
কোন smell গুলো সারায়?
| Smell | কীভাবে সাহায্য করে |
|---|---|
| Primitive Obsession | এটাই মূল ওষুধ — rich concept অবশেষে real type আর real rules পায়। |
| Data Clumps | একই move group-এ: রাস্তা + শহর + ZIP মিলে একটা Address object। |
| Duplicate Code | Validation আর formatting-এর scattered copy গুলো একটা class-এ মিলে যায়। |
| Large Class | Host class গুলো সেই responsibility ছেড়ে দেয় যেটা কখনো তাদের ছিল না। |
| Shotgun Surgery | Phone formatting বদলাতে হলে PhoneNumber-এ একটা edit — app জুড়ে দশটা না। |
দ্রুত revision
+--------------------------------------------------------------+
| REPLACE DATA VALUE WITH OBJECT — REVISION |
+--------------------------------------------------------------+
| Idea : A primitive with rules/behaviour becomes a small |
| class (scribbled number -> contact card). |
| AKA : Replace Primitive with Object (Fowler, 2nd ed.) |
| Signs : repeated validation, value logic in host classes, |
| parameter mix-ups, units/format confusion. |
| Steps : 1. Self-encapsulate the field |
| 2. Create wrapper class (raw value + getter) |
| 3. Host stores the object, callers unchanged |
| 4. Add validation in constructor |
| 5. Move behaviour in, one method at a time |
| 6. Make it immutable + value equality |
| Key idea : value object = "AN equal thing is good enough" |
| C# bonus : record / readonly record struct = value object |
| with equality and immutability for FREE. |
| Cures : Primitive Obsession, Data Clumps, duplication. |
+--------------------------------------------------------------+নিজে চেষ্টা করো
ধরো ঢাকার একটা train app-এর ticket system-এ সব কিছু primitive হিসেবে রাখা আছে:
class Ticket {
constructor(
public passengerName: string,
public fromStation: string,
public toStation: string,
public fareRupees: number, // sometimes someone passes paise by mistake!
public classCode: string, // "I" or "II" — but "VIP" sneaks in sometimes
) {}
fareWithSurcharge(): number {
return this.fareRupees * 1.05;
}
display(): string {
return `${this.fromStation} -> ${this.toStation} (${this.classCode}) Rs.${this.fareRupees}`;
}
}তোমার কাজ:
fareRupees-কে একটাMoneyvalue object-এ promote করো যেটা পয়সা integer-এ রাখে, negative amount refuse করে, আরwithSurcharge(percent)offer করে যেটা নতুনMoneyreturn করে।classCode-কে একটাTravelClasstype-এ promote করো যেটা শুধু first বা second class allow করে। Hint: private constructor আর দুটো static instance সহ একটা class, অথবা validator দিয়ে wrap করা TypeScript union type।display-এ fare formattingMoney.toString()-এ নিয়ে যাও।- একটা ছোট test লেখো যেটা prove করে দুটো
Money.fromRupees(15)object equal — "AN equal thing", "THE same thing" না। - C#-এ bonus:
Money-কেreadonly record structহিসেবে আরTravelClass-কে staticFirstআরSecondinstance সহrecordহিসেবে implement করো। Count করো compiler তোমার জন্য কতটুকু equality code লিখেছে। উত্তর: সবটুকু। - চিন্তার প্রশ্ন: app-টায়
passengerName-ও আছে। এটাকেও কি value object বানানো দরকার? এই article-এর sorting table apply করো — এই app-এ কি name-এর rules বা behaviour আছে? না থাকলে, কোন future requirement যেমন ticket-এ initials print করা, সিদ্ধান্ত বদলে দেবে?
যদি তোমার Ticket constructor-কে আর পয়সায় fare দিয়ে বা "VIP" class দিয়ে call করা না যায় — শুধু তোমার মনে রাখা check-এর কারণে না, বরং কারণ types-ই refuse করছে — তাহলে তুমি ফাতেমা রুবেলের নোটবুকের জন্য যা করেছিল সেটাই করেছো: ভুলটা save করা impossible করে দিয়েছো।
সচরাচর জিজ্ঞাসা
- 'Replace Primitive with Object' আর এটা কি একই জিনিস?
- হ্যাঁ, একই জিনিস। Fowler-এর Refactoring বইয়ের প্রথম edition-এ নাম ছিল Replace Data Value with Object। দ্বিতীয় edition-এ নাম হয়েছে Replace Primitive with Object। ওষুধ একই, label নতুন — একটা bare string বা number যখন rules পেতে শুরু করে, তখন সেটাকে একটা ছোট class-এ promote করো।
- কোন value তার নিজের class পাওয়ার যোগ্য হয়েছে সেটা কীভাবে বুঝবো?
- তিনটা লক্ষণ দেখো: value-টার rules আছে (phone number-এ 11 digit থাকতে হবে), value-টার behaviour আছে (format করো, compare করো, mask করো), অথবা একই check যেখানেই value যাচ্ছে সেখানে copy-paste হচ্ছে। একটা লক্ষণ দেখলেই ভাবতে শুরু করো — দুটো বা তিনটা দেখলে কাজে লেগে পড়ো।
- এতে কি শত শত ছোট class হয়ে যাবে না?
- না, কারণ বেশিরভাগ value-এর কোনো rule-ই নেই। একটা loop counter সংখ্যাই থাকবে। তুমি শুধু সেই value-গুলো wrap করবে যেগুলো নিয়ে business আসলেই care করে — phone number, money, email, PIN code। এগুলো সাধারণত মুষ্টিমেয়, আর প্রতিটা দশ-বিশটা scattered check সরিয়ে দেয়।
- নতুন object টা mutable হবে নাকি immutable?
- প্রায় সবসময় immutable হওয়া উচিত। একটা value object একটা মান represent করে — যেমন সংখ্যা ৭। তুমি '৭-কে বদলাও' না, তুমি অন্য সংখ্যা ব্যবহার করো। Immutability একটা free safety দেয়: অন্য কেউ তোমার phone number নষ্ট করতে পারবে না। C# record আর TypeScript-এর readonly field এটা সহজ করে দেয়।
- Database বা JSON-এ save করার সময় কী করবো?
- এটাই এই refactoring-এর আসল খরচ। তোমার ORM বা serializer-কে শেখাতে হবে কীভাবে object-টাকে একটা column বা string-এ map করবে। বেশিরভাগ tool এটা ভালোভাবেই support করে (যেমন EF Core-এ value converter আর owned type আছে), কিন্তু boundary-তে mapping-এর জন্য একটু সময় রেখো।
আরো দেখো
সম্পর্কিত পাঠ
Self Encapsulate Field: একজন দারোয়ান তোমার ডেটা পাহারা দিক
Self Encapsulate Field সহজভাবে বোঝানো — একটা class কেন তার নিজের field পড়া ও লেখার জন্য getter এবং setter ব্যবহার করে, নিরাপদ ধাপ, TypeScript ও C# উদাহরণসহ।
Change Value to Reference: বিশটা ফটোকপি না, একটাই অফিস ফাইল
Change Value to Reference সহজ ভাষায় — একই entity-র ডুপ্লিকেট কপি কেন পুরনো হয়ে যায়, আর registry বা repository দিয়ে একটাই shared instance কীভাবে data consistent রাখে।
Change Reference to Value: যেকোনো ১০ টাকার নোটই সমান
Change Reference to Value সহজভাবে বোঝানো হয়েছে — একটা shared, mutable reference object-কে কীভাবে content-based equality সহ একটা ছোট immutable value object-এ রূপান্তর করতে হয়, TypeScript আর C# record-এর উদাহরণসহ।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।