Change Value to Reference: বিশটা ফটোকপি না, একটাই অফিস ফাইল
Change Value to Reference সহজ ভাষায় — একই entity-র ডুপ্লিকেট কপি কেন পুরনো হয়ে যায়, আর registry বা repository দিয়ে একটাই shared instance কীভাবে data consistent রাখে।
📄 রহিমের বিশটা ফটোকপি
ধরো রহিম ঢাকার একটা স্কুলে সপ্তম শ্রেণীতে পড়ে। তার record — ঠিকানা, blood group, বাবার phone number — স্কুলের অফিসের একটা ফাইলে আছে। সেটা maintain করেন কেরানি জামাল সাহেব, বাইশ বছর ধরে সেই অফিস চালাচ্ছেন। এখন কল্পনা করো স্কুলটা একটা অদ্ভুত কাজ করলো: প্রতিটা classroom, sports room, library, আর canteen প্রত্যেকে রহিমের record-এর নিজস্ব ফটোকপি রাখলো। বিশটা ফটোকপি, সব ঝকঝকে, সব একই রকম, সব সঠিক — যেদিন তৈরি হয়েছিল সেদিন।
কিছুদিন কোনো সমস্যা নেই। তারপর রহিমের পরিবার বাসা বদললো, মিরপুর থেকে উত্তরায়। তার বাবা অফিসে এসে ঠিকানা update করলেন। জামাল সাহেব যত্ন করে অফিস ফাইলটা ঠিক করলেন। অফিস ফাইল এখন সঠিক... কিন্তু library-র ফটোকপিতে এখনো মিরপুর। sports room-এরটাতেও। বাকি সতেরোটাতেও, drawer-এ চুপচাপ বসে আছে, প্রত্যেকটা এখন আত্মবিশ্বাসের সাথে ভুল।
library যখন বই ফেরত না দেওয়ার reminder পাঠালো, সেটা গেলো পুরনো বাসায় মিরপুরে। সেখানে একজন অবাক নতুন ভাড়াটে পড়লেন এমন একটা বইয়ের কথা যেটা তিনি কখনো নেননি। আরো খারাপ: বিকেলে রহিম football খেলতে গিয়ে পা মচকালো, স্কুল nurse-এর এখনই তার বাবার phone number দরকার। তার সামনে তিনটা ফটোকপি আছে — sports room, medical room, আর তার নিজের drawer থেকে — আর সেগুলোতে দুটো আলাদা number লেখা। কাঁদতে থাকা বাচ্চাকে সামনে রেখে কোনটা বিশ্বাস করবেন?
ফটোকপিগুলো পুরনো হয়ে গেছে। আর এর কারণটা লক্ষ্য করো: দুনিয়ায় একজনই আসল রহিম আছে। বিশটা কাগজ তাকে হওয়ার ভান করছিল, কিন্তু যখনই তার details বদলালো, ভানটা ভেঙে পড়লো। পরিবর্তনশীল কিছুর কপিগুলো সবসময় আলাদা হয়ে যায়। এটা দুর্ভাগ্য না, এটা math — প্রতিটা update এখন বিশটা জায়গায় পৌঁছাতে হবে। আর প্রথম মিস হওয়া delivery-ই সত্যের দুটো version তৈরি করে।
প্রতিটা স্কুল যে সমাধান ব্যবহার করে: অফিসে একটা ফাইল, আর সবাই সেটাকে refer করে। library রহিমের ঠিকানা রাখে না, রাখে "রহিম, ভর্তি নম্বর ১০২৪ — অফিস ফাইল দেখুন।" ঠিকানা বদলালে জামাল সাহেব একটা জায়গায় বদলান, আর প্রতিটা বিভাগ তাৎক্ষণিকভাবে নতুন সত্য দেখতে পায়, কারণ তারা সবাই একই ফাইল দেখছে।
কোডে এটাই হলো Change Value to Reference: এমন entity-র নতুন কপি তৈরি করা বন্ধ করো যার একটা আসল identity আছে, আর সবাইকে একটাই single instance share করাও।
🧠 Change Value to Reference কী?
Change Value to Reference হলো Martin Fowler-এর Refactoring বই থেকে একটা refactoring। পরিস্থিতিটা এরকম: তোমার program অনেকগুলো আলাদা object তৈরি করছে যেগুলো সবাই একই বাস্তব জিনিস represent করে। ধরো customer C-1-এর দশটা order আছে। প্রত্যেক order নিজের Customer object বানাচ্ছে। প্রতিটা কপি জন্মের সময় সঠিক আর শীঘ্রই ভুল হয়ে যায় — কারণ আসল customer-এর data বদলায়, কিন্তু কপিগুলো সে খবর পায় না।
সমাধানটা হলো: সমস্ত creation একটা single access point-এর মাধ্যমে পাঠাও — একটা factory, registry, বা repository — যেটা একই identity-র জন্য চাইলে প্রতিবার একই object ফেরত দেয়। Refactoring-এর পর দশটা order সবাই একটাই Customer instance-কে point করবে। যেকোনো order-এর মাধ্যমে তার credit limit update করো, অন্য সব order সেটা তাৎক্ষণিকভাবে দেখবে — কারণ তারা সবাই একই অফিস ফাইল দেখছে।
এই refactoring আসলে একটা গভীর প্রশ্নের পাঠ:
"এটা কি THE same জিনিস, নাকি শুধু AN equal জিনিস?"
- একটা value হলো "একটা equal জিনিস"। যেকোনো ১০ টাকার amount অন্য যেকোনো ১০ টাকার সমান। কপি করা ক্ষতিকর না। তারিখ, টাকা, phone number, coordinates — এগুলোতে value equality আছে।
- একটা reference (domain-driven design ভাষায় entity) হলো "THE same জিনিস"। ঠিক একজন customer C-1 আছে, ভর্তি নম্বর ১০২৪ সহ একজনই ছাত্র আছে। এগুলোর identity আছে। C-1 দাবি করে এমন দুটো object হলো bug হওয়ার অপেক্ষায় একটা মিথ্যা।
Refactoring Guru সুন্দরভাবে বলেছে: reference হলো যখন একটা বাস্তব-জগতের object program-এ একটা object-এর সাথে correspond করে (user, order, product)। Value হলো যখন একটা বাস্তব-জগতের concept অনেক ছোট ছোট program object হিসেবে থাকে (তারিখ, পরিমাণ, ঠিকানা)। Eric Evans-এর DDD বই entity আর value object-এর মধ্যে এই একই লাইন টানে।
পরীক্ষাটা হলো mutability আর identity। দুটো প্রশ্ন করো: (১) বাস্তব জগতে এই জিনিসটার কি একটাই আসল identity আছে? (২) এর data কি বদলাতে পারে, আর সবাইকে কি সেই পরিবর্তন দেখতে হবে? দুটোই হ্যাঁ = এটাকে reference করো, একটা access point-এর মাধ্যমে share করো। দুটোই না = এটাকে value রাখো আর twin article পড়ো, Change Reference to Value।
কলেজ কর্নার: formal ভাষায়, ফটোকপির সমস্যা হলো replication-এর অধীনে একটা consistency সমস্যা। Distributed systems course-এ শেখায় যে যখনই তুমি mutable state replicate করো, তোমাকে একটা update-propagation strategy বেছে নিতে হবে, আর প্রতিটা strategy-তে cost আছে — এটাই CAP theorem-এর মূল কথা। একটা process-এর মধ্যে, Change Value to Reference হলো সবচেয়ে সস্তা strategy: আদৌ replicate করো না। একটা canonical instance মানে consistency বিনামূল্যে, কারণ synchronize করার কিছুই নেই। এটা enforce করার pattern হলো Identity Map (Fowler-এর Patterns of Enterprise Application Architecture-এ catalogued): identity key থেকে instance-এর একটা table, যেকোনো construction-এর আগে দেখা হয়।
🔔 কখন এটা দরকার?
এই পরিস্থিতিগুলো দেখলে বুঝবে:
১. Stale-copy bug। Support একটা screen-এ customer-এর email update করলো; অন্য screen এখনো পুরনোটা দেখছে আর ব্যবহার করছে, কারণ প্রতিটা screen নিজের object বানিয়েছিল। Classic ফটোকপির লক্ষণ — দুটো phone number সহ nurse।
২. Mutation যেটা propagate করতে হবে। Loyalty tier upgrade, বদলানো credit limit, সংশোধিত ঠিকানা — যখন program-এর একটা অংশ লেখে আর অন্য অংশগুলোকে নতুন value পড়তে হয়, কপিগুলো structurally ভুল।
৩. একটা value object যেটা বড় হয়েছে। প্রায়ই এটা Replace Data Value with Object-এর পরে আসে: তুমি customer name-কে Customer class-এ wrap করলে (ভালো!), কিন্তু তারপর Customer-এ purchase history-র মতো mutable state যোগ হলো। একটা value যেটা পরিবর্তনশীল, shared data জমা করে সেটা আসলে reference হতে চাইছে।
৪. ভারী duplicate থেকে memory অপচয়। দশ হাজার order প্রত্যেকে একই ২০০-field-এর Customer-এর নিজস্ব কপি রাখা সম্পূর্ণ অপচয়। একটা shared instance আর দশ হাজার pointer অনেক সস্তা।
৫. Collection-এ identity confusion। Code চেষ্টা করছে "কতজন distinct customer" গণনা করতে, কিন্তু ভুল উত্তর পাচ্ছে কারণ একই ব্যক্তি পাঁচটা আলাদা object হিসেবে আছে।
এই refactoring পরোক্ষভাবে কিছু smell-ও সারায়। Primitive Obsession প্রায়ই loose string-এ identity লুকিয়ে রাখে (customerId সর্বত্র raw pass হচ্ছে, entity সব জায়গায় rebuild হচ্ছে)। আর প্রতিটা order-এ copy করা customer field-এর Data Clumps ঠিক সেই ফটোকপিগুলোই যেগুলো এই refactoring সরিয়ে দেয়।
কখন ব্যবহার করবে না: object ছোট, immutable, আর identity-মুক্ত হলে (টাকা, একটা তারিখ) sharing কিছুই দেয় না আর machinery-র দাম পড়ে। এটাই হলো inverse refactoring-এর সাথে seesaw — নিচে Benefits and risks section-এ ঠিকমতো বুঝবো।
দ্রুত triage-এর জন্য একটা symptom-to-diagnosis টেবিল:
| তুমি যে symptom দেখছো | সম্ভাব্য কারণ | এই refactoring কাজে লাগবে? |
|---|---|---|
| দুটো screen একজন customer সম্পর্কে দ্বিমত করে | প্রতিটা screen নিজের কপি বানিয়েছে | ✅ হ্যাঁ — একটা instance share করো |
| Distinct-customer count অনেক বেশি | একই ব্যক্তি পাঁচটা object হিসেবে | ✅ হ্যাঁ — প্রতিটা identity-র জন্য একটা object |
| একটা money amount "নিজে থেকে" বদলে গেছে | Shared mutable value | ❌ না — inverse refactoring দরকার |
| একটা batch-এ update অন্য batch-এ দেখা যাচ্ছে না | প্রতিটা batch-এ আলাদা কপি | ✅ হ্যাঁ — প্রতিটা identity-র জন্য registry |
| Test একা পাস করে, একসাথে fail করে | Static registry test-এর মধ্যে leak করছে | ⚠️ Registry-কে injected repository-তে refactor করো |
👀 এক নজরে আগে ও পরে
TypeScript-এ দেখাই। আগে — প্রতিটা order brand-new একটা Customer বানাচ্ছে:
// BEFORE — every classroom photocopies the record
class Customer {
constructor(
public readonly id: string,
public name: string,
public loyaltyPoints: number = 0,
) {}
addPoints(points: number): void {
this.loyaltyPoints += points;
}
}
class Order {
public customer: Customer;
constructor(customerId: string, customerName: string) {
// A fresh Customer per order — even for the same person!
this.customer = new Customer(customerId, customerName);
}
}
const a = new Order("C-1", "Dana");
const b = new Order("C-1", "Dana");
a.customer.addPoints(100);
console.log(b.customer.loyaltyPoints); // 0 — b's photocopy never heard the news
console.log(a.customer === b.customer); // false — two objects pretending to be one personপরে — creation একটা repository-র মাধ্যমে যাচ্ছে যেটা প্রতিটা identity-র জন্য একটাই instance দেয়:
// AFTER — one office file, everyone refers to it
class Customer {
constructor(
public readonly id: string,
public name: string,
public loyaltyPoints: number = 0,
) {}
addPoints(points: number): void {
this.loyaltyPoints += points;
}
}
class CustomerRepository {
private readonly byId = new Map<string, Customer>();
getOrCreate(id: string, name: string): Customer {
let customer = this.byId.get(id);
if (!customer) {
customer = new Customer(id, name);
this.byId.set(id, customer);
}
return customer;
}
}
class Order {
public readonly customer: Customer;
constructor(repo: CustomerRepository, customerId: string, customerName: string) {
this.customer = repo.getOrCreate(customerId, customerName);
}
}
const repo = new CustomerRepository();
const a = new Order(repo, "C-1", "Dana");
const b = new Order(repo, "C-1", "Dana");
a.customer.addPoints(100);
console.log(b.customer.loyaltyPoints); // 100 — everyone sees the office file
console.log(a.customer === b.customer); // true — THE same thinga.customer === b.customer লাইনটাই এর মূল কথা। আগে: false — দুটো ফটোকপি। পরে: true — একটা shared ফাইল। বাস্তব জগতের identity এখন memory-তেও reflect হচ্ছে।
কলেজ কর্নার: সেই === check হলো reference equality — "এগুলো কি একই heap address?" Entity-র জন্য, reference equality হলো একটা process-এর মধ্যে ঠিক যেটা দরকার, কারণ এটা বাস্তব-জগতের identity-কে reflect করে। Process boundary পার হলে (JSON API, message queue), pointer travel করতে পারে না, তাই identity-কে key দিয়ে বহন করতে হবে — C-1 — আর arrive করে repository-র মাধ্যমে local instance-এ re-anchor করতে হবে। এই কারণেই entity সবসময় একটা explicit ID field বহন করে, value object সাধারণত করে না। ID হলো serializable করা identity।
🪜 ধাপে ধাপে, নিরাপদ উপায়ে
কীভাবে করবে? এই ধাপগুলো follow করো:
১. Identity key বেছে নাও। এই দুটোকে "THE same জিনিস" কী করে? সাধারণত একটা ID — ভর্তি নম্বর, customer code, NID নম্বর। কোনো natural key না থাকলে একটা বানাও। Key ছাড়া কোন request share করা উচিত সেটা বোঝার উপায় নেই।
২. Lookup-এর মালিক বেছে নাও। তিনটা common option: class-এ একটা static factory method (দ্রুত, কিন্তু global), একটা registry object, বা — real application-এর জন্য সবচেয়ে ভালো — তোমার application-এর তৈরি আর inject করা একটা repository (dependency injection)। Injected repository-ই পছন্দ করো; static registry test-গুলোকে একে অপরের সাথে লড়াই করায়।
৩. Constructor-এর পাশে access method যোগ করো। এখনো constructor সরাবে না। getOrCreate যোগ করো যেটা map check করে আর না থাকলেই বানায়:
class CustomerRepository {
private readonly byId = new Map<string, Customer>();
getOrCreate(id: string, name: string): Customer {
const existing = this.byId.get(id);
if (existing) return existing;
const fresh = new Customer(id, name);
this.byId.set(id, fresh);
return fresh;
}
}৪. new Customer(...) call site-গুলো একে একে repo.getOrCreate(...)-এ replace করো। প্রতিটা replacement-এর পর compile করো আর test করো। যে code কখনো customer mutate করে না, তার জন্য behavior এখনো একই।
৫. পিছনের দরজা বন্ধ করো। একবার কোনো caller সরাসরি new ব্যবহার না করলে, constructor-কে private করে দাও (C#-এ সহজ; TypeScript-এ class-এর বদলে একটা creation interface export করো, বা constructor private mark করো আর একটা static create expose করো যেটা শুধু repository module ব্যবহার করতে পারে)। এখন registry-ই একমাত্র প্রবেশপথ, আর one-instance-per-identity নিয়ম bypass করা যাবে না।
৬. Mutation path-গুলো audit করো। এটাই যে ধাপটা সবাই এড়িয়ে যায়। কিছু পুরনো code হয়তো independent copy থাকার উপর নির্ভর করছিল — যেমন একটা "what-if" calculation যেটা সাময়িকভাবে customer-এর discount tweak করতো। Sharing-এর পরে সেই tweak এখন সবার কাছে leak করবে। Shared object-এ প্রতিটা write খুঁজে বের করো আর জিজ্ঞেস করো: পুরো program এটা দেখবে? না হলে সেই code-কে explicit copy দাও।
Behavior পরিবর্তন লুকিয়ে আছে ধাপ ৬-এ। Refactoring-এর আগে "তোমার" customer mutate করা শুধু তোমাকে প্রভাবিত করতো। পরে customer mutate করা সবাইকে প্রভাবিত করবে যারা এটা ধরে আছে — real update-এর জন্য এটাই চাইছিলে, কিন্তু temporary local fiddling-এর জন্য এটা চাওনি। আরো: instance share হলে দুটো thread এখন একই object-এ race করতে পারে। তোমার code multi-threaded হলে synchronization যোগ করো বা mutation একটা জায়গায় সীমাবদ্ধ করো।
🏫 একটা বড় বাস্তব উদাহরণ
ধরো ঢাকার একটা coaching center management app batch আর student track করে। প্রতিটা Batch enrollment row থেকে নিজের Student object বানাচ্ছিল, তাই Physics আর Math-এ পড়া একই student দুইবার ছিল — আর একটা batch-এ record করা fee payment কখনো অন্যটাতে দেখা যাচ্ছিল না। Accountant প্রতিটা শনিবার এমন সংখ্যা মেলাতে কাটাতেন যেগুলো কখনোই আলাদা হওয়া উচিত ছিল না:
// BEFORE — each batch photocopies its students
class Student {
constructor(
public readonly admissionNo: string,
public name: string,
public feesPaid: number = 0,
) {}
payFees(amount: number): void {
this.feesPaid += amount;
}
}
class Batch {
public students: Student[] = [];
enroll(admissionNo: string, name: string): Student {
const s = new Student(admissionNo, name); // fresh copy every time!
this.students.push(s);
return s;
}
}
const physics = new Batch();
const maths = new Batch();
const rahimInPhysics = physics.enroll("A-1024", "Rahim");
const rahimInMaths = maths.enroll("A-1024", "Rahim");
rahimInPhysics.payFees(5000);
console.log(rahimInMaths.feesPaid); // 0 — the accountant is now confusedএকটা StudentDirectory introduce করার পরে (আমাদের অফিস ফাইল রুম, কাউন্টারের পেছনে জামাল সাহেব):
// AFTER — one directory, every batch refers to it
class StudentDirectory {
private readonly byAdmissionNo = new Map<string, Student>();
getOrRegister(admissionNo: string, name: string): Student {
let student = this.byAdmissionNo.get(admissionNo);
if (!student) {
student = new Student(admissionNo, name);
this.byAdmissionNo.set(admissionNo, student);
}
return student;
}
count(): number {
return this.byAdmissionNo.size; // distinct students — finally a true answer
}
}
class Batch {
public students: Student[] = [];
constructor(private readonly directory: StudentDirectory) {}
enroll(admissionNo: string, name: string): Student {
const student = this.directory.getOrRegister(admissionNo, name);
this.students.push(student); // a pointer to the one file, not a photocopy
return student;
}
}
const directory = new StudentDirectory();
const physics = new Batch(directory);
const maths = new Batch(directory);
physics.enroll("A-1024", "Rahim");
maths.enroll("A-1024", "Rahim");
physics.students[0].payFees(5000);
console.log(maths.students[0].feesPaid); // 5000 — both batches see the truth
console.log(directory.count()); // 1 — one Rahim, as in real lifeএকটা refactoring-এ দুটো bug মারা গেলো: fee payment এখন সব জায়গায় দৃশ্যমান, আর "আমাদের কতজন student আছে?" অবশেষে আসল সংখ্যা দিচ্ছে। লক্ষ্য করো directory প্রতিটা Batch-এ inject করা হয়েছে — static global না — তাই একটা test তার নিজের fresh directory বানাতে পারবে আর কখনো অন্য test-এর সাথে collide করবে না।
💜 C#-এ একই refactoring
C# আমাদের private constructor দিয়ে funnel enforce করতে দেয় — কেউ repository-কে লুকিয়ে bypass করতে পারবে না:
public class Student
{
public string AdmissionNo { get; }
public string Name { get; private set; }
public decimal FeesPaid { get; private set; }
// Private: the directory is the ONLY gate.
private Student(string admissionNo, string name)
{
AdmissionNo = admissionNo;
Name = name;
}
public void PayFees(decimal amount) => FeesPaid += amount;
public class Directory
{
private readonly Dictionary<string, Student> _byAdmissionNo = new();
public Student GetOrRegister(string admissionNo, string name)
{
if (!_byAdmissionNo.TryGetValue(admissionNo, out var student))
{
student = new Student(admissionNo, name); // allowed: nested class
_byAdmissionNo[admissionNo] = student;
}
return student;
}
public int Count => _byAdmissionNo.Count;
}
}
var directory = new Student.Directory();
var a = directory.GetOrRegister("A-1024", "Rahim");
var b = directory.GetOrRegister("A-1024", "Rahim");
a.PayFees(5000m);
Console.WriteLine(b.FeesPaid); // 5000 — same office file
Console.WriteLine(ReferenceEquals(a, b)); // True — THE same thingতিনটা C# নোট:
ReferenceEquals(a, b)হলো C#-এর "THE same object?" জিজ্ঞেস করার উপায় — এটাrecordযা দেয় তার ঠিক বিপরীত। Record content দিয়ে compare করে (value semantics);Student-এর মতো entity ইচ্ছাকৃতভাবে default reference equality রাখে। এখানে private constructor সহclassবেছে নেওয়া, আরMoney-র জন্যrecord— এটাই language keyword-এ লেখা entity/value split।- Nested
Directoryclass হলো একটা পরিপাটি trick: nested class private constructor call করতে পারে, তাই funnel compiler-enforced অন্য কাউকে creation expose না করে। - ORM এটা আগে থেকেই করে। Entity Framework Core-এর change tracker হলো একটা identity map: একটা
DbContext-এর মধ্যে student A-1024 দুইবার query করলে একই tracked instance ফেরত দেয়। যখন তুমি repository-র দিকে refactor করছো, তুমি in-memory model-কে ORM-এর সাথে align করছো।
Instance thread জুড়ে shared হলে, dictionary access lock-এ wrap করো বা ConcurrentDictionary.GetOrAdd ব্যবহার করো — shared mutable state হলো sharing-এর মূল্য, synchronization দিয়ে দিতে হয়।
Python-এ একই funnel, যেখানে directory একমাত্র sanctioned creator:
class Student:
def __init__(self, admission_no: str, name: str) -> None:
self.admission_no = admission_no
self.name = name
self.fees_paid = 0
def pay_fees(self, amount: int) -> None:
self.fees_paid += amount
class StudentDirectory:
def __init__(self) -> None:
self._by_admission_no: dict[str, Student] = {}
def get_or_register(self, admission_no: str, name: str) -> Student:
if admission_no not in self._by_admission_no:
self._by_admission_no[admission_no] = Student(admission_no, name)
return self._by_admission_no[admission_no]
directory = StudentDirectory()
a = directory.get_or_register("A-1024", "Rahim")
b = directory.get_or_register("A-1024", "Rahim")
a.pay_fees(5000)
assert b.fees_paid == 5000 # one shared file
assert a is b # Python's identity check — THE same thingকলেজ কর্নার: Python-এর is operator হলো pure reference equality (একই object id), আর == __eq__ call করে, override না করলে identity-তে default করে। Entity/value split তাই এভাবে map হয়: entity default __eq__ (identity)-এর উপর নির্ভর করে আর field-ভিত্তিক custom __hash__ দেওয়া হয় না। Value object contents দিয়ে __eq__ override করে আর __hash__-কে এর সাথে consistent রাখতে হয়। এগুলো mix up করলে — ধরো, একটা mutable entity-কে তার mutable field দিয়ে hash করা — প্রতিটা set আর dict corrupt হয়ে যাবে যখনই কোনো field বদলাবে, কারণ object এখন ভুল hash bucket-এ থাকবে।
🛠️ IDE সাপোর্ট
কোনো one-click "Change Value to Reference" command নেই, কিন্তু IDE-গুলো মূল sub-step-গুলো automate করে:
| Tool | সহায়ক পদক্ষেপ |
|---|---|
| Visual Studio / Rider (C#) | Constructor-এ Find Usages প্রতিটা new Customer(...)-এর list করে; Change Signature repository parameter thread করতে সাহায্য করে; Rider-এর Replace constructor with factory method refactoring তোমার জন্য funnel বানিয়ে দেয়। |
| IntelliJ IDEA (Java) | Refactor → Replace Constructor with Factory Method built-in আছে — ঠিক আমাদের recipe-র ধাপ ৩ — তারপর call site migrate করতে Find Usages। |
| VS Code (TypeScript) | Constructor private mark করো; compiler তখন প্রতিটা new call site error হিসেবে list করে — ধাপ ৪-এর জন্য একটা বিনামূল্যের, সম্পূর্ণ to-do list। |
| সবগুলো | Migration-এর পরে, পুরো solution-এ new Customer / new Student খোঁজো — নিশ্চিত করো পিছনের দরজা বন্ধ। |
⚖️ সুবিধা ও ঝুঁকি — আর seesaw
আসলে, Change Value to Reference আর Change Reference to Value হলো perfect inverse। একটা refactoring অন্যটাকে undo করে। তারা একটা seesaw-এর দুই প্রান্তে, আর যে প্রশ্নটা seesaw কাত করে সেটা সবসময় একই: "এটা কি THE same জিনিস, নাকি শুধু AN equal জিনিস?"
| Value (কপি) | Reference (shared) | |
|---|---|---|
| বাস্তব-জগতের অর্থ | "একটা equal জিনিস" — যেকোনো ১০ টাকা যেকোনো ১০ টাকার সমান | "THE same জিনিস" — একজনই রহিম আছে |
| Equality | Content দিয়ে | Identity দিয়ে (একই object) |
| Mutability | Immutable — replace করো, কখনো modify না | Mutable — in place update করো, সবাই দেখবে |
| সবচেয়ে ভালো | টাকা, তারিখ, phone number, ঠিকানা | Customer, student, account, order |
| Machinery | কিছু না — স্বাধীনভাবে বানাও | Registry/repository + lifecycle care |
এই article-এর refactoring জিনিসগুলো ডান কলামে ঠেলে দেয়। Inverse article সেগুলো বামে ফিরিয়ে দেয়। কোনো দিক "ভালো" না — entity ডানে, value বামে, আর bug সেখানেই থাকে যেখানে কোনো concept ভুল দিকে বসে আছে।
এখন এই direction-এর জন্য সৎ হিসাব:
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Update automatically propagate হয় — stale-copy bug অসম্ভব হয়ে যায়। | Shared mutable state: একটা accidental write এখন সব জায়গায় দৃশ্যমান। |
| Model সত্য বলে: একটা real entity = একটা object। | Static registry global-এর মতো কাজ করে — injected repository পছন্দ করো। |
| অনেক holder একটা heavy entity share করলে memory সাশ্রয়। | Lifecycle প্রশ্ন আসে: কখন একটা instance evict হবে? Memory চিরকাল বাড়তে পারে। |
| Creation, lookup, আর lifetime একটা জায়গায় centralized। | একটা shared object-এ race করা thread-গুলোর synchronization দরকার। |
| Entity দিয়ে counting আর grouping অবশেষে সঠিক উত্তর দেয়। | Serialization naive identity ভাঙে — process জুড়ে pointer না, ID identity বহন করে। |
🧹 কোন smell-গুলো সারায়?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Primitive Obsession | Loose customerId string, সর্বত্র entity data rebuild সহ, repository-র পেছনে একটা real shared entity হয়ে যায়। |
| Data Clumps | প্রতিটা order-এ copy-paste করা customer field একটা object-এ single reference-এ collapse হয়। |
| Duplicate Code | "এই field থেকে একটা customer বানাও" logic, প্রতিটা creation site-এ repeated, একটা factory/repository-তে চলে যায়। |
| Data Class | Entity একটা guarded lifecycle আর real behavior পায় raw copyable data-র বদলে। |
| Shotgun Surgery | Entity কীভাবে তৈরি বা cached হয় তা পরিবর্তন করা repository-তে একটাই edit হয়ে যায়। |
📦 দ্রুত revision box
+--------------------------------------------------------------+
| CHANGE VALUE TO REFERENCE — REVISION |
+--------------------------------------------------------------+
| Idea : Many copies of one real entity -> ONE shared |
| instance (office file, not photocopies). |
| Key Q : "Is it THE same thing, or just AN equal thing?" |
| THE same thing -> reference (this refactoring) |
| AN equal thing -> value (see the inverse) |
| Trigger : mutable data + identity + stale-copy bugs |
| Steps : 1. Pick the identity key (ID) |
| 2. Choose lookup owner (prefer injected repo) |
| 3. Add getOrCreate beside the constructor |
| 4. Migrate new X(...) call sites one by one |
| 5. Make the constructor private |
| 6. Audit mutations + add thread safety |
| Watch out: hidden global registries, eviction/lifetime, |
| code that secretly relied on private copies. |
| Inverse : Change Reference to Value (the seesaw's far end) |
+--------------------------------------------------------------+✍️ Practice exercise
ধরো একটা food-delivery app restaurant-গুলো এভাবে model করছে:
class Restaurant {
constructor(
public readonly fssaiLicense: string, // unique license number
public name: string,
public isOpen: boolean = true,
public rating: number = 0,
) {}
}
class DeliveryOrder {
public restaurant: Restaurant;
constructor(license: string, name: string) {
this.restaurant = new Restaurant(license, name); // photocopy!
}
}Bug report: একটা restaurant রাতের জন্য বন্ধ হয়, app isOpen = false mark করে — কিন্তু শুধু একটা order-এর কপিতে। অন্য screen-গুলো এখনো এটাকে খোলা দেখাচ্ছে, আর নতুন order একটা বন্ধ kitchen-এ আসতে থাকে। Owner একটা অন্ধকার kitchen-এ দাঁড়িয়ে order notification ping হতে দেখছেন।
তোমার কাজ:
১. একটা RestaurantRegistry বানাও getOrRegister(license, name) সহ যেটা প্রতিটা license-এর জন্য একটাই instance ফেরত দেয়। এটাকে injected dependency করো, static না।
২. DeliveryOrder-কে registry ব্যবহার করতে migrate করো, তারপর Restaurant-এর constructor বাইর থেকে inaccessible করো।
৩. একটা test লেখো: একই license-এর জন্য দুটো order বানাও, একটার মাধ্যমে isOpen = false set করো, আর assert করো অন্যটা সেটা দেখছে (আর উভয়ই === একই object ধরছে)।
৪. Audit প্রশ্ন: app-এ একটা "simulate surge pricing" feature আছে যেটা what-if calculation-এ সাময়িকভাবে একটা restaurant-এর rating বাড়িয়ে দেয়। তোমার refactoring-এর পরে কী ভুল হবে, আর সেই একটা জায়গা কীভাবে ঠিক করবে?
৫. C#-এ bonus: private constructor আর nested Registry class দিয়ে funnel enforce করো, আর GetOrRegister-কে ConcurrentDictionary.GetOrAdd দিয়ে thread-safe করো।
৬. Seesaw প্রশ্ন: একই app-এ প্রতিটা order-এর সাথে একটা DeliveryFee amount আছে। সেটাও কি registry-র পিছনে যাওয়া উচিত? মূল প্রশ্ন দিয়ে উত্তর দাও — আর যদি উত্তর হয় "এটা AN equal জিনিস", তুমি ইতিমধ্যে জানো পরের কোন article পড়তে হবে।
যদি ধাপ ৩-এর test === check-এর জন্য true print করে, ফটোকপিগুলো চলে গেছে। এখন একটাই অফিস ফাইল আছে, আর জামাল সাহেব এটা নিখুঁতভাবে রাখছেন।
সচরাচর জিজ্ঞাসা
- value object আর reference object-এর পার্থক্য কী?
- নিজেকে জিজ্ঞেস করো: 'এটা কি THE same জিনিস, নাকি AN equal জিনিস?' Reference object-এর identity আছে — customer C-1 দুনিয়ায় একজনই, তাই program-এ তার জন্য একটাই object থাকবে, সবাই সেটাকেই point করবে। Value object-এর কোনো identity নেই — যেকোনো ১০ টাকা অন্য যেকোনো ১০ টাকার সমান, তাই কপি করা সমস্যা না। Reference share করে; value copy করে।
- কপিগুলো কখন আসলে bug হয়ে যায়?
- যখনই data CHANGE হতে পারে। একটা customer-এর নামের দশটা read-only কপি শুধুই অপচয়। কিন্তু customer-এর credit limit যদি বদলায়, তাহলে দশটা কপি মানে প্রতিটা update-এর পর নয়টা কপি পুরনো হয়ে যাবে। Mutable shared state-ই হলো trigger — তখনই তোমার একটা shared instance দরকার।
- registry কি আসলে ছদ্মবেশী global variable?
- static registry এভাবে behave করতে পারে, হ্যাঁ — এটাই মূল risk। Test-গুলো একে অপরের সাথে conflict করে, আর instance চিরকাল বেঁচে থাকে। ভালো version হলো একটা repository যেটা তোমার application-এর মালিকানায় আর dependency হিসেবে inject করা — কোনো static dictionary না। একই idea, নিয়ন্ত্রিত lifetime।
- এটা database আর ORM-এর সাথে কীভাবে সম্পর্কিত?
- সরাসরি সম্পর্কিত। Entity Framework-এর মতো ORM 'identity map' implement করে — একটা DbContext-এর মধ্যে customer C-1 দুইবার load করলে একই tracked object ফেরত দেয়। ORM তোমার জন্য Change Value to Reference করছে। সমস্যা তখন আসে যখন তুমি সেটা bypass করে হাতে হাতে duplicate entity তৈরি করো।
- একাধিক server-এ এটা দরকার হলে কী করব?
- In-memory sharing শুধু একটা process-এর মধ্যেই কাজ করে। একাধিক server-এ 'একটা shared instance' মানে 'database-এ একটা row' বা 'cache-এ একটা record' — identity object pointer-এ না, ID দিয়ে preserve হয়। প্রতিটা server locally একটা identity map থেকে benefit পেতে পারে, কিন্তু database-ই হলো আসল একটা ফাইল।
আরো দেখো
সম্পর্কিত পাঠ
Change Reference to Value: যেকোনো ১০ টাকার নোটই সমান
Change Reference to Value সহজভাবে বোঝানো হয়েছে — একটা shared, mutable reference object-কে কীভাবে content-based equality সহ একটা ছোট immutable value object-এ রূপান্তর করতে হয়, TypeScript আর C# record-এর উদাহরণসহ।
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 আর পরিষ্কার করা যায়।