Replace Method with Method Object: বড় রান্নার জন্য আলাদা স্টেশন বানাও
Replace Method with Method Object শেখো বিয়ের রান্নাঘরের গল্প দিয়ে — TypeScript ও C# উদাহরণ আর নিরাপদ ধাপ-ধাপ পদ্ধতি দিয়ে, একদম শুরু থেকে।
বিয়ের বিরিয়ানি স্টেশনের গল্প
ধরো তোমার বাড়িতে বিয়ে। দুইশো মেহমান আসবে। বাড়ির রাঁধুনি সালমা খালা অসাধারণ — ত্রিশ বছরের অভিজ্ঞতা, বিয়ে থেকে শুরু করে ঈদের ভোজ পর্যন্ত সব সামলেছেন। কিন্তু আজ রাতের মেনুতে একটা দৈত্য আছে: বিয়ের বিরিয়ানি। ভেজানো চাল, বেরেস্তা, মেরিনেট মুরগি, জাফরানের দুধ, পুদিনা, ঘি, দম — ডজনখানেক উপকরণ, সবকিছু একসাথে সামলাতে হবে।
প্রথমে সালমা খালা চেষ্টা করলেন সাধারণ চুলায়, রান্নাঘরের মাঝখানে বসে। বিপদ শুরু হলো। বিরিয়ানির জন্য এগারোটা বাটি দরকার, কিন্তু কাউন্টারে দুপুরের ডাল, চায়ের কেটলি আর কারো টিফিন বক্সও আছে। তার ভাগ্নে রুবেল পাশ দিয়ে যাওয়ার সময় জিজ্ঞেস করল, "খালা, কোন বাটিটা বিরিয়ানির বেরেস্তা আর কোনটা রায়তার?" ঘুরে না তাকিয়ে খালা উত্তর দিতে পারছেন না।
আরও বাজে হলো, রুবেলকে কোনো কাজ দিতে গেলে এক হাত ভর্তি বাটিও ধরিয়ে দিতে হয়। "এটা নাও, এটাও, এটাও — কাজ শেষে চালের বাটি আর মশলার বাটি ফিরিয়ে দিও।" রুবেল একটা বাটি ফেলে দিল। ডালে বেরেস্তা পড়ে গেল। সালমা খালার ধৈর্য কমতে শুরু করল।
তারপর জ্ঞানী ক্যাটারিং চাচা এলেন। এক নজর দেখেই স্পষ্ট কথাটা বললেন: "বেটা, এই রান্নাটা একজনের একটা চুলার জন্য না। এটাকে নিজের স্টেশন দাও।"
উঠানে আলাদা বিরিয়ানি স্টেশন বসানো হলো। নিজের টেবিল, নিজের চুলা, আর লেবেল লাগানো তাক: একটা তাক ভেজানো চালের জন্য, একটা বেরেস্তার জন্য, একটা জাফরানের দুধের জন্য। বিরিয়ানির দরকার সব কিছু স্টেশনেই আছে।
তখন জাদু হলো। সালমা খালা রুবেলকে বলতে পারলেন, "বিরিয়ানি স্টেশনে যাও, layering step করো।" রুবেলের এগারোটা বাটি বহন করতে হলো না — সব তাকে আছে, ঠিক জায়গায়। বিশাল কাজটা এখন ছোট ছোট নামওয়ালা ধাপে ভাগ হয়ে গেল: চাল তৈরি, পেঁয়াজ ভাজা, layer দেওয়া, দম দেওয়া। সন্ধ্যার মধ্যে তিনজন মিলে স্টেশন চালাচ্ছে, আর সালমা খালা চা নিয়ে তদারকি করছেন।
এই ছবিটা মনে রাখো। কোডে, অনেক জট পাকানো local variable-সহ একটা বিশাল method হলো সাধারণ চুলায় বিরিয়ানির মতো। সেটাকে helper method-এ ভাগ করা যাচ্ছে না, কারণ প্রতিটা helper-এর কাছে সেই local variable-গুলো ঢালাওভাবে পাঠাতে হবে — এগারো বাটি হাতে রুবেলের মতো।
সমাধান হলো ক্যাটারিং চাচার পরামর্শ: সেই একটা রান্নার জন্য আলাদা স্টেশন বানাও। Refactoring-এর ভাষায়, স্টেশন হলো একটা নতুন class, লেবেল লাগানো তাক হলো field, আর রান্নার ধাপগুলো হলো ছোট ছোট private method যেগুলো সব সেই তাক ব্যবহার করে। এই refactoring-এর নাম Replace Method with Method Object।
Replace Method with Method Object কী?
Replace Method with Method Object একটা লম্বা, জট পাকানো method-কে নিজের class-এ নিয়ে যায়। রেসিপিটা এরকম:
- method টা যে কাজ করে সে নামে একটা নতুন class তৈরি করো —
BiryaniCooker,PriceCalculator,ReportBuilder। - method-এর parameter আর local variable-গুলো নতুন class-এ private field হিসেবে নাও। Parameter-গুলো constructor-এর মাধ্যমে সেট হবে, local-গুলো কাজের সময়।
- method-এর body নতুন class-এর একটা public method-এ নাও। নাম দাও
compute(),run(), বাexecute()। - আসল method-টাকে one-liner করো — শুধু object তৈরি করে call করবে।
এটা কেন কাজ করে? আসলে scope-এর কারণে। আসল method-এর ভেতরে local variable-গুলো শুধু সেই method-ই দেখতে পায়। বাইরের কোনো helper method extract করতে গেলে সে সেই scope-এর বাইরে, তাই প্রতিটা মান parameter হিসেবে পাঠাতে হবে আর প্রতিটা পরিবর্তন return value হিসেবে ফেরত আনতে হবে।
পাঁচটা local variable যদি প্রতিটা fragment পড়ে আর লেখে, তাহলে extraction এতটাই বিশ্রী হয়ে যায় যে ছেড়ে দিতে ইচ্ছে করে। ঠিক রুবেলকে এগারো বাটি বহন করতে বলার মতো।
কিন্তু সেই local variable-গুলো একবার class-এর field হয়ে গেলে, তাদের scope বদলায় "এই একটা method" থেকে "পুরো object"-এ। Object-এর ভেতরের প্রতিটা helper method বিনামূল্যে সেগুলো দেখতে পায়, যেভাবে বিরিয়ানি স্টেশনের প্রতিটা রাঁধুনি প্রতিটা তাক নাগালে পায়।
হঠাৎ Extract Method — যেটা বারবার ব্যর্থ হচ্ছিল — অনায়াসে সহজ হয়ে যায়।
একটা লাইনে মনে রাখো: যখন কোনো method-এর local variable এতটাই জট পাকানো যে কিছু extract করাই যাচ্ছে না, তখন method-টাকে তার নিজের class-এ নিয়ে যাও — local variable-গুলো field হয়ে যাবে, আর প্রতিটা helper ধাপ সেগুলো পাবে। Method object লক্ষ্য না; এটা সেই কৌশল যা Extract Method-কে আবার সম্ভব করে তোলে।
নামের বিষয়ে একটু কথা। Refactoring বইয়ের প্রথম সংস্করণে (১৯৯৯) এই technique-এর নাম Replace Method with Method Object। দ্বিতীয় সংস্করণে (২০১৮) Martin Fowler নাম বদলে রাখলেন Replace Function with Command। কারণ তুমি যে object তৈরি করো — যেটা একটা কাজকে wrap করে, configure হয়, তারপর চালানো হয় — সেটাই command object। Command design pattern-এর কাছের আত্মীয়।
Fowler নতুন সংস্করণে সৎভাবে সতর্কও করেন: বেশিরভাগ সময় তিনি সাধারণ function পছন্দ করেন, কারণ একটা computation-এর জন্য পুরো class ভারী। এই refactoring তখনই ব্যবহার করো যখন জটিলতা সত্যিই দাবি করে। এটা Composing Methods পরিবারের ভারী অস্ত্র, রোজকার হাতিয়ার না।
কলেজ কর্নার: command object phrase টা সব design patterns কোর্সে আসে। Command object একটা কাজকে বস্তুতে পরিণত করে — "কিছু করা"-কে "ধরে রাখার মতো একটা জিনিস"-এ রূপান্তর করে। একবার কোনো কাজ object হয়ে গেলে সে এমন ক্ষমতা পায় যা সাধারণ function call কখনো পায় না। List-এ সংরক্ষণ করে পরে চালানো যায় (job queue), অন্য thread-এ চালানো যায়, সব input সহ log করা যায়, ব্যর্থতার পরে retry করা যায়, অথবা undo() method জুড়ে দেওয়া যায়। প্রতিটা text editor-এর undo stack আসলে একটা list of command objects।
JavaScript বা Python-এ closure-ও অনেকটা এই কাজ করতে পারে। পার্থক্য হলো: closure state অদৃশ্যভাবে capture করে, কিন্তু command object তার state named field হিসেবে ঘোষণা করে আর undo, describe-র মতো extra method expose করতে পারে। যখন এই extra power দরকার, class-টা তার complexity-র দাম উশুল করে। না হলে Fowler-এর পরামর্শই থাকে — সাধারণ function পছন্দ করো।
কখন দরকার?
সংকেতটা খুব নির্দিষ্ট, ভালো করে শিখে নাও: তুমি Extract Method দিয়ে একটা Long Method ছোট করার চেষ্টা করেছিলে, আর local variable-গুলো তোমাকে হারিয়ে দিয়েছে।
সেই হারটা সাধারণত এরকম লাগে:
- পনেরোটা লাইন select করলে যেগুলো স্পষ্টতই একটা ধাপ। Extract Method click করলে, IDE পাঁচটা parameter আর একটা অসম্ভব return-সহ নতুন function propose করল। Fragment টা
subtotal,taxRate, আরitemsপড়ে, আরsubtotalওdiscountলেখে। বেশিরভাগ ভাষায় শুধু একটা মান return করা যায়, তাই IDE tuple বা object return করার পরামর্শ দেয় — এতটাই বিশ্রী যে cancel করে দিলে। - অন্য fragment চেষ্টা করো। একই গল্প, অন্য পাঁচটা variable। Temporary variable-গুলো একটা জাল তৈরি করেছে যা পুরো method-কে একটা অবিভাজ্য blob হিসেবে আটকে রেখেছে।
- Method বাড়তেই থাকে, কারণ প্রতিটা নতুন requirement কে restructure করার চেয়ে জুড়ে দেওয়া সহজ।
আরও কিছু পরিস্থিতি একই দিকে ইঙ্গিত করে:
- Computation-এর নিজস্ব test দরকার। Algorithm তার নিজের class-এ গেলে, তৈরি করা input দিয়ে সরাসরি test করা যায়। বড় class টেনে আনতে হয় না।
- Algorithm ভুল বাড়িতে অতিথি।
Orderclass-এর ভেতরে ১০০ লাইনের pricing calculation সেই class-কে ফুলিয়ে দেয় আর order-এর আসল দায়িত্ব লুকিয়ে ফেলে।PriceCalculator-এ সরিয়ে দিলে host ছোট হয় — এটা Large Class-এও সাহায্য করে। - মনে হচ্ছে একটা design pattern লুকিয়ে আছে। লম্বা জট পাকানো computation প্রায়ই একটা Strategy বা Command হওয়ার অপেক্ষায় থাকে। Method object সেই জন্মের প্রথম ধাপ।
যে পরিস্থিতিতে ব্যবহার করা উচিত না:
- যদি হালকা কিছু refactoring — Split Temporary Variable, Replace Temp with Query, Remove Assignments to Parameters — local variable-এর জট সাধারণ Extract Method-এর জন্য যথেষ্ট খুলে দিতে পারে, তাহলে সেগুলোই করো আগে। ভারী সমস্যার জন্যই ভারী অস্ত্র।
- Method লম্বা হলেও যদি local variable-গুলো জট পাকানো না হয়, তাহলে সাধারণ Extract Method-ই কাজ করে। স্টেশন দরকার নেই।
নিচের quadrant হলো ক্যাটারিং চাচার বিচার, chart হিসেবে আঁকা। তোমার method যত top-right-এর দিকে, স্টেশন বানানো তত বেশি দরকার:
অনেকে Extract Method আর Replace Method with Method Object মিলিয়ে ফেলে। এই তুলনাটা সহজ করে দেখাচ্ছে:
| প্রশ্ন | Extract Method | Replace Method with Method Object |
|---|---|---|
| কী সরে? | কয়েক লাইন একটা নতুন function-এ | পুরো method একটা নতুন class-এ |
| Local variable-এর কী হয়? | Parameter ও return হিসেবে পাঠানো হয় | Private field-এ পরিণত হয় |
| খরচ | প্রায় বিনামূল্যে — একটা নতুন function | একটা class, একটা constructor, কয়েকটা field |
| কখন কাজ করে | Local কম বা স্বাধীন | Local অনেক আর জট পাকানো |
| কখন ব্যর্থ হয় | Fragment-এর ৪+ param আর ২+ return দরকার | প্রায় কখনো ব্যর্থ না, তবে প্রায়ই overkill |
| চেষ্টা করো | সবসময় আগে | Extraction হেরে যাওয়ার পরেই |
আগে ও পরে এক নজরে
ধরো একটা TypeScript function আছে যেটা student-এর scholarship score গণনা করে — marks, attendance, আর activity points থেকে। এর তিনটা working variable সব জায়গায় পড়া ও লেখা হচ্ছে, তাই কোনো অংশ cleanly extract করা যাচ্ছে না:
// BEFORE: three locals woven through every step — Extract Method keeps failing
function scholarshipScore(marks: number[], attendancePct: number, activityPoints: number): number {
let base = 0;
let bonus = 0;
let penalty = 0;
for (const m of marks) {
base += m;
if (m >= 90) bonus += 5; // writes bonus, reads marks
}
base = base / marks.length;
if (attendancePct < 75) {
penalty += (75 - attendancePct) * 2; // writes penalty
if (base > 80) penalty = penalty / 2; // ...but reads base!
}
bonus += Math.min(activityPoints, 20);
if (penalty > bonus) bonus = 0; // reads penalty, writes bonus
return Math.round(base + bonus - penalty);
}attendance block extract করার চেষ্টা করো: এটা attendancePct ও base পড়ে, আর penalty লেখে। activity block চেষ্টা করো: এটা activityPoints ও penalty পড়ে, আর bonus লেখে। প্রতিটা candidate fragment দুই বা তিনটা working variable ধরে। এটাই সাধারণ চুলায় বিরিয়ানি।
এখন method object — রান্নাটা নিজের স্টেশন পেল:
// AFTER: a station with shelves (fields) — every step can reach everything
class ScholarshipScorer {
private base = 0;
private bonus = 0;
private penalty = 0;
constructor(
private readonly marks: number[],
private readonly attendancePct: number,
private readonly activityPoints: number,
) {}
compute(): number {
this.scoreMarks();
this.applyAttendancePenalty();
this.applyActivityBonus();
return Math.round(this.base + this.bonus - this.penalty);
}
private scoreMarks(): void {
for (const m of this.marks) {
this.base += m;
if (m >= 90) this.bonus += 5;
}
this.base = this.base / this.marks.length;
}
private applyAttendancePenalty(): void {
if (this.attendancePct < 75) {
this.penalty += (75 - this.attendancePct) * 2;
if (this.base > 80) this.penalty = this.penalty / 2;
}
}
private applyActivityBonus(): void {
this.bonus += Math.min(this.activityPoints, 20);
if (this.penalty > this.bonus) this.bonus = 0;
}
}
// The original function becomes a one-line delegator
function scholarshipScore(marks: number[], attendancePct: number, activityPoints: number): number {
return new ScholarshipScorer(marks, attendancePct, activityPoints).compute();
}এখন compute() দেখো। এটা ক্যাটারিং চাচার checklist-এর মতো পড়া যায়: marks score করো, attendance penalty দাও, activity bonus দাও, total ফেরত দাও। প্রতিটা helper method শূন্য parameter নেয় আর কিছু return করে না, কারণ তাক — base, bonus, penalty — field হিসেবে সেখানেই আছে। যে extraction আগে অসম্ভব ছিল সেটা এখন অনায়াসে সহজ।
Caller-এর দিক থেকে কথোপকথন কঠিন হয় না। স্টেশন configure করো, রান্নার কথা বলো, ফলাফল নাও:
আর এখানে পুরস্কার মাপা হলো একমাত্র মুদ্রায় যেটা extraction-এর সময় গুরুত্বপূর্ণ — প্রতিটা helper ধাপকে কতটা মালপত্র বহন করতে হয়:
ধাপে ধাপে, নিরাপদ উপায়ে
এই refactoring অনেক কোড সরায়, তাই শৃঙ্খলা বেশি দরকার। সোনার নিয়ম: আগে একটা কার্যকর, পরীক্ষিত, delegating অবস্থায় পৌঁছাও, তারপর decompose শুরু করো।
শুরু করার আগে আর নিচের প্রতিটা ধাপের পরে test চালাও। সবচেয়ে বিপজ্জনক মুহূর্ত হলো ধাপ ৪ — যখন body নতুন class-এ copy করো আর local-গুলো field-এ রূপান্তর করো। একটা this. মিস হলে বা একটা local পেছনে রয়ে গেলে behavior নীরবে বদলে যেতে পারে। Method-এর test না থাকলে আগে characterization test লেখো: কয়েকটা input দাও, output রেকর্ড করো, assert করো। তুমি অস্ত্রোপচার করতে যাচ্ছো; pulse monitor ছাড়া অপারেশন করো না।
Scholarship উদাহরণটা একটু একটু করে refactor করব।
ধাপ ১ — খালি স্টেশন তৈরি করো। Computation-এর নামে একটা নতুন class তৈরি করো। এর বেশি কিছু না এখনই:
// INTERMEDIATE: empty class — code compiles, tests untouched and green
class ScholarshipScorer {}ধাপ ২ — প্রতিটা parameter-এর জন্য field যোগ করো, constructor-এর মাধ্যমে সেট করো। Parameter হলো স্টেশনে পৌঁছানো উপকরণ। পৌঁছানোর পরে আর বদলায় না, তাই readonly mark করো:
// INTERMEDIATE
class ScholarshipScorer {
constructor(
private readonly marks: number[],
private readonly attendancePct: number,
private readonly activityPoints: number,
) {}
}Test চালাও। এখনো সবুজ — এই class কেউ ব্যবহার করছে না।
ধাপ ৩ — প্রতিটা local variable-এর জন্য field যোগ করো। Local variable-গুলো হলো working তাক: base, bonus, penalty, প্রতিটা local-এর মতো একই initial value দিয়ে শুরু।
ধাপ ৪ — Method body compute()-এ copy করো আর প্রতিটা local ও parameter reference field reference-এ convert করো। এটা সূক্ষ্ম ধাপ। যন্ত্রের মতো কাজ করো: body paste করো, local declaration মুছে দাও (এখন সেগুলো field), আর প্রতিটা reference-এর আগে this. দাও। আসল method যদি host class-এর অন্য method call করে থাকে, তাহলে host object-টাও constructor-এ পাঠাও আর সেটার মাধ্যমে call করো।
ধাপ ৫ — আসল method-কে delegate করাও। পুরো body বদলে একলাইনে:
// INTERMEDIATE: behavior identical, structure new — the critical checkpoint
function scholarshipScore(marks: number[], attendancePct: number, activityPoints: number): number {
return new ScholarshipScorer(marks, attendancePct, activityPoints).compute();
}এখন পুরো test suite চালাও। এটাই সবচেয়ে গুরুত্বপূর্ণ checkpoint: একই input, একই output, নতুন ঘর। এই মুহূর্তে compute() এখনো একটা লম্বা কুৎসিত block — এটা ঠিক আছে। একই ধাপে decompose আর সরানো করো না। ট্রাক চলার সময় মুভার ফার্নিচার সাজায় না।
ধাপ ৬ — এখন compute() Extract Method দিয়ে মুক্তভাবে ভাগ করো। যেহেতু প্রতিটা আগের local এখন field, প্রতিটা fragment শূন্য parameter আর শূন্য return value দিয়ে extract হয়। একটা ধাপ extract করো, test চালাও। পরেরটা extract করো, test চালাও। এভাবেই scoreMarks(), applyAttendancePenalty(), আর applyActivityBonus() জন্ম নিয়েছে।
ধাপ ৭ — স্টেশন পরিষ্কার করো। সব field private রয়েছে কিনা নিশ্চিত করো। Class-এ একটা doc comment দাও। আর দেখো কোনো field readonly হতে পারে কিনা। Public surface ঠিক দুটো হওয়া উচিত: constructor আর compute()।
ধাপের বদলে অবস্থা হিসেবে দেখলে, method ঠিক চারটা অবস্থার মধ্য দিয়ে যায়। সবসময় জানা উচিত তুমি কোন অবস্থায় আছো:
কলেজ কর্নার: লক্ষ্য করো তুমি কী trade করলে — এটা একটা সত্যিকারের engineering trade-off, বিনামূল্যের ভোজ না।
Local variable-এর একটা চমৎকার বৈশিষ্ট্য আছে। তাদের জীবনকাল একটা method call, তাই অন্য কোনো কোড কখনো তাদের মাঝপথে দেখতে পারে না। Field অন্যরকম: সেগুলো object-এর ভেতরে shared mutable state, আর প্রতিটা helper method যেকোনো ক্রমে সেগুলো পড়তে ও লিখতে পারে।
Object বাইরে থেকে এখনো নিরাপদ (field-গুলো private, আর প্রতিটা computation-এর জন্য নতুন object তৈরি হয়)। কিন্তু ভেতরে, সঠিকতা নির্ভর করে compute()-এর ধাপগুলো সঠিক ক্রমে call করার উপর। আমাদের উদাহরণে applyAttendancePenalty()-এর আগে applyActivityBonus() call করলে penalty > bonus rule ভুল কাজ করবে।
এটাকে temporal coupling বলে: একটা ordering dependency যা compiler check করতে পারে না। Mature codebase এটা নিয়ন্ত্রণ করে compute()-কে একমাত্র conductor হিসেবে রেখে, আর helper method-গুলোকে কখনো একে অপরকে call করতে না দিয়ে। Functional programmer-রা কেন mutable state নিয়ে ভয় পান — এই ছোট class টা সেটার নিখুঁত classroom উদাহরণ।
বাস্তব জীবনের একটা বড় উদাহরণ
আবার বিয়ের গল্পে ফিরি। ধরো catering company-র একটা quoting function আছে যেটা একটা ভোজের খরচ হিসাব করে: উপকরণ, শ্রম, জ্বালানি, ভাড়া, উৎসবের extra charge। তিন বছর ধরে বড় হয়েছে, আর এখন এরকম দেখাচ্ছে — সত্যিকারের সাধারণ চুলায় বিরিয়ানি:
interface FeastOrder {
guests: number;
dishes: { name: string; costPerPlate: number; isSpecial: boolean }[];
isFestivalSeason: boolean;
needsTandoor: boolean;
}
// BEFORE: every block reads and writes ingredientCost, labourHours, surcharge
function quoteFeast(order: FeastOrder): number {
let ingredientCost = 0;
let labourHours = 0;
let surcharge = 0;
for (const dish of order.dishes) {
ingredientCost += dish.costPerPlate * order.guests;
labourHours += dish.isSpecial ? 3 : 1;
if (dish.isSpecial && order.guests > 100) {
labourHours += 2; // big special dishes need extra hands
surcharge += 500; // ...and a special-dish surcharge
}
}
if (order.isFestivalSeason) {
surcharge += ingredientCost * 0.1; // reads ingredientCost, writes surcharge
labourHours = labourHours * 1.25; // festival staff work slower shifts
}
if (order.needsTandoor) {
surcharge += 1500;
if (labourHours > 40) surcharge += 800; // reads labourHours, writes surcharge
}
const labourCost = labourHours * 200;
return Math.round(ingredientCost + labourCost + surcharge);
}Festival block extract করার চেষ্টা করো: এটা ingredientCost পড়ে, surcharge লেখে, আর labourHours নতুন করে লেখে। Tandoor block চেষ্টা করো: এটা labourHours পড়ে আর surcharge লেখে। তিনটা working variable প্রতিটা block জুড়ে আড়াআড়ি ছড়িয়ে। ঠিক এই কারণে এই method তিন বছর ধরে পরিষ্কার হতে পারেনি।
Quote-টাকে নিজের স্টেশন দাও:
// AFTER: FeastQuote is the station; its fields are the labelled shelves
class FeastQuote {
private ingredientCost = 0;
private labourHours = 0;
private surcharge = 0;
constructor(private readonly order: FeastOrder) {}
compute(): number {
this.costAllDishes();
this.applyFestivalSeason();
this.applyTandoorCharges();
return Math.round(this.ingredientCost + this.labourCost() + this.surcharge);
}
private costAllDishes(): void {
for (const dish of this.order.dishes) {
this.ingredientCost += dish.costPerPlate * this.order.guests;
this.labourHours += dish.isSpecial ? 3 : 1;
if (dish.isSpecial && this.order.guests > 100) {
this.labourHours += 2;
this.surcharge += 500;
}
}
}
private applyFestivalSeason(): void {
if (!this.order.isFestivalSeason) return;
this.surcharge += this.ingredientCost * 0.1;
this.labourHours = this.labourHours * 1.25;
}
private applyTandoorCharges(): void {
if (!this.order.needsTandoor) return;
this.surcharge += 1500;
if (this.labourHours > 40) this.surcharge += 800;
}
private labourCost(): number {
return this.labourHours * 200;
}
}
function quoteFeast(order: FeastOrder): number {
return new FeastQuote(order).compute();
}compute() জোরে পড়ো: সব dishes-এর খরচ, উৎসবের মৌসুম apply করো, tandoor charge apply করো, total করো। একজন নতুন teammate দশ সেকেন্ডে pricing policy বুঝে যাবে। প্রতিটা policy তার নামওয়ালা method-এ আছে, তাই boss বললে "festival surcharge এখন ১২ শতাংশ," পরিবর্তনটা applyFestivalSeason()-এর ভেতরে একটা লাইন।
আর লক্ষ্য করো ক্যাটারিং চাচা যে bonus-এর কথা বলেননি: এখন quote একটা object হওয়ায় নতুন ক্ষমতা প্রায় বিনামূল্যে আসে। Ingredient cost, labour, আর surcharge আলাদাভাবে দেখানো detailed bill চাও? তিনটা getter method যোগ করো — মানগুলো তাকেই বসে আছে। Festival আর non-festival quote তুলনা করতে চাও? দুটো FeastQuote object তৈরি করো। রাতভর একশো quote queue করতে চাও? Object list-এ অপেক্ষা করতে পারে, কিন্তু অর্ধেক-শেষ local variable পারে না।
C#-এ একই refactoring
C# team-গুলো pricing, payroll, আর report generation কোডে এই pattern ক্রমাগত দেখে। এখানে refactoring-এর shape, সংক্ষিপ্তভাবে:
// BEFORE: tangled locals inside Payroll
public class Payroll
{
public decimal MonthlySalary(Employee emp, int daysWorked, int overtimeHours)
{
decimal basePay = emp.DailyRate * daysWorked;
decimal overtimePay = 0m;
decimal deduction = 0m;
if (overtimeHours > 0)
{
overtimePay = overtimeHours * emp.DailyRate / 8m * 1.5m;
if (basePay > 50000m) overtimePay *= 0.8m; // reads basePay!
}
if (daysWorked < 20)
{
deduction = (20 - daysWorked) * emp.DailyRate * 0.5m;
if (overtimePay > 0m) deduction *= 0.75m; // reads overtimePay!
}
return basePay + overtimePay - deduction;
}
}// AFTER: the computation gets its own class
public class SalaryCalculation
{
private readonly Employee _emp;
private readonly int _daysWorked;
private readonly int _overtimeHours;
private decimal _basePay;
private decimal _overtimePay;
private decimal _deduction;
public SalaryCalculation(Employee emp, int daysWorked, int overtimeHours)
{
_emp = emp;
_daysWorked = daysWorked;
_overtimeHours = overtimeHours;
}
public decimal Compute()
{
_basePay = _emp.DailyRate * _daysWorked;
ApplyOvertime();
ApplyAttendanceDeduction();
return _basePay + _overtimePay - _deduction;
}
private void ApplyOvertime()
{
if (_overtimeHours <= 0) return;
_overtimePay = _overtimeHours * _emp.DailyRate / 8m * 1.5m;
if (_basePay > 50000m) _overtimePay *= 0.8m;
}
private void ApplyAttendanceDeduction()
{
if (_daysWorked >= 20) return;
_deduction = (20 - _daysWorked) * _emp.DailyRate * 0.5m;
if (_overtimePay > 0m) _deduction *= 0.75m;
}
}
public class Payroll
{
public decimal MonthlySalary(Employee emp, int daysWorked, int overtimeHours)
=> new SalaryCalculation(emp, daysWorked, overtimeHours).Compute();
}C#-বিশেষ কয়েকটা কথা। Constructor-এ সেট করা field readonly mark করো — compiler নিশ্চিত করবে উপকরণ পৌঁছানোর পরে আর বদলায় না। Working field private রাখো। আর আসল method যদি Payroll-এর অন্য member ব্যবহার করে, তাহলে Payroll instance constructor-এ পাঠাও। অনেক C# team এই class-গুলোকে role অনুযায়ী নাম দেয় — SalaryCalculation, InvoiceBuilder, RouteOptimizer — যেটা MonthlySalaryMethodObject-এর চেয়ে অনেক স্বাভাবিক পড়ায়।
একই ধারণা Python-এ, সবচেয়ে সংক্ষিপ্ত রূপে। Python-এ access keyword না থাকায় underscore convention private-এর কাজ করে:
# AFTER, Python flavour: a tiny method object
class SalaryCalculation:
def __init__(self, daily_rate, days_worked, overtime_hours):
self._daily_rate = daily_rate
self._days_worked = days_worked
self._overtime_hours = overtime_hours
self._base = 0.0
self._overtime = 0.0
self._deduction = 0.0
def compute(self):
self._base = self._daily_rate * self._days_worked
self._apply_overtime()
self._apply_attendance_deduction()
return self._base + self._overtime - self._deduction
def _apply_overtime(self):
if self._overtime_hours <= 0:
return
self._overtime = self._overtime_hours * self._daily_rate / 8 * 1.5
if self._base > 50000:
self._overtime *= 0.8
def _apply_attendance_deduction(self):
if self._days_worked >= 20:
return
self._deduction = (20 - self._days_worked) * self._daily_rate * 0.5
if self._overtime > 0:
self._deduction *= 0.75IDE সাপোর্ট
এটা সেই বিরল refactoring-গুলোর একটা যেখানে একটা IDE family সত্যিকারের one-click version দেয়:
- IntelliJ IDEA (Java): এর একটা dedicated refactoring আছে যার নামই Extract Method Object (Refactor → Extract → Method Object...). তুমি code select করো, আর IDE class তৈরি করে, local-গুলো field-এ রূপান্তর করে, constructor বানায়, আর আসল method-কে delegator হিসেবে লেখে — আমাদের নিরাপদ ক্রমের ১ থেকে ৫ ধাপ এক action-এ। এমনকি "inner class" option দেয় যদি স্টেশনটা প্রথমে আসল class-এর ভেতরে রাখতে চাও।
- JetBrains Rider / ReSharper (C#): single-click method-object action নেই, কিন্তু combination ভালো কাজ করে: class manually তৈরির পরে Extract Method, grouped member সরাতে Extract Class, আর local একটা একটা করে promote করতে Introduce Field। Rider-এর Introduce Parameter Object ছোট cousin — তখন কাজে লাগে যখন শুধু parameter list সমস্যা, local নয়।
- Visual Studio (C#): Extract Method (Ctrl+R, M) আর Introduce Field quick action-গুলো mechanical piece cover করে। Class তৈরি আর constructor wiring manual কিন্তু দ্রুত। Roslyn-based extension আরো যোগ করে।
- VS Code (TypeScript/JavaScript): Extract to method/function আর Extract to constant refactoring ধাপ ৬-এ সাহায্য করে। ধাপ ১ থেকে ৫-এ class setup manual। TypeScript compiler এখানে বন্ধু — প্রতিটা মিস হওয়া
this.লাল squiggle হয়ে undeclared variable বলে জানায়।
One-click support থাকলেও, একই checkpoint-এ test চালাও। IDE চমৎকার কিন্তু জাদুকর না — বিশেষত যখন method তার host class-এর field স্পর্শ করে বা sneaky early return আছে।
সুবিধা এবং ঝুঁকি
| সুবিধা | ঝুঁকি ও সীমাবদ্ধতা |
|---|---|
| জট পাকানো local field হয়ে যায়, তাই Extract Method অবশেষে কাজ করে — long method ছোট নামওয়ালা ধাপে কাটা যায় | একটা computation-এর জন্য একটা পুরো নতুন class: বেশি file, বেশি indirection, বেশি surface area |
Algorithm স্বাধীনভাবে testable হয় — তৈরি করা input দিয়ে object বানাও আর compute() call করো | Local থেকে mutable field-এ উন্নীত হওয়া মানে object-এর ভেতরে shared mutable state; helper method ভুল ক্রমে চললে bug আসে |
compute() policy ধাপের readable checklist হয়; প্রতিটা policy পরিবর্তন একটা ছোট method স্পর্শ করে | Lighter refactoring যদি local-এর জট খুলে দিতে পারে, তাহলে এটা overkill — সবসময় আগে ওগুলো চেষ্টা করো |
| বিনামূল্যের bonus: intermediate value expose করা যায়, object undo, logging, queueing, বা async ক্ষমতা পেতে পারে | "Method object" নামটা সবখানে apply করার লোভ দেয়; Fowler নিজে বেশিরভাগ সময় সাধারণ function পছন্দ করেন |
| প্রায়ই লুকানো design প্রকাশ করে — অনেক method object পরিপক্ব হয়ে Strategy বা Command হয় | Host class-এর member দরকার হলে host-কে পাঠাতে হবে, যা নতুন class-কে তার সাথে couple করে |
সহজ কথায়: এই refactoring একটা scope সমস্যাকে একটা structure খরচের বিনিময়ে বদলায়। সেই খরচ তখনই দাও যখন scope সমস্যা সত্যিকারের — যখন extraction সত্যিই ব্যর্থ হয়েছে জট পাকানো local-এর কারণে।
কোন smell এটা সারায়?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Long Method | এটা সবচেয়ে খারাপ long method-গুলোর ভারী অস্ত্রের সমাধান — যেগুলোর আন্তঃবোনা local সাধারণ Extract Method-কে হারিয়ে দেয়। Method-কে class-এ নিয়ে যাও, তারপর মুক্তভাবে ভাগ করো |
| Large Class | Order বা Account থেকে একশো লাইনের computation তার নিজের class-এ সরালে host ছোট হয় আর তার আসল দায়িত্বে ফিরে আসে |
| Long Parameter List | Method object-এর ভেতরে helper ধাপগুলো parameter পাঠানোর বদলে field-এর মাধ্যমে state share করে। বাইরে থেকে caller সব একবারে পাঠায়, একটা constructor-এর মাধ্যমে |
| Duplicate Code | Algorithm একবার নামওয়ালা, testable class-এ গেলে, একই computation-এর ছড়িয়ে পড়া কাছাকাছি-copy গুলো মুছে সেই একটা স্টেশনে পাঠানো যায় |
দ্রুত revision box
+--------------------------------------------------------------------+
| REPLACE METHOD WITH METHOD OBJECT — REVISION CARD |
+--------------------------------------------------------------------+
| Story : The wedding biryani gets its OWN STATION with |
| labelled shelves — fields every step can reach. |
| Signal : Extract Method keeps failing — every fragment |
| needs 3+ locals in and 2+ values out. |
| Recipe : new class -> params + locals become fields -> |
| body moves into compute() -> old method delegates |
| -> NOW extract small steps freely. |
| Checkpoint: full test suite green right after delegation, |
| BEFORE any decomposition. Move, verify, then carve. |
| Naming : Fowler 2nd ed. calls it "Replace Function with |
| Command" — same mechanics, command-object framing. |
| Caution : heavy artillery. Try Split Temporary Variable and |
| Replace Temp with Query first. Keep fields private. |
| Bonus : the new class often matures into Strategy / Command. |
+--------------------------------------------------------------------+অনুশীলন
নিজের স্টেশন বানাও। নিচে একটা জট পাকানো TypeScript function আছে একটা school transport app থেকে — মাসিক bus fee হিসাব করে। এর তিনটা working variable প্রতিটা block জুড়ে আড়াআড়ি, তাই সাধারণ extraction ব্যর্থ। অনুশীলনের জন্য নিখুঁত উপকরণ:
function busFee(distanceKm: number, daysPerWeek: number, isACBus: boolean, siblingCount: number): number {
let fee = 0;
let discount = 0;
let fuelExtra = 0;
fee = distanceKm * 30;
if (distanceKm > 10) {
fuelExtra = (distanceKm - 10) * 12;
if (fee > 600) fuelExtra = fuelExtra * 0.8; // reads fee, writes fuelExtra
}
fee = fee * (daysPerWeek / 5);
if (isACBus) {
fee = fee * 1.4;
fuelExtra = fuelExtra * 1.2;
}
if (siblingCount > 0) {
discount = fee * 0.1 * Math.min(siblingCount, 2);
if (fuelExtra > 200) discount += 50; // reads fuelExtra, writes discount
}
return Math.round(fee + fuelExtra - discount);
}তোমার কাজ:
- জট প্রমাণ করো। IDE-র Extract Method দিয়ে AC-bus block আর sibling block extract করার চেষ্টা করো। প্রতিটা চেষ্টার জন্য লিখে রাখো IDE কতটা parameter আর return value চাইল। এটাই সেই ব্যর্থতা যা ভারী অস্ত্রকে ন্যায্যতা দেয়।
- Characterization test লেখো। কমপক্ষে পাঁচটা: কম দূরত্ব, AC সহ বেশি দূরত্ব, তিনজন ভাইবোন, সপ্তাহে দুইদিন, আর একটা সব-মিলিয়ে case। বর্তমান output expected value হিসেবে রেকর্ড করো। কিছু সরানোর আগে সবুজ।
- নিরাপদ ক্রমে refactoring করো। একটা
BusFeeCalculationclass তৈরি করো।distanceKm,daysPerWeek,isACBus,siblingCountreadonly constructor field করো।fee,discount,fuelExtraprivate working field করো। Bodycompute()-এ নিয়ে যাও।busFee-কে one-line delegator করো। থামো আর সব test চালাও। - ভাগ করো।
baseFee(),applyFuelExtra(),applyACPremium(),applySiblingDiscount()extract করো — বা আরো ভালো নাম খুঁজে পেলে সেটা। প্রতিটা extraction-এর পরে test চালাও। লক্ষ্য করো কীভাবে প্রতিটা extraction এখন শূন্য parameter নেয়। - Temporal coupling খোঁজো। ইচ্ছাকৃতভাবে
compute()-এর ভেতরে দুটো call swap করো — যেমন AC premium-এর আগে sibling discount apply করো — আর দেখো কোন test ব্যর্থ হয়। এক বাক্যে লেখো কেন ব্যর্থ হলো। তারপর ক্রম ঠিক করো। এটাই কলেজ কর্নারের পাঠ সশরীরে: field-গুলো তোমাকে স্বাধীনতা দিয়েছে, আর স্বাধীনতার মূল্য হলো ক্রমের দায়িত্ব নেওয়া। - ভাবো। তোমার চূড়ান্ত
compute()-এর দিকে তাকাও। একজন নতুন teammate কি শুধু চারটা লাইন পড়ে school-এর bus pricing policy বুঝে যেতে পারবে? হ্যাঁ হলে, স্টেশন তৈরি হয়েছে, তাক লেবেল পেয়েছে, আর সালমা খালা নিজে একটা বিরিয়ানির প্লেট ধরিয়ে দিতেন।
সচরাচর জিজ্ঞাসা
- কখন Extract Method-এর বদলে Replace Method with Method Object ব্যবহার করব?
- আগে Extract Method চেষ্টা করো। Method object তখনই দরকার যখন extraction বারবার ব্যর্থ হচ্ছে — কারণ প্রতিটা টুকরো একই জট পাকানো local variable পড়ছে আর লিখছে, তাই প্রতিটা extracted piece-এর জন্য বিশাল parameter list দরকার। Method object সেই local-গুলোকে field বানিয়ে দেয়, তারপর extraction সহজ হয়।
- Fowler কেন এর নাম Replace Function with Command রাখলেন?
- Refactoring বইয়ের দ্বিতীয় সংস্করণে Martin Fowler নাম বদলান। কারণ এই refactoring-এ যে object তৈরি হয় — একটা কাজকে wrap করে, configure করা যায়, তারপর চালানো যায় — সেটাকেই তিনি command object বলেন। Command design pattern-এর মতো। Mechanics একই, শুধু নামটা বদলেছে।
- একটা method-এর জন্য পুরো class বানানো কি বেশি বাড়াবাড়ি না?
- হ্যাঁ, এটা একটা class, একটা constructor আর কিছু field যোগ করে — এটাই এর আসল খরচ। Fowler নিজেই বলেন বেশিরভাগ সময় তিনি সাধারণ function পছন্দ করেন। Method object তখনই দরকার যখন method এতটাই জটিল যে অন্য উপায়ে decompose করাই যাচ্ছে না, অথবা undo বা step-by-step execution-এর মতো বাড়তি ক্ষমতা দরকার।
- Method object-এর field কি private রাখা উচিত?
- হ্যাঁ। Field-গুলো শুধু compute method আর তার private helper-দের জন্য — এরা parameter পাস না করেও state share করে। Class-এর বাইরে থেকে কেউ এগুলো পড়বে না বা লিখবে না। Public surface ছোট রাখো: একটা constructor আর compute() বা run()-এর মতো একটা method।
- Method object আর Command design pattern কি একই জিনিস?
- এরা কাছের আত্মীয়। Method object একটা computation-কে তার working state সহ wrap করে। Command pattern একটা request-কে object বানায় যাতে সেটা queue, log বা undo করা যায়। Method object প্রায়ই পরে আসল Command বা Strategy-তে পরিণত হয় — এটা এই refactoring-এর বোনাস।
আরো দেখো
সম্পর্কিত পাঠ
Extract Method: একটা বিশাল ফাংশনকে ছোট ছোট নামওয়ালা helper-এ ভাগ করো
Extract Method ধাপে ধাপে শিখে নাও। একটা লম্বা ফাংশন থেকে এলোমেলো block বের করে তাকে একটা পরিষ্কার নাম দাও, আর তোমার কোডকে একটা সহজ to-do লিস্টের মতো পড়ার যোগ্য করে তোলো।
Split Temporary Variable: একটা বালতি দুই কাজ করতে পারে না
দুই বালতির গল্প দিয়ে Split Temporary Variable শেখো — TypeScript ও C# উদাহরণ আর নিরাপদ ধাপ সহ। প্রতিটা variable-কে একটাই কাজ আর একটাই সৎ নাম দাও।
Replace Temp with Query: তাজা জিজ্ঞেস করো, বাসি চিরকুটে ভরসা করো না
ক্যান্টিনের সিঙ্গারার গল্প দিয়ে Replace Temp with Query বোঝো — TypeScript আর C# উদাহরণ, নিরাপদ ধাপ, আর একটাই সত্যের উৎস।
Substitute Algorithm: স্কুলে যাওয়ার নতুন সোজা রাস্তা
Substitute Algorithm রিফ্যাক্টরিং শেখো সাইকেলের রুটের গল্প দিয়ে — TypeScript আর Python উদাহরণ সহ, আর টেস্ট-ফার্স্ট নিরাপত্তার নিয়ম যেটা সব শিক্ষার্থীর জানা দরকার।