Move Field: ডেটা রাখো যেখানে সে কাজে লাগে
Move Field শেখো একটা মজাদার স্কুলের গল্প দিয়ে। ডেটাকে সেই class-এ সরাও যেটা আসলে ওই ডেটা ব্যবহার করে, যাতে state আর behaviour একসাথে বাস করতে পারে।
ক্রিকেট কিট কীভাবে গণিতের ক্লাসরুমে গেল
ধরো Green Valley School-এর Class 7-এর গণিত ক্লাসরুম। পিছনের কোণে একটু তাকাও। duster আর graph paper chart-এর পাশে... একটা পুরো ক্রিকেট কিট! ব্যাট, প্যাড, স্টাম্প, দুটো হেলমেট, একটা ব্যাগ ভর্তি ball।
গণিত ঘরে ক্রিকেট কিট কেন? কেউ ঠিকমতো মনে করতে পারে না। বছর কয়েক আগে sports room-এ রং হচ্ছিল, sports teacher জনাব করিম কিটটা এখানে রেখেছিলেন "কয়েকদিনের জন্য" — আর সেটা আর ফেরেনি। গণিত শিক্ষিকা মিসেস রহেলা জিজ্ঞেস করাই বাদ দিয়েছেন।
এবার দেখো এই একটা ব্যাপার প্রতিদিন কত ঝামেলা বানায়। প্রতিটা games period-এ জনাব করিম দুই ছেলেকে পাঠান — সাধারণত রুবেল আর তারিক — গণিত ক্লাসরুমে: "যাও কিটটা নিয়ে আসো।" ওরা দরজায় টোকা দেয়, মিসেস রহেলা linear equation-এর পাঠ থামান, পুরো Class 7 ঘাড় ঘুরিয়ে দেখে, ছেলেরা ভারী ব্যাগ টেনে ভবন পার হয়, আর period শেষে আবার ফিরিয়ে আনে। মিসেস রহেলাকে বাইরের লোকের জন্য আলমারি খোলা রাখতে হয়। একটা ball হারালে তাকেই দোষ দেওয়া হয় — যে জিনিস তিনি কখনো ছুঁয়ে দেখেননি তার জন্য! গত term-এ একটা helmet-এর strap ভাঙলে sports office সত্যিই তাকে চিঠি পাঠাল "school property ভালো করে দেখাশোনা করুন" বলে।
এক সপ্তাহের traffic গোনো: পাঁচটা games period, দুটো করে যাতায়াত, প্রতিবারে দুজন ছেলে। বিশটা ছেলে-যাত্রা ভবন পেরিয়ে, চল্লিশটা algebra পাঠে ব্যাঘাত, আর এক শিক্ষিকা এমন আলমারির চাবি বহন করছেন যার ভেতরের জিনিস তিনি কখনো ছুঁয়ে দেখেননি।
এক সোমবারে principal জনাব সালাম করিডোরে হাঁটছিলেন। দেখলেন রুবেল আর তারিক সেই সপ্তাহে তৃতীয়বার ব্যাগ টেনে নিয়ে যাচ্ছে। তিনি সহজ প্রশ্নটা করলেন: "আসলে কে এই কিট ব্যবহার করে?" Sports department — প্রতিদিন। গণিত ক্লাস — কখনো না। কিট চলে গেল sports room-এ, মাঠ থেকে দুই মিনিট দূরে। ছেলেরা দৈনিক অভিযান বন্ধ করল। মিসেস রহেলা শান্তিতে আলমারি বন্ধ করলেন। জনাব করিম নিজের কিট নিজে চেক করেন, হারানো ball-এর হিসাব নিজেই দেন। জিনিস থাকে যেখানে কাজে লাগে।
কোডে, ডেটা হলো ক্রিকেট কিট। একটা field কখনো কখনো এক class-এ থাকে, আর সম্পূর্ণ আলাদা একটা class প্রতিদিন সেটা পড়ে আর লেখে। প্রতিটা ব্যবহার মানে class boundary পেরিয়ে একটা যাত্রা — এখানে একটা public getter, ওখানে একটা setter, দুটো class কোনো কারণ ছাড়াই জড়িয়ে যাচ্ছে। সমাধান হলো জনাব সালামের সমাধান, আর এর নাম হলো: Move Field।
Move Field কী জিনিস?
Move Field হলো এমন একটা refactoring যেখানে একটা field এক class (the source) থেকে অন্য class-এ (the target) সরানো হয় — সেই class-এ যেটা আসলে ওই ডেটা ব্যবহার করে। এটা Martin Fowler-এর Refactoring catalog-এ আছে, object-এর মধ্যে feature সরানোর একটা মৌলিক পদক্ষেপ হিসেবে, এর যমজ Move Method-এর পাশে।
এক নিঃশ্বাসে পুরো পরিকল্পনা:
- target class-এ একই field তৈরি করো।
- একটা একটা করে পুরনো field থেকে নতুনটায় সব read আর write redirect করো।
- কেউ আর পুরনোটা না ছুঁলে সেটা মুছে দাও।
সবসময়ের মতো refactoring-এ, behaviour বদলানো যাবে না। প্রোগ্রাম আগে যা হিসাব করত পরেও তাই করবে। যা বদলায় তা হলো কোডের আকার: ডেটা এখন সেই method-গুলোর পাশে থাকে যেগুলো তার উপর নির্ভর করে।
একটু ভাবো — field-এর বাড়ি কেন গুরুত্বপূর্ণ? যখন দেখো annualInterestRate declare হয়েছে InterestPolicy-র ভেতরে, কোড বলছে: "সুদের হার policy-র ব্যাপার।" যখন একই field Account-এর ভেতরে থাকে, কোড মালিকানা নিয়ে একটু মিথ্যা বলছে। কোডে মিথ্যা দামী — প্রতিটা নতুন programmer-কে কষ্ট করে সত্য আবিষ্কার করতে হয়, ঠিক যেমন Green Valley-র প্রতিটা নতুন শিক্ষককে অবাক হয়ে জানতে হতো যে ক্রিকেট কিট graph paper chart-এর পেছনে আছে।
একটা mechanical cost-ও আছে। বাইরে থেকে বেশি ব্যবহার হয় এমন field ধরে রাখা class-কে public getter আর setter expose করতে বাধ্য করে। ওই খোলা দরজা দিয়ে আরও বেশি বাইরের code field-এর উপর নির্ভর করতে আসে, আর আস্তে আস্তে দুটো class আলাদা করা অসম্ভব হয়ে যায়। মিসেস রহেলার খোলা আলমারি ঠিক এটাই: এমন কিছুতে public modifier যেটা শুরু থেকেই private হওয়া উচিত ছিল।
একটা সহজ detective কৌশল: একটা field নিয়ে সব জায়গা দেখো যেখানে পড়া বা লেখা হচ্ছে। IDE-র "Find Usages" এক ক্লিকেই করে দেয়। তালিকায় যদি একটা অন্য class আধিপত্য করে, তুমি গণিত ঘরে ক্রিকেট কিট খুঁজে পেয়েছ। field-এর আসল ঘর হলো সেই usage তালিকার সবার উপরের class — আর প্রায়ই, ডেটার অর্থও সংখ্যার সাথে একমত হয়।
আরেকটা কথা মাথায় রাখো: Move Field প্রায়ই প্রথম domino। ডেটা নিজের ঘরে গেলে, পুরনো জায়গায় আটকে থাকা method-গুলো সেটাকে follow করতে পারে — সেটাই Move Method। Fowler বলেন যখন fields আর methods একসাথে নতুন জায়গায় যায়, তুমি আসলে Extract Class বা Inline Class করছ, Move Field হলো সেই কাজের হাতিয়ার।
কলেজ কোণা: Move Field হলো ডেটার দিক থেকে সেই একই metrics-এর উত্তর যা Move Method ঠিক করে। মূলত বাইরে থেকে access করা হয় এমন field একটা textbook encapsulation violation — metrics-এ এটা দেখা যায় বেশি CBO (Coupling Between Objects — প্রতিটা বাইরের read বা write একটা dependency edge) আর বেশি LCOM (Lack of Cohesion of Methods — ধরে রাখা class-এর নিজের method কখনো field ছোঁয় না) হিসেবে। Detection tool-গুলো method-এর দিক থেকে দেখে: ATFD (Access To Foreign Data) দিয়ে Feature Envy-তে flag করা method ঠিক সেই field-গুলোর দিকে আঙুল তোলে যেগুলো সরানো দরকার। এক class-এর কয়েকটা method একই বিদেশি field-কে envy করলে, একটা field সরানো অনেক method সরানোর চেয়ে সস্তা — একটা কিট সরানো বিশটা ছেলের যাত্রার চেয়ে ভালো।
কখন দরকার হয়?
এই পরিস্থিতিগুলো দেখলে Move Field ব্যবহার করো:
১. যে field নিজের class-এর চেয়ে অন্য class বেশি ব্যবহার করে। এটা হলো Feature Envy-র ডেটা সংস্করণ। সাধারণত বলি একটা method অন্য class-এর ডেটাকে envy করে — কিন্তু উল্টো দিক থেকে দেখলে বলা যায় ডেটা তার behaviour থেকে দূরে আটকে আছে। যেভাবেই দেখো, read আর write বারবার class boundary পার হচ্ছে।
২. দুটো class পরস্পরকে একটু বেশিই চেনে। Class A ক্রমাগত class B-র field খোঁচায়, B খোঁচায় A-র field — এটা Inappropriate Intimacy। প্রতিটা field-কে যে class সত্যিই চালায় তার কাছে সরালে দুটো class-কে পরস্পর সম্পর্কে কম জানতে হয়।
৩. বড় কাজের প্রস্তুতি। Large Class-এ Extract Class করার পরিকল্পনা? আসল কাজ হয় field by field আর method by method — Move Field হলো সেই কাজের হাতুড়ি। একইভাবে, Inline Class দিয়ে Lazy Class-কে তার ব্যবহারকারীর সাথে মেশানোর সময়, প্রতিটা field Move Field-এ চড়ে যায়।
৪. অর্থ বদলে গেছে। কখনো কোড একসময় ঠিক ছিল, কিন্তু domain বেড়েছে। discount percentage একসময় Order-এর একটা সাধারণ সংখ্যা ছিল; এখন পুরো একটা DiscountPolicy class আছে, আর percentage স্পষ্টতই তার ভেতরে থাকা উচিত। design বিকশিত হওয়ার সাথে সাথে Move Field কোডকে সৎ রাখে।
নিচের busFee field-এর জন্য detective কৌশল চালাও, প্রমাণ সাজাও:
busFee-তে access | কোন class থেকে | Read নাকি write |
|---|---|---|
student.busFee * 4 in termBillFor | TransportOffice | read |
student.busFee += in applyFuelHike | TransportOffice | read + write |
Student-এর ভেতরে কোথাও | Student | কোথাও না |
কাগজে মালিক কখনো ছোঁয় না; একজন বাইরের লোক প্রতিবার ছোঁয়। একই প্রমাণ chart হিসেবে:
আর কখন সরানোর আগে থামবে?
- অনেক ব্যবহারকারী, কোনো একক ঘর নেই। চারটা class যদি সমানভাবে field ব্যবহার করে, একটায় সরালে শুধু অভিযোগকারী বদলাবে। field আর তার অপারেশন নিজের নতুন class পাওয়ার যোগ্য হতে পারে — সেটাই Extract Class।
- Persistence আর serialisation। database column-এ map করা বা JSON format-এ লেখা field বাইরের জগতের সাথে আটকে আছে। সরালে migration, converter, আর version জটিলতা আসে। সম্ভব, কিন্তু খরচ মেপে দেখো।
- একটা উচ্চস্বরের method। শুধু একটা method এই সপ্তাহে পাঁচবার ব্যবহার করেছে বলে field সরাবে না। transfer দেওয়ার আগে codebase জুড়ে পুরো read আর write-এর ছবিটা দেখো।
সিদ্ধান্ত, একটা chart হিসেবে। দুটো প্রশ্ন: কে ডেটা ব্যবহার করে, আর এটা বাইরের জগতে কতটা আটকা?
আগে আর পরে এক নজরে
একটা ছোট TypeScript উদাহরণ। স্কুলের Student class busFee ধরে রাখছে — কিন্তু এটা ছোঁয় শুধু TransportOffice।
// BEFORE — the bus fee data sits on Student, used only by TransportOffice
class Student {
constructor(
public name: string,
public rollNumber: number,
public busFee: number, // who actually uses this?
) {}
}
class TransportOffice {
termBillFor(student: Student): number {
return student.busFee * 4; // reaching into Student every time
}
applyFuelHike(student: Student, percent: number): void {
student.busFee += (student.busFee * percent) / 100;
}
}busFee হলো ক্রিকেট কিট। Student কখনো ব্যবহার করে না; TransportOffice পড়ে, লেখে, bill করে। ডেটা সেই office-এ সরাও যেটা concept-টার মালিক:
// AFTER — the transport office keeps its own fee register
class Student {
constructor(
public name: string,
public rollNumber: number,
) {}
}
class TransportOffice {
private busFees = new Map<number, number>(); // rollNumber -> fee
setBusFee(rollNumber: number, fee: number): void {
this.busFees.set(rollNumber, fee);
}
termBillFor(student: Student): number {
return (this.busFees.get(student.rollNumber) ?? 0) * 4;
}
applyFuelHike(percent: number): void {
for (const [roll, fee] of this.busFees) {
this.busFees.set(roll, fee + (fee * percent) / 100);
}
}
}লাভগুলো দেখো। Student শুধু সত্যিকারের student-সংক্রান্ত ডেটায় ছোট হয়ে গেল। fee — একটা transport concept — এখন transport office-এ থাকে, billing আর hike rule-এর পাশে। আর বোনাসটা লক্ষ্য করো: applyFuelHike-এর আর কোনো student দরকার নেই; office তার নিজের register একটা loop-এ আপডেট করে।
ধাপে ধাপে, নিরাপদে
ডেটা সরানো method সরানোর চেয়ে সূক্ষ্ম। কারণ অনেক জায়গা field পড়তে বা লিখতে পারে। একটা method-এর একটা body থাকে যা copy করা যায়; একটা field-এ সব জায়গায় ছড়ানো ছোট ছোট অনেক স্পর্শ থাকে, আর একটাও write মিস করলে দুটো cupboard থাকে যার প্রতিটায় অর্ধেক কিট। নিরাপদ পদ্ধতি যায় একটা দরজা দিয়ে।
ধাপ ১ — আগে field encapsulate করো (Self-Encapsulate Field)। field যদি public হয়, যে কেউ সরাসরি ছুঁতে পারে, আর তুমি নিরাপদে সবাইকে redirect করতে পারবে না। তাই আগে সব access একটা getter আর setter দিয়ে যাওয়া নিশ্চিত করো:
// INTERMEDIATE STATE 1 — one doorway for all access
class Student {
private _busFee: number;
get busFee(): number {
return this._busFee;
}
set busFee(value: number) {
this._busFee = value;
}
}Compile করো আর test চালাও। Behaviour অপরিবর্তিত — শুধু একটা দরজা যোগ হয়েছে। স্কুলের ভাষায়: কিট সরানোর আগে জনাব সালাম নির্দেশ দিলেন সব কিটের অনুরোধ মিসেস রহেলার desk দিয়ে যেতে হবে — ছেলেরা সরাসরি আলমারিতে হাত দিতে পারবে না। একটা দরজা হলে, দরজা redirect করা মানে সবাই redirect হয়।
ধাপ ২ — target class-এ field তৈরি করো। TransportOffice-এ fee storage যোগ করো (roll number দিয়ে key করা একটা map), আর পূরণ করার জন্য একটা setter। Student-এ এখনো কিছু মুছবে না।
ধাপ ৩ — পুরনো দরজা নতুন ঘরে forward করো। এটাই চালাক কৌশল। পুরনো getter আর setter থাকে, কিন্তু এখন তারা target-এ forward করে:
// INTERMEDIATE STATE 2 — old doorway forwards to the new home
class Student {
constructor(private transport: TransportOffice, public rollNumber: number) {}
get busFee(): number {
return this.transport.feeOf(this.rollNumber);
}
set busFee(value: number) {
this.transport.setBusFee(this.rollNumber, value);
}
}এখন ডেটার মাত্র একটা আসল copy আছে, কিন্তু পুরনো সব caller এখনো কাজ করে। Compile করো, test করো, শ্বাস নাও।
ধাপ ৪ — একটা একটা করে caller redirect করো। যেখানে যেখানে student.busFee আছে সেখানে গিয়ে সরাসরি TransportOffice-এর সাথে কথা বলার জন্য বদলাও। প্রতিটা redirection-এর পর compile করো আর test করো। ছোট ছোট ধাপে কিছু ভাঙলে কোন পরিবর্তনে ভেঙেছে সেটা বোঝা যায়।
ধাপ ৫ — পুরনো দরজা ভাঙো। "Find Usages" যখন দেখাবে Student-এর getter আর setter-এ zero caller, সেগুলো মুছে দাও। Field সরকারিভাবে নতুন ঘরে চলে গেছে।
ধাপ ৬ — method-গুলোকে follow করতে দাও। source class-এ এমন method খোঁজো যেগুলো শুধু সেখানে ছিল কারণ field ছিল। এখন তারা একটা খালি ঘরে দাঁড়িয়ে। Move Method দিয়ে তাদের ঘরে পাঠাও।
Field-এর পুরো যাত্রা, state হিসেবে আঁকা — প্রতিটা state একটা stable, কার্যকর প্রোগ্রাম:
এখানে test বাধ্যতামূলক। একটা field-এ read আর write দুটোই থাকে, আর একটা write redirection মিস করা ধূর্ত: প্রোগ্রাম চলতে থাকে, কিন্তু ডেটার দুটো copy আলাদা হয়ে যায়, আর দিন কয়েক পরে ভুল উত্তর পাও। প্রতিটা redirection-এর পর test চালাও, আর নিশ্চিত হও অন্তত একটা test field-এর মান বদলায় আর ফিরে পড়ে। এরকম test না থাকলে শুরু করার আগে লিখে নাও।
একটা বড় real-life উদাহরণ
এবার TypeScript-এ পুরো ক্রিকেট কিটের গল্প। maths classroom sports equipment রাখছে, আর sports period-এর code বারবার ভুল ঘরে হাত বাড়াচ্ছে।
// BEFORE
class MathsClassroom {
public charts: string[] = ["Graph paper", "Geometry models"];
public cricketKit: string[] = ["Bat", "Pads", "Stumps", "Helmet"]; // ?!
teachAlgebra(): string {
return "Solving equations using " + this.charts.join(", ");
// note: cricketKit is never used in this class
}
}
class SportsPeriod {
startMatch(room: MathsClassroom): string {
// the daily expedition to the maths room
const kit = room.cricketKit;
if (!kit.includes("Stumps")) return "Cannot play — stumps missing!";
return "Match started with: " + kit.join(", ");
}
addEquipment(room: MathsClassroom, item: string): void {
room.cricketKit.push(item); // writing into another class's cupboard
}
}স্পর্শ গোনো। MathsClassroom নিজে কখনো cricketKit খোলে না। SportsPeriod প্রতিটা method-এ পড়ে আর লেখে। ডেটা তার behaviour থেকে দূরে সরে গেছে। Move Field প্রয়োগ করো:
// AFTER
class MathsClassroom {
public charts: string[] = ["Graph paper", "Geometry models"];
teachAlgebra(): string {
return "Solving equations using " + this.charts.join(", ");
}
}
class SportsRoom {
private cricketKit: string[] = ["Bat", "Pads", "Stumps", "Helmet"];
kitList(): string[] {
return [...this.cricketKit];
}
addEquipment(item: string): void {
this.cricketKit.push(item);
}
hasStumps(): boolean {
return this.cricketKit.includes("Stumps");
}
}
class SportsPeriod {
constructor(private room: SportsRoom) {}
startMatch(): string {
if (!this.room.hasStumps()) return "Cannot play — stumps missing!";
return "Match started with: " + this.room.kitList().join(", ");
}
}পরের ছবিতে তিনটা জিনিস মজার:
- Kit
privateহয়ে গেল। আগে বাইরের লোক হাতড়াতে পারে বলে public ছিল। এখন sports room নিজের আলমারি নিয়ন্ত্রণ করে আর ভদ্রভাবে প্রশ্নের সুযোগ দেয় যেমনhasStumps()। MathsClassroomসৎ। পড়লে বুঝতে পারবে maths classroom কীসের। কোণে কোনো চমকের sports bag নেই।- Method-গুলো ডেটার পিছু নিল।
addEquipmentকিটের সাথে চলে গেল — একটা ছোট Move Method, Move Field-এর পিঠে চড়ে, ঠিক Fowler যেভাবে বলেন: ডেটা সরাও, behaviour নিজেই ঘরে আসে।
সাপ্তাহিক traffic count, আগে আর পরে:
যে দুটো স্পর্শ থেকে যায় সেগুলো SportsRoom-এর public method দিয়ে যায় — অন্য কারো আলমারিতে হাত না দিয়ে, সঠিক দরজায় ভদ্র টোকা।
কলেজ কোণা: kitList() লক্ষ্য করো — এটা copy return করে ([...this.cricketKit]), live array নয়। এটাই field সরানো আর সত্যিকারের encapsulation-এর পার্থক্য। live collection return করলে প্রতিটা caller এখনো তোমার আলমারিতে হাত রেখেছে। Researcher-রা এটাকে representation exposure বলেন; code review-এ শুনবে "internals leak করো না"। একটা Move Field যেটা leaked mutable reference দিয়ে শেষ হয় সেটা কিট সরিয়েছে কিন্তু আলমারি খোলা রেখে গেছে।
C# আর Python-এ একই refactoring
একই কাজ C#-এ, bank-এর উদাহরণে। Account interest rate ধরে রাখছে, কিন্তু শুধু InterestCalculator-ই এটা ব্যবহার করে।
// BEFORE
class Account
{
public decimal Balance { get; set; }
public decimal AnnualInterestRate { get; set; } // stranded data
}
class InterestCalculator
{
public decimal MonthlyInterest(Account account)
{
// reaching back into Account for the rate every time
return account.Balance * account.AnnualInterestRate / 12m;
}
}Rate interest rules-কে বর্ণনা করে, account-কে নয়। সরিয়ে দাও:
// AFTER
class Account
{
public decimal Balance { get; set; }
}
class InterestCalculator
{
private readonly decimal _annualRate;
public InterestCalculator(decimal annualRate)
{
_annualRate = annualRate;
}
public decimal MonthlyInterest(Account account)
{
return account.Balance * _annualRate / 12m;
}
}দেখো dependency কীভাবে সরু হলো। আগে calculator Account থেকে দুটো জিনিস চাইত; এখন শুধু Balance চাই। account এমন ডেটার setter expose করা বন্ধ করল যেটা সে কখনো ব্যবহার করেনি, আর rate এমনকি readonly হয়ে গেল। ডেটা ঘরে গেলে প্রায়ই পুরনো জায়গায় যেভাবে সম্ভব ছিল না সেভাবে lock down করা যায়।
একই ধারণার Python সংস্করণ:
# BEFORE — the rate is stranded on the account
class Account:
def __init__(self, balance, annual_rate):
self.balance = balance
self.annual_rate = annual_rate # never used by Account itself
class InterestCalculator:
def monthly_interest(self, account):
return account.balance * account.annual_rate / 12
# AFTER — the rate lives with the rules that use it
class Account:
def __init__(self, balance):
self.balance = balance
class InterestCalculator:
def __init__(self, annual_rate):
self._annual_rate = annual_rate
def monthly_interest(self, account):
return account.balance * self._annual_rate / 12IDE সাপোর্ট
| Tool | সাপোর্ট | কীভাবে |
|---|---|---|
| IntelliJ IDEA / JetBrains family | শক্তিশালী | Field select করো, F6 (Refactor → Move); static field সম্পূর্ণ স্বয়ংক্রিয়ভাবে সরে |
| JetBrains Rider / ReSharper (C#) | শক্তিশালী | Move static field সরায়; Extract Class instance field তার method-সহ নিতে পারে |
| Visual Studio (plain) | Manual | "Find All References" দিয়ে প্রতিটা redirection guide করে নিরাপদ ধাপগুলো ব্যবহার করো |
| VS Code (TypeScript) | Manual | Rename + "Find All References" হলো power tool; single-doorway technique বাকিটা করে |
যে tool-ই হোক, single-doorway ধারণা (আগে encapsulate, তারপর দরজা redirect) move-টাকে নিরাপদ রাখে — IDE শুধু boring অংশগুলো automate করে।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / খরচ | |
|---|---|---|
| Design-এর সততা | Field-এর অবস্থান শেষ পর্যন্ত সত্য বলে কোন class concept-টার মালিক | ভুল target বেছে নিলে নতুন মিথ্যা বলে — আগে সব usage পড়ো |
| Encapsulation | পুরনো class public getter/setter মুছে দেয়; নতুন ঘর field-কে private বা readonly করতে পারে | Transition চলাকালে temporary delegating accessor একটু noise যোগ করে |
| Coupling | Read আর write প্রতিদিন class boundary পার হওয়া বন্ধ করে | Source-এর এখনো মাঝে মাঝে access দরকার হলে back-reference আসতে পারে — সাময়িক রাখো |
| Unblocking | আটকে থাকা method-কে Move Method দিয়ে ডেটার পিছু নিতে মুক্ত করে | Missed write-site দুটো আলাদা হওয়া data copy বানায় — এক দরজা দিয়ে redirect করো |
| Persistence | সামনে পরিষ্কার domain model | ORM-mapped বা serialised field schema migration সাথে টানে — বাজেট করো |
পরিবারের নোট: Move Field আর Move Method হলো দুটো ইট; Extract Class আর Inline Class হলো সেগুলো দিয়ে তৈরি ভবন। কয়েকটা field একই নতুন জায়গায় যেতে চাইলে, এক ইট করে না রেখে Extract Class ব্যবহার করো। কোনো class-এর সব field একটা একটা করে চলে যাচ্ছে দেখলে, স্বীকার করো এটা Inline Class আর কাজ ঠিকমতো শেষ করো।
কলেজ কোণা: design review-এ refactoring defend করতে সংখ্যা চাইলে, দুটো class-এর afferent আর efferent coupling মাপো। সরানোর আগে Student-এর একটা অতিরিক্ত afferent dependency ছিল আর বাড়তি public surface; পরে, তার public API সংকুচিত হলো আর dependency arrow দুর্বল হয়ে শুধু identity-তে (rollNumber) নেমে এল। বারবার Move Field করাই codebase-এর instability metric-কে সৎ রাখে।
কোন smell ঠিক করে?
| Smell | Move Field কীভাবে সাহায্য করে |
|---|---|
| Feature Envy | সমাধানের ডেটার দিক — লোভী method সরানোর বদলে লোভনীয় ডেটা ঘরে সরাও |
| Inappropriate Intimacy | প্রতিটা state তার আসল মালিকের কাছে থাকলে class-গুলো একে অপরের field খোঁচানো বন্ধ করে |
| Large Class | আটকে থাকা field বের করে নেওয়া বড় class-কে ছোট করে |
| Shotgun Surgery | ডেটা আর তার নিয়ম এক class-এ মিললে, একটা পরিবর্তন একটা file edit করে |
| Lazy Class | Inline Class চলাকালে, Move Field হলো প্রতিটা field host-এ নিয়ে যাওয়ার হাতিয়ার |
দ্রুত revision box
+--------------------------------------------------------------+
| MOVE FIELD — CHEAT CARD |
+--------------------------------------------------------------+
| Story : cricket kit in the maths room -> sports room |
| Problem : field declared in class A, used daily by class B |
| Detect : Find Usages — one OTHER class dominates the list |
| Fix : encapsulate -> create in target -> old accessor |
| delegates -> redirect callers 1-by-1 -> delete |
| Rule : data belongs where its BEHAVIOUR lives |
| Test : after every redirection; include a write+read test|
| Watch : missed writes = two drifting copies of the data |
| Don't : move ORM/serialised fields casually; move when |
| many classes share it equally (try Extract Class) |
| Friends : Self-Encapsulate Field, Move Method, Extract Class|
+--------------------------------------------------------------+অনুশীলনের প্রশ্ন
ধরো একটা ছোট canteen system যেখানে একটা field ভুল ঘরে আছে।
class Student {
constructor(
public name: string,
public rollNumber: number,
public canteenDiscountPercent: number, // hmm...
) {}
}
class CanteenCounter {
billFor(student: Student, amount: number): number {
const discount = (amount * student.canteenDiscountPercent) / 100;
return amount - discount;
}
doubleDiscountDay(student: Student): void {
student.canteenDiscountPercent *= 2;
}
}তোমার কাজ:
canteenDiscountPercent-এর প্রতিটা read আর write তালিকাভুক্ত করো। কোন class আধিপত্য করে? কোন class কখনো ব্যবহার করে না? এই field-এর জন্য নিজে চিত্র ৩-এর মতো pie আঁকো।- single-doorway পদ্ধতি ব্যবহার করে field-টা
CanteenCounter-এ সরাও: আগেStudent-এ encapsulate করো, delegating intermediate সংস্করণ লেখো, তারপরbillForআরdoubleDiscountDayredirect করো, তারপর পুরনো accessor মুছো। প্রতিটা ধাপের পর test করো (চেষ্টা করো: amount 100, discount 10 percent → bill 90; double-discount day-এর পরে → bill 80)। মনে রাখোdoubleDiscountDayএকটা write — এটাই সেই redirection যা তাড়াহুড়ো করলে কষ্ট দেয়। - সরানোর পরে, discount data কি
CanteenCounter-এর ভেতরেprivateহতে পারে? সেটা করো, আর নিশ্চিত হও যে expose করা যেকোনো list বা map হলো copy, live নয়। - এই field-কে চিত্র ৪-এর quadrant-এ রাখো: কে ব্যবহার করে, আর কতটা আটকানো? school-এর database-এ প্রতিটা student record-এর বিপরীতে discount store করা থাকলে তোমার উত্তর কি বদলাত?
- Bonus প্রশ্ন: ধরো library-ও book fine-এর জন্য একই discount ব্যবহার করতে শুরু করে। এখন দুটো class field ব্যবহার করে। এটা কি এখনো
CanteenCounter-এ থাকা উচিত? একটা ছোটDiscountPolicyclass দেখতে কেমন হবে sketch করো (code দরকার নেই), আর সেটা বানাতে কোন refactoring ব্যবহার করবে তার নাম দাও।
প্রশ্ন ৫-এর উত্তর যদি "Extract Class" হয়, তুমি আজকের ইটকে আগামীকালের ভবনের সাথে জুড়ে দিয়েছ — ঠিক এভাবেই real refactoring কাজ করে। আর প্রশ্ন ৪ যদি database নিয়ে দ্বিধায় ফেলে, ভালো: তুমি শিখে গেছ যে কিছু ক্রিকেট কিট মেঝেতে bolt করা — সেগুলো সরাতে দুজন শক্তিশালী ছেলে নয়, একটা work order লাগে।
সচরাচর জিজ্ঞাসা
- Move Field মানে আসলে কী?
- Move Field মানে হলো একটা field — মানে একটা ডেটার টুকরা — যে class-এ declare করা আছে সেখান থেকে সেই class-এ সরানো যেটা আসলে ওটা ব্যবহার করে। নতুন class-এ field তৈরি করো, একটা একটা করে সব read আর write নতুন জায়গায় পাঠাও, তারপর পুরনো field মুছে দাও। প্রোগ্রাম আগের মতোই চলে, শুধু ডেটার বাড়ি বদলায়।
- কীভাবে বুঝব কোনো field আসলে কোথায় থাকা উচিত?
- সব read আর write দেখো। অন্য কোনো class যদি নিজের class-এর চেয়ে অনেক বেশি ওই field ছোঁয়, তাহলে সেই অন্য class-ই আসল ঘর। আর মানেটাও ভাবো: এই ডেটা কোন concept বর্ণনা করে? interest rate, interest policy-র কথা বলে, account-এর নয় — যদিও আজকে account-এ store করা থাকে।
- সরানোর আগে কেন field encapsulate করতে হবে?
- field যদি public হয়, project-এর যেকোনো জায়গার code সরাসরি ছুঁতে পারে, আর তুমি নিরাপদে সেগুলো redirect করতে পারবে না। আগে field-টাকে getter আর setter-এর মধ্যে ঢুকিয়ে দাও (Self-Encapsulate Field), তাহলে একটাই দরজা থাকবে। তখন দরজা redirect করলে সবাই redirect হয়ে যাবে, কেউ বাদ পড়বে না।
- Move Field আর Move Method-এর মধ্যে সম্পর্কটা কী?
- এরা জুটি। অনেক সময় একটা method ভুল class-এ থাকে কারণ দরকারি field সেখানে আটকে আছে। আগে field সরাও, তাহলে লোভী method-গুলো Move Method দিয়ে নিজের ঘরে ফিরতে পারে। Fowler এই দুটোকে object-এর মধ্যে feature সরানোর দুটো মূল অপারেশন হিসেবে দেখান।
- কখন field সরানো উচিত না?
- সরাবে না যদি অনেক class সমানভাবে ব্যবহার করে — তখন কোনো একটা সঠিক ঘর নেই, Extract Class ভালো হবে। database mapping বা serialised format-এ আটকানো field সরাতে হলে schema migration আর অনেক ঝামেলা — সাবধানে ভাবো। আর মাত্র একটা method কয়েকবার ব্যবহার করেছে বলে field সরাবে না; পুরো ছবিটা দেখো আগে।
আরো দেখো
সম্পর্কিত পাঠ
Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে
একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।
Extract Class: অতিরিক্ত কাজে ডুবে যাওয়া class-কে একটু সাহায্য করো
Extract Class refactoring শেখো একটা মজার school office-এর গল্পের মাধ্যমে। একটা overloaded class-কে দুটো focused class-এ ভাগ করো — প্রতিটার একটাই কাজ।
Inline Class: যে Class কিছুই করে না, তাকে মিলিয়ে দাও
Inline Class refactoring শেখো একটা school committee-র গল্পের মাধ্যমে। যে class কিছুই করে না তাকে তার user-এর সাথে মিলিয়ে দাও আর অকারণ layer মুছে ফেলো।
Feature Envy: যে method সারাদিন অন্যের class-এ বসে থাকে
Feature Envy code smell শেখো একটা সহজ স্কুলের গল্পের মাধ্যমে। যখন একটা method নিজের class-এর চেয়ে অন্য class-এর data বেশি ব্যবহার করে, তখন সেটা আসলে ওই অন্য class-এই থাকার কথা। সারানোর উপায় হলো Move Method।