Preserve Whole Object: পুরো ID Card দেখাও
Preserve Whole Object refactoring শেখো একটা school ID card-এর গল্প দিয়ে — TypeScript আর C# example সহ, safe step-by-step mechanics, আর object pass করলে coupling বাড়ে কিনা সেটার সৎ আলোচনা।
ID Card আর কাগজের চিরকুটের গল্প
ধরো, রুবেল class six-এ পড়ে। স্কুলের library-তে বই নিতে গেলে librarian মামা একটু কড়া — প্রমাণ না দেখলে বই দেবেন না।
প্রথমবার গেলে দরজার সিনিয়র ভাই "procedure" বলে দিল। একটা কাগজ নাও। ID card দেখে roll number লেখো। নাম লেখো। class আর section লেখো। card-এর expiry date লেখো। তারপর সেই কাগজ librarian মামাকে দাও।
রুবেল করল। R-2031 লিখল, তারপর রুবেল হোসেন, তারপর 6-B, তারপর MAR-2027 — পেছনে লম্বা queue তৈরি হচ্ছে। Librarian মামা কাগজ পড়লেন, মিলিয়ে দেখলেন, বই দিলেন। এই পুরো সময়ে রুবেলের গলায় ঝুলছিল laminated ID card — যেখানে এই সব তথ্য স্কুল অফিস নিজে ছেপে দিয়েছে — সেটা কেউ ছুঁলোই না।
পরের সপ্তাহে নতুন নিয়ম এলো: blood group-ও দেখতে হবে (trekking club-এর বইয়ের জন্য, কারণটা জিজ্ঞেস করো না)। এখন সবার কাগজ ভুল। সবাইকে আরেকটা লাইন যোগ করতে হবে। সিনিয়ররা বিরক্ত হয়ে notice আপডেট করল। Queue আরও লম্বা হলো। আর জামাল নামের ছেলেটা roll number ভুল লিখল — কাগজে R-2013, card-এ R-2031 — ভুল ছাত্রের নামে বই ইস্যু হয়ে গেল। দুই মাস পর আসল রুবেল fine notice পেল। মহা ঝামেলা।
তারপর একদিন librarian মামা চশমার উপর দিয়ে পুরো queue দেখে বললেন: "বাবারা। ID card দেখলেই তো হয়। কাগজে copy করছো কেন?"
একদম ঠিক কথা! ID card-এ roll number, name, class, expiry, blood group — সব আছে। ছাপা, laminated, copy করার ভুল হওয়ার সুযোগ নেই। নতুন নিয়ম আসলে ছাত্রদের কিছু করতে হয় না — তারা সবসময় পুরো card-ই দিচ্ছে, শুধু librarian মামা একটা extra line পড়েন। কাগজের চিরকুট ছিল বাড়তি কাজ, ভুল করার সুযোগ, আর নিয়ম বদলালেই ভেঙে পড়ত।
Counter-এ কথাবার্তা আক্ষরিক অর্থেই পাঁচটা exchange থেকে একটায় নেমে আসে:
Code-এও একই ঘটনা। কোনো caller একটা student object থেকে student.rollNo, student.name, student.className টেনে বের করে একটা method-এ আলাদা আলাদা parameter হিসেবে পাঠাচ্ছে — চিরকুট বানাচ্ছে। পরে method-এর আরেকটা field দরকার হলে signature বদলাতে হয়, আর প্রতিটা caller-কে edit করতে হয়। এই সমস্যা দূর করার refactoring-এর নাম Preserve Whole Object: object ভাঙা বন্ধ করো, চিরকুট না দিয়ে পুরো card দাও।
Preserve Whole Object আসলে কী?
Preserve Whole Object হলো সেই refactoring যেখানে তুমি একটা object থেকে বের করা কয়েকটা parameter-এর জায়গায় সেই পুরো object-টাকেই একটা parameter হিসেবে পাঠাও। Method নিজে body-র ভেতরে যা দরকার নেয়।
চেনার উপায়: call করার ঠিক আগে caller একটু unpacking করে —
const low = range.low;
const high = range.high;
if (room.withinRange(low, high)) { ... }— আর called method ঠিক সেই টুকরোগুলো consume করে। Object-টা সামনেই দাঁড়িয়ে ছিল, পুরোপুরি তৈরি, আর আমরা দরজায় দাঁড়িয়েই সেটাকে ছিঁড়ে ফেললাম।
এই refactoring Martin Fowler-এর Refactoring বইয়ের দুটো edition-এই আছে। Fowler-এর classic example হলো room temperature check: day-এর low আর high আলাদা করে না পাঠিয়ে পুরো temperature range object-টাই পাঠাও।
একটু ভাবো, রুবেলের card-এ হয়তো আট রকম তথ্য ছিল। চিরকুটে সেগুলোর অর্ধেক হাতে লিখে নেওয়া হয়েছিল — লাইনে দাঁড়িয়ে, কলম দিয়ে:
পুরো card দেখালে কী লাভ?
- ছোট signature। তিন, চার, পাঁচটা parameter মিলে একটা হয়। এটা সরাসরি Long Parameter List সমস্যায় আঘাত করে।
- ভবিষ্যৎ proof। পরে method-এর নতুন field দরকার হলে — blood group-এর ঘটনার মতো — শুধু method-এর body বদলাবে। কোনো caller-কে ছুঁতে হবে না। চিরকুট পদ্ধতিতে প্রতিটা caller বদলাতে হতো।
- ভুল করার উপায় নেই। Caller ভুল order-এ field পাঠাতে পারবে না (
withinRange(high, low)— এটা একটা classic silent bug) কারণ আর কোনো loose field নেই। - Boilerplate সাফ। প্রতিটা call site-এ unpacking ritual আর লাগবে না।
- Design X-ray। পুরো object আসার পর প্রায়ই দেখা যায় method শুধু সেই object-এর data ব্যবহার করছে — এটাই Feature Envy smell। এরপর স্বাভাবিক পরের পদক্ষেপ হলো Move Method:
withinRangeহয়তো range-এর উপরেই থাকা উচিত।
একটা কথা মনে রাখো: কয়েকটা argument যদি একই object থেকে আসে, তাহলে সেই object-ই হলো আসল argument। Card দেখাও, reader পড়ুক। পুরো object নিলে method কম নিতে পারে — কিন্তু শুধু ছেঁড়া field নিলে পরে বেশি নেওয়ার কোনো উপায় নেই, প্রতিটা caller ভেঙে পড়বে।
একটা সৎ সতর্কতা: পুরো object pass করলে method সেই object-এর type-এর উপর dependent হয়ে পড়ে। যে method দুটো number নিত, সে যেকোনো দুটো number দিয়ে কাজ করত। এখন TempRange নিলে শুধু TempRange দিয়েই কাজ হবে। একই module-এর ভেতরে এই trade-off প্রায় সবসময় worth it। Module বা service boundary পেরিয়ে গেলে নাও হতে পারে। Benefits and risks section-এ এটা ভালো করে দেখব — এই refactoring-এর সবচেয়ে গুরুত্বপূর্ণ fine print এটাই।
College corner: classic software engineering বইগুলো (Yourdon আর Constantine-এর structured design, পরে Meilir Page-Jones) এই trade-off-এর নাম দিয়েছে। আলাদা primitive value পাঠানো হলো data coupling — সবচেয়ে loose, সবচেয়ে healthy। পুরো structure পাঠানো যেখানে callee শুধু কিছু অংশ ব্যবহার করে — সেটা stamp coupling — একটু বেশি tight। Preserve Whole Object ইচ্ছে করেই data coupling থেকে stamp coupling-এর দিকে নিয়ে যায়, আর এটা justify হয় যখন change-resilience-এর লাভ dependency-র খরচের চেয়ে বেশি হয়। Module boundary-তে এই হিসাব উল্টে যায়: public API যদি তোমার rich internal Student type-এর সাথে stamp-coupled হয়, তাহলে তোমার পুরো domain model contract হয়ে যায়। এজন্য narrow interface-এর middle path এত গুরুত্বপূর্ণ।
কখন এটা দরকার?
এই লক্ষণগুলো দেখলে বুঝবে:
- Unpacking ritual। Call করার আগে দুই বা তার বেশি লাইন শুধু একটা object থেকে field বের করছে, আর সেগুলো শুধু argument হিসেবে ব্যবহার হচ্ছে। চিরকুট লেখা হচ্ছে।
- Long Parameter List।
register(name, rollNo, className, section, bloodGroup)— প্রতিটা argument একই origin object থেকে। Fowler সাহেবের পরামর্শ: value গুলো এক object থেকে আসলে, সেই object-টাই পাঠাও। - Data Clumps। একই field-এর দল —
low, high;street, city, pincode;name, rollNo, className— অনেক জায়গায় একসাথে ঘুরছে। এগুলো আগে থেকেই কোনো object-এ থাকলে Preserve Whole Object সব জায়গায় clump-টা সরিয়ে দেবে। না থাকলে আগে Introduce Parameter Object করে object বানাও। - Signature churn। এই বছর method-এর parameter list দুইবার বেড়েছে, আর প্রতিবার প্রতিটা caller বদলাতে হয়েছে। সেই বদলানোটাই library counter-এ চিরকুট পুনরায় লেখার queue।
- Wrong-order bug। আগে কোনো bug হয়েছে যেখানে একই type-এর দুটো argument swap হয়ে গিয়েছিল (
withinRange(high, low)) — এটা প্রমাণ যে loose field বিপজ্জনক।
আর কখন না করবে:
- Value গুলো একটা object থেকে আসছে না। Caller যদি
lowএক জায়গা থেকে আরhighঅন্য জায়গা থেকে নেয়, তাহলে দেখানোর মতো কোনো card নেই। জোর করে object বানিও না। - Method-এর সেই type জানা উচিত না।
clamp(value, min, max)-এর মতো general utility সব দিক থেকে caller পায় — এটাকেTempRange-এর সাথে চেইন করলে কাজের জিনিস নষ্ট হবে। - Boundary পেরিয়ে যেখানে thin রাখতে চাও। কোনো payment module-এর function যদি শুধু একটা email field পড়ার জন্য পুরো
Studentobject নেয়, তাহলে module সারাজীবন Student-এর shape-এর উপর dependent থাকবে। Primitive বা ছোট dedicated type এখানে better contract। - শুধু একটা trivial field ব্যবহার হচ্ছে। একটা string পড়ার জন্য পুরো object pass করলে লাভ কম, coupling বেশি। বিবেচনা করো, dogma না।
Choice হিসেবে একটা flow দেখো — "না" উত্তর গুলো dead end না, sibling refactoring-এর দিকে নিয়ে যায়:
Before আর After এক নজরে
TypeScript-এ library counter। Before code চিরকুট লিখছে:
interface Student {
rollNo: string;
name: string;
className: string;
cardExpiry: Date;
bloodGroup: string;
}
class Library {
// BEFORE: the method receives a slip of copied fields
canBorrow(rollNo: string, className: string, cardExpiry: Date): boolean {
if (cardExpiry < this.today()) return false;
if (this.finesOwedBy(rollNo) > 0) return false;
return this.allowedClasses.includes(className);
}
}
// every caller performs the copying ritual:
const rollNo = student.rollNo;
const className = student.className;
const cardExpiry = student.cardExpiry;
if (library.canBorrow(rollNo, className, cardExpiry)) {
issueBook(student, book);
}এখন trekking club-এর নিয়ম আসল: canBorrow-কে bloodGroup-ও check করতে হবে। চিরকুট পদ্ধতিতে signature-এ চতুর্থ parameter আসে, আর codebase-এর প্রতিটা caller-কে আরেকটা field copy করতে হয়। বরং — card দেখাও:
class Library {
// AFTER: the method receives the whole card and reads what it needs
canBorrow(student: Student): boolean {
if (student.cardExpiry < this.today()) return false;
if (this.finesOwedBy(student.rollNo) > 0) return false;
if (!this.allowedClasses.includes(student.className)) return false;
return student.bloodGroup !== ""; // new rule: body-only change!
}
}
// callers shrink to one honest line:
if (library.canBorrow(student)) {
issueBook(student, book);
}নতুন নিয়মে একটা method body বদলেছে। কোনো caller টের পায়নি। কোনো চিরকুট পুনরায় লেখা হয়নি। আর wrong-order bug (canBorrow(className, rollNo, ...) — দুটোই string, compiler চুপ) এখন আর লেখাই সম্ভব না।
Class diagram-এ দেখো — তিনটা loose string আর একটা date-এর জায়গায় সেই type-টাই এলো যে সবসময় এগুলো ধরে রেখেছিল:
Safe পদ্ধতিতে ধাপে ধাপে
কিছু না ভেঙে এটা করার trick হলো একটা ছোট সময়ের জন্য method দুটোই — পুরো object আর পুরনো loose parameter — একসাথে accept করবে। Codebase এই states-এর মধ্যে দিয়ে যাবে:
এইভাবে ওঠো:
- Single origin confirm করো। প্রতিটা caller check করো: loose argument গুলো কি সত্যিই একটা object থেকে আসছে যেটা caller-এর কাছে আছে? কোনো caller যদি বাইরে থেকে value compute করে, সেটা mark করো — adapter লাগতে পারে বা থামতে হতে পারে।
- Whole-object parameter পুরনো গুলোর পাশে যোগ করো। এখনো কেউ ব্যবহার করছে না, সব compile হচ্ছে।
// Intermediate state 1: both the card AND the slip are accepted
canBorrow(student: Student, rollNo: string, className: string, cardExpiry: Date): boolean {
if (cardExpiry < this.today()) return false;
if (this.finesOwedBy(rollNo) > 0) return false;
return this.allowedClasses.includes(className);
}
// callers updated mechanically: library.canBorrow(student, rollNo, className, cardExpiry)- Body-র ভেতরে একটা একটা করে loose parameter-কে object-এর read দিয়ে replace করো। প্রতিটা replacement-এর পর test run করো — এক লাইনের কাজ, পুরোপুরি reversible।
// Intermediate state 2: body now reads from the object; old params are dead weight
canBorrow(student: Student, rollNo: string, className: string, cardExpiry: Date): boolean {
if (student.cardExpiry < this.today()) return false;
if (this.finesOwedBy(student.rollNo) > 0) return false;
return this.allowedClasses.includes(student.className);
}- Unused parameter গুলো signature থেকে সরাও, আর প্রতিটা caller-এ argument আর extraction line মুছে দাও। Compiler (বা unused variable warning) প্রতিটা জায়গায় নিজে নিয়ে যাবে।
- Full suite run করো। Behaviour হুবহু একই থাকতে হবে — একই field পড়া হচ্ছে, শুধু একটু পরে।
- Follow-up খোঁজো।
canBorrowকি এখন বেশিরভাগstudent-এর data ব্যবহার করছে,Library-র নিজের খুব কম? সেটাই Feature Envy দেখাচ্ছে — Move Method বিবেচনা করো। আর দেখো method আসলে পুরো student লাগে, নাকি শুধু কিছু field —IdCardinterface করলে coupling কম রাখা যায়।
দুটো ফাঁদ। প্রথমত, পুরনো code-এ order bug লুকিয়ে থাকতে পারে: migrate করার সময় দেখতে পারো কোনো caller আগে থেকেই high, low swap করে পাঠাচ্ছিল — congratulations, real bug পেয়েছ, কিন্তু এটাকে আলাদা clearly-labelled change হিসেবে fix করো, refactoring-এর ভেতরে চুপ করে না। দ্বিতীয়ত, method-কে object mutate করতে দিও না। Loose copy থাকলে method original damage করতে পারত না; পুরো object হাতে পেলে কোনো অসতর্ক student.cardExpiry = ... সম্ভব হয়ে যায়। Read-only type পছন্দ করো (TypeScript-এর Readonly<Student>, C#-এর record বা IReadOnly... interface) — card-টাকে laminated রাখো।
একটা বড় বাস্তব example
ধরো তারিকের বাবা online-এ মশলা বিক্রি করেন। Shop-এর code-কে answer করতে হয়: এই address-এ কি delivery দেওয়া সম্ভব? Before version প্রতিটা doorstep-এ address ছিঁড়ে ফেলছে:
interface Address {
street: string;
city: string;
state: string;
pincode: string;
isRemoteArea: boolean;
}
class DeliveryService {
// BEFORE: four shreds of one address
isServiceable(pincode: string, state: string, isRemoteArea: boolean, weightKg: number): boolean {
if (!this.servedStates.includes(state)) return false;
if (this.blockedPincodes.has(pincode)) return false;
if (isRemoteArea && weightKg > 10) return false;
return true;
}
estimateDays(pincode: string, isRemoteArea: boolean): number {
const zone = this.zoneOf(pincode);
return isRemoteArea ? zone.baseDays + 3 : zone.baseDays;
}
}
// caller — the shredding ritual, twice:
const pincode = order.address.pincode;
const state = order.address.state;
const remote = order.address.isRemoteArea;
if (service.isServiceable(pincode, state, remote, order.weightKg)) {
const days = service.estimateDays(pincode, remote);
showPromise(days);
}Smell-এর stack দেখো: একই দল (pincode, state, isRemoteArea) অনেক signature-এ ঘুরছে — textbook Data Clumps — আর প্রতিটা signature Long Parameter List-এর দিকে যাচ্ছে। Preserve Whole Object করার পর:
class DeliveryService {
// AFTER: the address travels whole; weight stays separate — it is not part of the address
isServiceable(address: Address, weightKg: number): boolean {
if (!this.servedStates.includes(address.state)) return false;
if (this.blockedPincodes.has(address.pincode)) return false;
if (address.isRemoteArea && weightKg > 10) return false;
return true;
}
estimateDays(address: Address): number {
const zone = this.zoneOf(address.pincode);
return address.isRemoteArea ? zone.baseDays + 3 : zone.baseDays;
}
}
// caller:
if (service.isServiceable(order.address, order.weightKg)) {
showPromise(service.estimateDays(order.address));
}দুটো জিনিস খেয়াল করো। প্রথমত, weightKg আলাদা parameter-ই থাকল — এটা parcel-এর জিনিস, address-এর না। Preserve Whole Object মানে "সব কিছু একটা bag-এ ঢুকিয়ে দাও" না — মানে হলো "যে bag আগে থেকেই আছে সেটাকে ছেঁড়া বন্ধ করো"। দ্বিতীয়ত, courier partner পরে flood-affected district-এর নিয়ম যোগ করলে (Address-এ নতুন district field) শুধু দুটো method body বদলাবে। আগের design-এ প্রতিটা caller-এ পঞ্চম parameter যেত।
Shop-এর codebase-এ isServiceable-এর জন্য এগারোটা call site ছিল। নতুন নিয়মে কতটা edit লাগে দেখো:
বারোটা edit (এগারো caller + signature) বনাম দুটো method body। এই ফারাক codebase যত সফল হয় তত বাড়ে — এজন্যই চিরকুট-style signature-গুলো codebase বড় হলে আরও বেশি কষ্ট দেয়।
C#-এ একই refactoring
একই কাজ, hostel management system-এ — room-এর temperature plan দিনের আবহাওয়ার সাথে মেলে কিনা দেখছে:
// BEFORE
public class Room
{
public int Temperature { get; }
public bool WithinPlan(int low, int high)
=> Temperature >= low && Temperature <= high;
}
// caller dismantles the plan object first:
var low = heatingPlan.Low;
var high = heatingPlan.High;
if (!room.WithinPlan(low, high))
alerts.Raise($"Room {room.Id} outside heating plan");পুরো plan pass করার পর:
// AFTER
public record TempRange(int Low, int High)
{
public bool Includes(int value) => value >= Low && value <= High;
}
public class Room
{
public int Temperature { get; }
public bool WithinPlan(TempRange range) => range.Includes(Temperature);
}
// caller:
if (!room.WithinPlan(heatingPlan.Range))
alerts.Raise($"Room {room.Id} outside heating plan");Parameter count ছাড়াও আরেকটা জিনিস হলো: TempRange পুরোটা আসার পর comparison logic নিজেই range-এর উপরে Includes হিসেবে চলে গেল — data আর behaviour একসাথে হলো। এটাই Feature Envy follow-up স্বাভাবিকভাবে ঘটছে। C# record ব্যবহারে object immutable থাকে, তাই WithinPlan পড়তে পারে কিন্তু plan damage করতে পারে না — laminated card-এর গ্যারান্টি, language-ই enforce করছে।
College corner: narrow interface-এর middle path হলো Interface Segregation Principle parameter-এ apply করা। পুরো Athlete type-এর সাথে coupling-এর বদলে তুমি শুধু method-টা যা পড়ে তার সবচেয়ে ছোট shape declare করো — TypeScript-এ structural type হিসেবে শুধু name, house, আর chestNo সহ একটা object; C#-এ ছোট interface যা rich type implement করে। এতে method তার সত্যিকারের dependency signature-এ বলে দেয় (পড়তে ভালো), shape মেটানো যেকোনো কিছু accept করে (reuse ভালো — teacher, guest, mascot), আর rich type-এর unrelated change থেকে নিরাপদ (stability ভালো)। TypeScript-এর structural typing-এ এটা প্রায় বিনামূল্যে; nominal language-এ একটু ceremony লাগে। সাধারণ নিয়ম: module-এর ভেতরে rich object পাঠাও; boundary-তে সবচেয়ে ছোট সৎ shape পাঠাও।
IDE support
কোনো বড় IDE-তে এখন "Preserve Whole Object" নামে একটাই action নেই — নতুন parameter থেকে field পড়ার ধাপটায় মানুষের চোখ লাগে। কিন্তু mechanics ভালোভাবেই support করে:
- IntelliJ IDEA / Rider / WebStorm — Change Signature (Ctrl+F6) whole-object parameter যোগ করে আর প্রতিটা call site-এ default argument দেয় (যেমন
student); পরে একইভাবে dead loose parameter সরায়। Find Usages প্রতিটা caller-এর extraction ritual খুঁজে দেয়, আর inspections unused parameter flag করে। - ReSharper / Visual Studio (C#) — Change Signature আর "parameter is never used" inspection একই কাজ করে; ReSharper-এর Transform Parameters refactoring loose parameter-কে নতুন class-এ bundle করতে পারে — এটা sibling move (Introduce Parameter Object) যখন whole object এখনো নেই।
- IntelliJ-এর Extract Parameter Object — sibling কাজ automate করে; কার্যকর যখন "card" আগে বানাতে হবে।
- TypeScript in VS Code — signature-wide automation নেই, কিন্তু compiler-ই migration engine: নতুন parameter যোগ করো, পুরনো সরাও, red squiggles follow করে caller by caller যাও।
সব জায়গায় practical recipe: Change Signature দিয়ে object যোগ করো, body reads manually swap করো (swap-এর পর test), Change Signature দিয়ে dead parameter সরাও, unused-variable warning দিয়ে call site পরিষ্কার করো।
লাভ আর ঝুঁকি
| দিক | লাভ | ঝুঁকি / খরচ |
|---|---|---|
| Signature length | ৩-৫ extracted param মিলে ১ হয়; সরাসরি Long Parameter List ঠিক করে | — |
| ভবিষ্যৎ change | নতুন field লাগলে শুধু body বদলায়; কোনো caller edit নেই | — |
| Correctness | Wrong-order argument bug (দুটো string swap) আর লেখা সম্ভব না | পুরনো caller-এ লুকানো swap bug migrate করার সময় বেরিয়ে আসতে পারে — আলাদা clearly-labelled change হিসেবে fix করো |
| Call-site noise | Unpacking ritual সব জায়গা থেকে মুছে যায় | — |
| Coupling | — | সৎ খরচ: method এখন object-এর type-এর উপর dependent। দুটো int নেওয়া function যেকোনো দুটো int দিয়ে কাজ করত; এখন শুধু TempRange দিয়ে কাজ হবে। Reuse সংকুচিত হয়, test-এ পুরো object বানাতে হয় |
| Encapsulation | — | Method এখন দুটো field-এর চেয়ে অনেক বেশি দেখতে পায় (আর mutable type হলে modify করতেও পারে) — misuse-এর বড় surface; read-only type বা narrow interface পছন্দ করো |
| Boundary | Module-এর ভেতরে: প্রায় সবসময় স্পষ্ট জয় | Module/service boundary পেরিয়ে, rich domain type ভারী contract হয়; primitive বা ছোট DTO সেখানে looser আর stable choice |
| Design insight | Feature Envy expose হয় — method-এর ভালো জায়গা খুঁজে পাওয়া যায় (Move Method) | — |
একটা সহজ মনে রাখার উপায়: Preserve Whole Object caller convenience আর change-resilience-এর জন্য type coupling নেয়। একই codebase region-এ যেখানে type-টা পরিচিত, এই trade excellent। Published API edge-এ দুইবার ভাবো — আর middle path বিবেচনা করো: narrow interface accept করো (শুধু { low, high } বা তিনটা field সহ IdCard) যাতে method ছোট stable shape-এর সাথে coupled হয়, পুরো rich object-এর সাথে না।
তিনটা shape পাশাপাশি তুলনা:
| Signature style | Example | Coupling | নতুন field-এর খরচ | সবচেয়ে ভালো কোথায় |
|---|---|---|---|---|
| Loose primitive (চিরকুট) | canBorrow(rollNo, className, expiry) | সবচেয়ে loose — যেকোনো value থেকে | প্রতিটা caller edit | ছোট utility, দূরের boundary |
| Whole object (card) | canBorrow(student) | সবচেয়ে tight — পুরো rich type | শুধু body বদলায় | একটা module-এর ভেতরে যেখানে type owned |
| Narrow interface (card cover-এর window) | canBorrow(card: IdCard) | ছোট stable slice | Body বদলায়, যদি field slice-এ থাকে | Module edge, shared helper |
যেকোনো candidate call-কে দুই-axis map-এ রাখো। Horizontal axis: method কতটা field দরকার; vertical axis: call কতটা module boundary পেরোচ্ছে:
Corner গুলো দেখো: canBorrow-এর অনেক field দরকার আর Student-এর পাশেই থাকে — card দাও। Payment module-এর দূরের boundary পেরিয়ে এক email দরকার — primitive পাঠাও। Transport module-এর announcement মাঝামাঝি — narrow তিন-field interface সবচেয়ে graceful জবাব।
কোন smell গুলো ঠিক হয়?
| Smell | Preserve Whole Object কীভাবে সাহায্য করে |
|---|---|
| Long Parameter List | Primary target — একই origin-এর কয়েকটা parameter মিলে একটা হয় |
| Data Clumps | ঘুরে বেড়ানো field-এর দল সব signature-এ তার existing home object দিয়ে replace হয় |
| Feature Envy | Cured না, বরং revealed — object পুরোটা আসার পর যে method শুধু সেটার data পড়ে সে expose হয়, Move Method-কে invite করে |
| Primitive Obsession | Call site-এ loose primitive-এর জায়গায় meaningful domain type আসে |
| Shotgun Surgery | কোনো check-এ field যোগ করলে আর প্রতিটা caller-এ ripple করে না |
পুরো ধারণাটা একটা mindmap-এ
Quick revision
+=============== PRESERVE WHOLE OBJECT ================+
| |
| SMELL : caller copies fields off one object |
| (the slip) and passes them one by one |
| canBorrow(rollNo, className, expiry) |
| |
| MOVE : pass the object itself (the ID card) |
| canBorrow(student) |
| |
| LADDER: 1 confirm single origin 2 add object |
| param BESIDE old ones 3 swap body reads |
| one at a time 4 drop dead params |
| 5 delete extraction lines at callers |
| |
| WIN : new field needed -> body-only change |
| COST : method is now COUPLED to the object type; |
| across boundaries prefer a narrow shape |
| |
| NEXT : body full of student.x? Feature Envy -> |
| consider Move Method |
+======================================================+Practice exercise
একটা sports day registration system প্রতিটা counter-এ athlete-কে ছিঁড়ে ফেলছে — refactor করো:
interface Athlete {
chestNo: string;
name: string;
ageGroup: "U12" | "U14" | "U16";
house: string;
hasMedicalClearance: boolean;
}
class EventDesk {
canRegister(ageGroup: string, hasMedicalClearance: boolean, chestNo: string): boolean {
if (!this.eventAgeGroups.includes(ageGroup)) return false;
if (!hasMedicalClearance) return false;
return !this.alreadyRegistered.has(chestNo);
}
announceEntry(name: string, house: string, chestNo: string): string {
return `${name} (${chestNo}) of ${house} house, please report to the track.`;
}
}
// caller:
const ageGroup = athlete.ageGroup;
const clearance = athlete.hasMedicalClearance;
const chestNo = athlete.chestNo;
if (desk.canRegister(ageGroup, clearance, chestNo)) {
speaker.say(desk.announceEntry(athlete.name, athlete.house, athlete.chestNo));
}তোমার কাজ:
- দুটো method-এর প্রতিটা parameter confirm করো যে সেটা একটাই
Athleteobject থেকে আসছে। - Both-parameters bridge apply করো:
canRegister-এ পুরনো parameter-এর পাশেathlete: Athleteযোগ করো, body reads একটা একটা করে swap করো, তারপর dead parameter সরাও।announceEntry-এর জন্য repeat করো। - Caller-এ extraction ritual মুছে দাও — দেখবে দুই লাইনে নেমে আসে।
- Sports teacher-এর নতুন নিয়ম: U12 athlete-রা register করতে পারবে শুধু তখনই যদি তাদের
house-এ ১০-এর কম entry থাকে। দেখাও যে refactored version-এ এটা শুধু body-তে change — আর লিখে রাখো before version-এ কতটা file touch করতে হতো। - Coupling প্রশ্ন: school-এর transport module-ও
announceEntry-style string চায়, কিন্তু teacher-দের জন্য — যারাAthleteনা।announceEntryকি পুরোAthleteনেবে? একটা narrow interface design করো (হয়তো{ name, house, chestNo }) আর দুটো বাক্যে বলো সেই boundary-তে thin shape কেন better contract। - Stretch: refactoring করার পর
canRegisterathlete-এর তিনটা field পড়ে আর নিজের মাত্র একটা (alreadyRegistered)। কোন smell দেখা যাচ্ছে, আর কোন follow-up refactoring বিবেচনা করবে? (Hint: Feature Envy paragraph আবার পড়ো।) - Chart করো:
canRegister,announceEntry, আর transport module-এর request-কে চিত্র ১০-এর quadrant chart-এ রাখো। কোনটা whole object পাবে, কোনটা narrow interface, আর কেন?
সচরাচর জিজ্ঞাসা
- Preserve Whole Object মানে এক কথায় কী?
- যখন কোনো caller একটা object থেকে কয়েকটা value তুলে আলাদা argument হিসেবে পাঠায়, তখন পুরো object-টাই পাঠাও — method নিজে যা দরকার নেবে। মানে হলো, library-তে গিয়ে roll number, name, class আলাদা কাগজে লিখে দেওয়ার বদলে সরাসরি ID card দেখাও।
- এটা Introduce Parameter Object থেকে আলাদা কীভাবে?
- Preserve Whole Object তখন ব্যবহার করো যখন value গুলো আগে থেকেই একটা object-এ আছে — তুমি শুধু সেটাকে ভেঙে ফেলা বন্ধ করো। Introduce Parameter Object তখন করো যখন value গুলো ছড়িয়ে-ছিটিয়ে আছে, কোনো object নেই — তখন আগে object বানাতে হয়। দুটোর goal এক (ছোট signature), কিন্তু শুরুর জায়গা আলাদা।
- পুরো object pass করলে কি coupling বাড়ে না?
- হ্যাঁ, সৎ কথা হলো — বাড়ে। যে method আগে দুটো number নিত, সে যেকোনো দুটো number দিয়ে কাজ করত। এখন Student object নিলে শুধু Student দিয়েই কাজ হবে, আর object-এর সব কিছু দেখতে পাবে। একই module-এর ভেতরে এই trade-off সাধারণত worth it। কিন্তু module boundary পেরিয়ে গেলে, primitive বা ছোট dedicated type-ই বেশি stable contract।
- দুটো আলাদা object থেকে value লাগলে কী করব?
- তাহলে pass করার মতো কোনো একটা whole object নেই। জোর করে একটা বানাতে যেও না। দুটো object আলাদা pass করো, অথবা আলাদা value-ই পাঠাও, অথবা একটু ভেবে দেখো method-টা আসলে অন্য কোথাও থাকা উচিত কিনা। Preserve Whole Object শুধু তখনই কাজে লাগে যখন argument গুলো সত্যিকারের একটা cohesive object থেকে আসছে।
- Feature Envy-র সাথে এর সম্পর্ক কী?
- পুরো object pass করার পর method-এর ভেতরে দেখবে সারি সারি call — range.low, range.high, range.includes। এটা দেখলেই বুঝবে method-টা নিজের class-এর চেয়ে অন্য object-এর data নিয়ে বেশি আগ্রহী — এটাই Feature Envy smell। পরের পদক্ষেপ হলো Move Method: logic-টাকে সেই object-এর কাছেই নিয়ে যাও।
আরো দেখো
সম্পর্কিত পাঠ
Replace Parameter with Method Call: দোকানদারকে তার নিজের দাম পড়ে শোনাতে যেও না
Replace Parameter with Method Call refactoring শেখো চায়ের দোকানের একটা মজার গল্পের মাধ্যমে — TypeScript আর C# উদাহরণসহ, নিরাপদ ধাপে ধাপে পদ্ধতি, আর testability-র সৎ হিসাব।
Parameterize Method: একটাই জুসের রেসিপি, শুধু সাইজটা দিয়ে দাও
জুসের দোকানের গল্পের মাধ্যমে Parameterize Method রিফ্যাক্টরিং শেখো — TypeScript আর C# উদাহরণ সহ, নিরাপদ ধাপে ধাপে mechanics, আর সেই সিস্যার নিয়ম যেটা Replace Parameter with Explicit Methods-এর সাথে জুটি বাঁধে।
Replace Parameter with Explicit Methods: গোপন কোড নয়, নামের বোর্ড লাগাও
Replace Parameter with Explicit Methods refactoring শেখো একটা ব্যাংক কাউন্টারের গল্পের মাধ্যমে — TypeScript আর Python উদাহরণ, safe mechanics, আর seesaw rule যেটা Parameterize Method-এর সাথে এর সম্পর্ক বোঝায়।
Long Parameter List: দশটা নির্দেশনার চায়ের অর্ডার
Long Parameter List কোড স্মেল সহজ ভাষায় — কেন বেশি argument-এর method বাগ তৈরি করে, আর কীভাবে parameter object দিয়ে call ছোট, পরিষ্কার আর নিরাপদ করা যায়।