Pull Up Method: পুরো স্কুলের জন্য একটাই নির্দেশিকা
Pull Up Method refactoring শেখো স্কুলের ছুটির আবেদনের গল্পের মাধ্যমে — subclass-এ duplicate হয়ে যাওয়া method-গুলো superclass-এ তুলে আনো, TypeScript আর C#-এ safe steps সহ, IDE dialog আর কখন Form Template Method বেছে নেবে সেটাসহ।
তিনটা ছুটির আবেদনের নিয়ম, তিনরকম নির্দেশিকা
ধরো Sunrise Public School-এ আবার ফিরে এসেছ। Pull Up Field গল্পে Headmistress Dr. Meena Kulkarni কী নিয়ম করেছিলেন মনে আছে? Shared তথ্য main office board-এ যাবে; section board-এ শুধু নিজের জিনিস থাকবে। Address-এর সমস্যা সমাধান হয়েছিল, আর head clerk Mr. Sharma অবশেষে শুধু একটা board maintain করছিলেন। কিন্তু নতুন একটা সমস্যা অপেক্ষা করছিল — এবার সমস্যাটা তথ্য নিয়ে না, নির্দেশিকা নিয়ে।
প্রতিটা section-এর notice board-এ হাতে লেখা একটা কাগজ ছিল — "ছুটির আবেদন কীভাবে করবে।" Class teacher Mrs. Iyer-এর 7A-র কাগজে লেখা: application লিখবে, parents-এর signature নেবে, এক দিন আগে class teacher-এর কাছে জমা দেবে। 7B-র কাগজেও প্রায় একই কথা — কিন্তু কোনো এক সময়ে Mr. Bose-এর section-এর আগের teacher লিখে গেছেন "দুই দিন আগে"। Ms. Fernandes-এর sports section 7C-র কাগজ 7A-র মতোই ছিল — শুধু parents-এর signature-এর কথাটাই বাদ পড়ে গিয়েছিল।
তিনটা আলাদা নিয়ম কেউ পরিকল্পনা করে বানায়নি। বছর আগে একজন monitor প্রথম কাগজটা লিখেছিল, পরের section copy করেছিল, তৃতীয় section সেই copy-র copy করেছিল — আর তারপর ছোট ছোট সম্পাদনা আলাদাভাবে ঢুকে গেছে প্রতিটায়। কাগজগুলো drift করেছে, ঠিক যেমন address করেছিল। তথ্য drift করে; নির্দেশিকা আরও খারাপভাবে drift করে, কারণ নির্দেশিকা "উন্নত" হয় সহায়ক মানুষদের হাতে, একটা board এক সময়ে।
সমস্যাটা সামনে এলো sports day-র সপ্তাহে। 7C-র একটা student parents-এর signature ছাড়াই ছুটি নিল — তার কাগজে তো চাওয়াই হয়নি। 7B-র একটা student এক দিন আগে application জমা দিল, যেমন তার 7A-র দাদা দিত — আর absent mark পেল, কারণ তার কাগজে দুই দিন আগের কথা ছিল। একই স্কুল, সবার মাথায় একই নিয়ম, কিন্তু তিনটা আলাদা behavior। Mr. Sharma-এর office সারা সপ্তাহ কাটাল কোন absent সত্যিকারের তা বের করতে।
Dr. Kulkarni একটু ভেবে obvious কাজটাই করলেন: একটাই "ছুটির আবেদন কীভাবে করবে" কাগজ লিখলেন, main office board-এ লাগালেন, আর তিনটা section board-এর কাগজের জায়গায় লিখলেন: "ছুটির নিয়ম: main office board দেখো।" একটা কাগজ। একটা নিয়ম। নিয়ম বদলালে এক জায়গায় update — আর Mr. Sharma-ই শুধু সেটার দায়িত্বে।
Code-এ নির্দেশিকার কাগজ হলো method — একটা procedure যা বলে কীভাবে কিছু করতে হয়। যখন একই method body অনেক subclass-এ বসে থাকে, আমরা সেটা উপরে superclass-এ নিয়ে যাই, যাতে একবার লেখা হয় আর সবাই inherit করে। এটাই Pull Up Method, আর এটা "generalization নিয়ে কাজ করা" পুরো family-র সবচেয়ে গুরুত্বপূর্ণ refactoring।
এই নিয়ম ঠিক রাখতে একজন teacher-এর এক সপ্তাহ কেমন যেত, fix-এর আগে আর পরে:
Pull Up Method আসলে কী?
Pull Up Method Martin Fowler-এর Refactoring catalog-এ আছে একটা সুন্দর ছোট instruction সহ:
যখন subclass-গুলোতে একই body-র method থাকে — বা যাদের body সৎভাবে এক করা যায় — সেই method-টা common superclass-এ নিয়ে যাও আর প্রতিটা subclass থেকে মুছে দাও।
Pull Up Field থেকে পার্থক্যটা লক্ষ করো। সেটা তথ্য সরিয়েছিল (address)। এটা সরায় behavior (নিয়ম)। আসল refactoring কাজে এরা একসাথে চলে: field আগে উপরে যায়, কারণ যে data method ব্যবহার করে সেটা child-এ আটকে থাকলে method parent-এ যেতে পারে না। Field হলো enabler; method হলো prize।
Duplicate behavior কেন হয়? ঠিক যেভাবে duplicate কাগজ হয়েছিল। একজন developer এক subclass-এ applyForLeave() লেখে। একজন teammate, sibling subclass বানাতে গিয়ে, copy করে নেয় — copy-paste যেকোনো editor-এর সবচেয়ে দ্রুত keystroke। অথবা দুজন developer, আলাদা মাসে, একই সমস্যা একইভাবে স্বাধীনভাবে solve করে, কেউ জানে না অন্যটার কথা। যেভাবেই হোক, superclass behavior সম্পর্কে চুপ থাকে যেটা প্রতিটা child perform করে, আর তুমি এখন hierarchy edition-এর classic Duplicate Code smell-এর মালিক।
আর duplicate behavior, duplicate data-র চেয়ে বেশি বিপজ্জনক, কারণ behavior নিয়ম বহন করে। একটা copy-তে bug fix করো, siblings-গুলো ভাঙা থাকে। একটায় feature যোগ করো, বাকিগুলো চুপে চুপে পিছিয়ে পড়ে। আমাদের 7B-র "দুই দিন আগে" edit-টা ঠিক এমনই: কেউ একটা copy উন্নত করেছে, বাকি দুটো জানেওনি। Codebase-এ, এটাই সেই bug report যেখানে লেখা থাকে "checking account-এ কাজ করছে কিন্তু savings account-এ না" — একই intended নিয়ম, drift করা copy।
এক লাইনে: Pull Up Method একটা method যা অনেক subclass-এ duplicate তা superclass-এ নিয়ে যায়, যাতে behavior একবার লেখা হয় আর সবাই inherit করে — ঠিক যেমন তিনটা drift করা section কাগজের বদলে office board-এ একটা school-wide কাগজ লাগানো।
Pull up করা method hierarchy-টাকেও document করে। যখন applyForLeave() Section-এ থাকে, parent class খোলাখুলি বলে: "এই স্কুলের প্রতিটা section এটা করতে পারে, আর এইভাবেই করে।" একজন reader-কে আর তিনটা subclass file খুলে line by line compare করতে হয় না shared contract বের করতে। Code-এর আকৃতি অবশেষে সত্যের আকৃতির সাথে মিলে যায়।
College corner: Pull Up Method হলো behavior-এ DRY apply করা, কিন্তু field-এর চেয়ে কড়া মানদণ্ড সহ। একটা field-এর একটাই কাজ — value ধরা — তাই "একই meaning" যাচাই করা সহজ। কিন্তু method logic encode করে, আর logic-এ edge case আছে: একটা >= বনাম >, একটা exception type যা caller ধরে, side effect-এর order। দুটো method body তখনই "একই knowledge" যখন এগুলোতে সব কিছুতে একমত হয়। তাই নিচের safe recipe-তে side-by-side reading আর characterization test-এর কথা বলা হয়েছে merge-এর আগে — DRY এক সত্যের অপ্রয়োজনীয় representation সরায়, কখনই দুটো আলাদা সত্য যারা মিলে মনে হয় সেটা না।
সব move-এর landscape একবার দেখো, এই post-এর hero pull-up branch-এর উপরে bold position-এ:
কখন এটা দরকার?
এই signal-গুলো দেখলে বুঝবে:
- দুই বা তার বেশি subclass-এ একই body-র method আছে। একই statement, একই logic, হয়তো whitespace-এর পার্থক্য। এটা textbook Duplicate Code — Fowler duplication-কে এক নম্বর smell বলেন কারণ আছে।
- Body-গুলো প্রায় একরকম। এটা বেশি common, আর বেশি interesting case। Copy-গুলো সাবধানে compare করো। অনেক সময় পার্থক্যগুলো ঘটনাক্রমে — একটা ভুলে যাওয়া update, typo, পুরনো constant (আমাদের 7C-র signature line বাদ পড়া)। একসাথে করলে একটা real bug fix হয়ে যায়। কিন্তু পার্থক্য যদি intentional হয়, জোর করো না; সেই ক্ষেত্রে সঠিক tool নিচে দেখাচ্ছি।
- একটা bug fix অনেক class-এ করতে হয়েছিল। শেষ commit message যদি হয় "fix validation in CheckingAccount AND SavingsAccount", method-টা উপরে যেতে চাইছে।
- একই method name sibling-দের মধ্যে একই উদ্দেশ্যে আছে। Body compare করার আগেই, sibling-দের মধ্যে identical name দেখলে investigate করার কারণ আছে।
আর যেগুলো দেখলে থামবে:
- শুধু কিছু subclass method-টা share করে। 7C-র একার behavior যদি হয়, সেটা
Section-এ তুললে 7A আর 7B অপ্রাসঙ্গিক কিছু inherit করতে বাধ্য হবে — এটা Refused Bequest তৈরি হওয়া real time-এ। Inverse refactoring, Push Down Method, আছে একটা বেশি general parent method-কে ঠিক জায়গায় নামিয়ে দিতে, ঠিক যেমন Push Down Field data-র ক্ষেত্রে করে। - Body-গুলো intentionally, meaningfully আলাদা। Merge করলে চুপে চুপে behavior বদলে যাবে — আর refactoring কখনই behavior বদলাতে পারবে না। Copy-গুলো যদি skeleton share করে কিন্তু এক-দুটো step-এ আলাদা, তাহলে সঠিক move হলো Form Template Method: skeleton pull up করো, differing step-গুলো abstract method হিসেবে রাখো।
- Method subclass-এর নিজস্ব member-এর উপর depend করে। এটা parent-এ compile হবে না। আগে সে data-তে Pull Up Field apply করো, অথবা parent-এ abstract method declare করো যে অংশগুলো parent জানে না তার জন্য।
আমাদের applyForLeave() method-কে placement map-এ দেখলে বোঝা যায় কেন এটা textbook pull-up candidate:
আগে আর পরে এক নজরে
দেখো code-এ ছুটির নিয়ম কেমন। প্রতিটা section class নিজের copy বহন করছে, আর copy-গুলো কাগজের মতোই drift করেছে:
// BEFORE: three copies of "the same" procedure, already drifting
abstract class Section {
protected leaveRegister: string[] = [];
}
class SectionSevenA extends Section {
applyForLeave(student: string, days: number): void {
if (days < 1) throw new Error("Leave must be at least 1 day");
this.leaveRegister.push(`${student}: ${days} day(s), parent signed`);
}
}
class SectionSevenB extends Section {
applyForLeave(student: string, days: number): void {
if (days < 1) throw new Error("Leave must be at least 1 day");
// someone added an extra rule here, only here:
if (days > 10) throw new Error("Long leave needs principal approval");
this.leaveRegister.push(`${student}: ${days} day(s), parent signed`);
}
}
class SectionSevenC extends Section {
applyForLeave(student: string, days: number): void {
if (days < 1) throw new Error("Leave must be at least 1 day");
this.leaveRegister.push(`${student}: ${days} day(s)`); // forgot the signature!
}
}Drift-টা লক্ষ করো। 7B একটা sensible নিয়ম পেয়েছে (long leave-এ approval দরকার) যেটা বাকিরা কখনো পায়নি। 7C signature-এর কথা হারিয়ে ফেলেছে। কোনো পার্থক্যই সিদ্ধান্ত ছিল না — দুটোই copy-paste maintenance-এর দুর্ঘটনা। স্কুলের আসল নিয়ম হলো একটা procedure, সবার জন্য long-leave check সহ।
Pull Up Method-এর পরে:
// AFTER: one procedure, written once, inherited by all sections
abstract class Section {
protected leaveRegister: string[] = [];
applyForLeave(student: string, days: number): void {
if (days < 1) throw new Error("Leave must be at least 1 day");
if (days > 10) throw new Error("Long leave needs principal approval");
this.leaveRegister.push(`${student}: ${days} day(s), parent signed`);
}
}
class SectionSevenA extends Section {} // inherits applyForLeave
class SectionSevenB extends Section {} // inherits applyForLeave
class SectionSevenC extends Section {} // inherits applyForLeaveতিনটা drift করা copy হয়ে গেছে একটা authoritative procedure। 7B-র ভালো ধারণা এখন সব section-কে protect করছে; 7C-র missing signature ফিরে এসেছে। পরবর্তী নিয়ম পরিবর্তন হবে একটাই edit।
Move-এর আগে duplicate procedure copy গোনা fields post-এর মতোই গল্প বলে — সমস্যাটা সমানভাবে ছড়িয়ে আছে, তাই কোনো একটা section "দায়িত্বে" মনে করে না, আর এইজন্যই কেউ fix করে না:
ধাপে ধাপে, নিরাপদভাবে
Pull Up Method-এ Pull Up Field-এর চেয়ে একটু বেশি যত্ন দরকার, কারণ behavior-এ সূক্ষ্ম পার্থক্য লুকিয়ে থাকতে পারে যেটা data-তে থাকে না। আগে bird's-eye view:
এবার বিস্তারিত recipe।
-
প্রতিটা copy line by line, পাশাপাশি রেখে পড়ো। skim করো না। Split view-এ copy-গুলো খুলে compare করো। প্রতিটা পার্থক্যের জন্য একটা verdict দরকার: ঘটনাক্রমে (drift, bug, পুরনো code) নাকি intentional (সত্যিকারের per-subclass নিয়ম)। আমাদের example-এ, 7B-র long-leave check ছিল school-wide নিয়ম যেটা spread হয়নি, আর 7C-র missing signature ছিল bug — দুটোই ঘটনাক্রমে, তাই unification safe আর এমনকি desirable।
-
Method-এর dependency check করো। Body কোন field আর method ব্যবহার করছে? সেটা যা-কিছু use করে সব superclass থেকে visible হতে হবে।
applyForLeaveleaveRegisterব্যবহার করে, যেটা ইতোমধ্যেSection-এ আছে — ভালো। যদি subclass-only field ব্যবহার করত, তাহলে এখানে থামতে হতো আর আগে Pull Up Field apply করতে হতো; subclass-only method call করলে, সেই method-টা parent-এabstractdeclare করতে হতো। -
আগে copy-গুলো subclass-এর ভেতরেই textually identical করো। সব copy-তে agreed unification apply করো, প্রতিটা class-এর পরে test run করো। এটা intermediate state:
// INTERMEDIATE: all three copies now identical, still in subclasses class SectionSevenC extends Section { applyForLeave(student: string, days: number): void { if (days < 1) throw new Error("Leave must be at least 1 day"); if (days > 10) throw new Error("Long leave needs principal approval"); this.leaveRegister.push(`${student}: ${days} day(s), parent signed`); } }এই step কেন? কারণ দুটো risky কাজ — behavior বদলানো আর behavior সরানো — আলাদা commit-এ রাখা হচ্ছে। এখন test break হলে জানবে unification ভুল ছিল, move না।
-
Superclass-এ method copy করো। একটা subclass-এর (এখন-identical) body নিয়ে
Section-এ method তৈরি করো। Subclass-গুলো এখনও নিজের copy রাখছে; শুধু parent-কে identical text দিয়ে override করছে। Compile করো আর test করো — সব green হওয়া উচিত। -
একটা subclass থেকে method মুছো। Compile করো। Test করো। সেই subclass এখন parent-এর version inherit করছে। Behavior অপরিবর্তিত, তাই test green থাকে।
-
প্রতিটা বাকি subclass-এর জন্য একটা একটা করে repeat করো। 7B থেকে মুছো, test করো। 7C থেকে মুছো, test করো। একটা বড় লাফের বদলে তিনটা ছোট, reversible step।
-
কোনো copy unify হতে না চাইলে strategy বদলাও — জোর করো না। একটা জেদী intentional পার্থক্য মানে তুমি Form Template Method চাইছ: shared skeleton parent-এ রাখো আর differing step-টা abstract method হিসেবে express করো যেটা প্রতিটা subclass নিজের মতো implement করে।
এই recipe-তে method-এর জীবন একটা ছোট, strict state machine — লক্ষ করো unified বসে আছে duplicated আর hoisted-এর মাঝে, আর test দুটো transition-ই guard করছে:
Test এখানে তোমার safety rope — Pull Up Field-এর চেয়েও বেশি — কারণ method body-তে এমন পার্থক্য থাকতে পারে চোখ ধরে না — একটা >= বনাম >, reordered condition, একটা আলাদা error message যার উপর caller depend করছে। প্রতিটা unification edit-এর পরে আর প্রতিটা deletion-এর পরে suite run করো। যদি subclass method-এ একদম test না থাকে, কিছু touch করার আগে কিছু characterization test লেখো — আজকের behavior আগে capture করো, তারপর সেই safety net-এর বিরুদ্ধে refactor করো।
Move-এর পরে, runtime picture pleasantly boring। যেকোনো section object-এ call করলে base-এ একটাই inherited implementation পাওয়া যায় — বেছে নেওয়ার মতো কোনো copy আর নেই:
একটা বড় real-life example
Section শুধু leave process করে না; Mr. Sharma-এর office-এর জন্য monthly report-ও তৈরি করে। Refactoring-এর আগে প্রতিটা section class-এ নিজের report method আছে — আর এই example দেখায় interesting case, যেখানে body-র বেশিরভাগ share করা কিন্তু একটা line সত্যিই per section আলাদা:
// BEFORE: 90% shared report logic, copied into every section
abstract class Section {
protected schoolAddress = "12 Lake Road, Pune 411001"; // pulled up earlier!
protected studentCount = 0;
protected absencesThisMonth = 0;
}
class SectionSevenA extends Section {
monthlyReport(month: string): string {
const header = `Sunrise Public School — ${this.schoolAddress}`;
const attendance = 100 - (this.absencesThisMonth / this.studentCount) * 100;
const special = "Math Olympiad practice: Tuesdays"; // differs per section
return `${header}\nClass 7A — ${month}\nAttendance: ${attendance.toFixed(1)}%\n${special}`;
}
}
class SectionSevenB extends Section {
monthlyReport(month: string): string {
const header = `Sunrise Public School — ${this.schoolAddress}`;
const attendance = 100 - (this.absencesThisMonth / this.studentCount) * 100;
const special = "Science club: Wednesdays"; // differs per section
return `${header}\nClass 7B — ${month}\nAttendance: ${attendance.toFixed(1)}%\n${special}`;
}
}
class SectionSevenC extends Section {
monthlyReport(month: string): string {
const header = `Sunrise Public School — ${this.schoolAddress}`;
const attendance = 100 - (this.absencesThisMonth / this.studentCount) * 100;
const special = "Swimming practice: Mon/Thu"; // differs per section
return `${header}\nClass 7C — ${month}\nAttendance: ${attendance.toFixed(1)}%\n${special}`;
}
}এটা কি pull up করা যাবে? Body-গুলো identical না — section name আর special activity line আলাদা। কিন্তু shape-টা দেখো: skeleton একই, আর পার্থক্যগুলো ছোট, ভালোভাবে contained, আর intentional। তাই skeleton pull up করি আর পার্থক্যগুলো abstract method হিসেবে রাখি:
// AFTER: skeleton upstairs, genuine differences downstairs
abstract class Section {
protected schoolAddress = "12 Lake Road, Pune 411001";
protected studentCount = 0;
protected absencesThisMonth = 0;
monthlyReport(month: string): string {
const header = `Sunrise Public School — ${this.schoolAddress}`;
const attendance = 100 - (this.absencesThisMonth / this.studentCount) * 100;
return `${header}\n${this.sectionName()} — ${month}\n` +
`Attendance: ${attendance.toFixed(1)}%\n${this.specialActivity()}`;
}
protected abstract sectionName(): string;
protected abstract specialActivity(): string;
}
class SectionSevenA extends Section {
protected sectionName() { return "Class 7A"; }
protected specialActivity() { return "Math Olympiad practice: Tuesdays"; }
}
class SectionSevenB extends Section {
protected sectionName() { return "Class 7B"; }
protected specialActivity() { return "Science club: Wednesdays"; }
}
class SectionSevenC extends Section {
protected sectionName() { return "Class 7C"; }
protected specialActivity() { return "Swimming practice: Mon/Thu"; }
}এটা Pull Up Method থেকে Form Template Method-এ পরিণত হচ্ছে, আর স্কুলের analogy-তে এখনও perfectly কাজ করছে: office কাগজে এখন লেখা "প্রতিটা section এই format-এ monthly report জমা দেবে; ফাঁকা জায়গায় তোমার section-এর নাম আর special activity লেখো।" Procedure shared; ফাঁকা জায়গা local। লক্ষ করো এটা আগের post-এর উপর কীভাবে build করছে — এই report method সরার আগে schoolAddress pull up করতে হয়েছিল, এইজন্যই এই series-এ field আগে আসে, method পরে।
College corner: যখন pull up করা method overridable step call করে, Liskov Substitution Principle override-এর উপর contract হয়ে যায়। Section.monthlyReport()-এর skeleton caller-দের কাছে fixed format-এর report-এর promise করে; প্রতিটা subclass-এর specialActivity() সেই skeleton-এর implicit expectations honor করতে হবে (printable line return করবে, normal section-এর জন্য throw করবে না, register চুপে চুপে mutate করবে না)। একটা override যেটা precondition কঠোর করে বা postcondition দুর্বল করে সেটা substitutability break করে যদিও code compile হয়। তাই Template Method শুধু duplication tool না — এটা contract-splitting tool: parent invariant অংশের contract-এর মালিক, আর প্রতিটা child শুধু variation point-এর মালিক, যেগুলো এতটাই ছোট যে reason করা যায়।
Plain pull-up আর template form-এর মধ্যে কীভাবে decide করবে? এই ছোট decision path:
আর maintenance-এর হিসাব এক chart-এ। Refactoring-এর আগে leave নিয়ম বদলাতে গেলে তিনটা class edit করতে হতো আর আশা করতে হতো কেউ মিস করেনি; পরে শুধু একটা:
Python-এ একই ধারণা
Python-এ template-style result ছোট আর পড়তে সহজ — NotImplementedError (বা abc.abstractmethod) abstract blank-এর role play করে:
# AFTER, in Python: skeleton upstairs, blanks downstairs
class Section:
school_address = "12 Lake Road, Pune 411001"
def apply_for_leave(self, student: str, days: int) -> None:
if days < 1:
raise ValueError("Leave must be at least 1 day")
if days > 10:
raise ValueError("Long leave needs principal approval")
self.leave_register.append(f"{student}: {days} day(s), parent signed")
def monthly_report(self, month: str) -> str:
return (f"Sunrise Public School — {self.school_address}\n"
f"{self.section_name()} — {month}\n{self.special_activity()}")
def section_name(self) -> str:
raise NotImplementedError
def special_activity(self) -> str:
raise NotImplementedError
class SectionSevenC(Section):
def __init__(self):
self.leave_register: list[str] = []
def section_name(self) -> str:
return "Class 7C"
def special_activity(self) -> str:
return "Swimming practice: Mon/Thu"Python inheritance lookup-টা visible করে দেয়: seven_c.apply_for_leave(...) call করলে SectionSevenC-তে এই method পাওয়া যায় না, MRO (method resolution order) দিয়ে Section-এ উঠে যায়, আর একটাই shared body run করে। Class chain-এ উপরে উঠে যাওয়া মানে student নিচে নেমে office board পড়তে যাওয়া।
C#-এ একই refactoring
C# inheritance :-দিয়ে লেখে আর overridable step virtual/abstract/override-দিয়ে, কিন্তু move-গুলো একই। আগে:
// BEFORE: duplicated method in each section class
abstract class Section
{
protected List<string> _leaveRegister = new();
}
class SectionSevenA : Section
{
public void ApplyForLeave(string student, int days)
{
if (days < 1) throw new ArgumentException("Leave must be at least 1 day");
_leaveRegister.Add($"{student}: {days} day(s), parent signed");
}
}
class SectionSevenB : Section
{
public void ApplyForLeave(string student, int days)
{
if (days < 1) throw new ArgumentException("Leave must be at least 1 day");
if (days > 10) throw new ArgumentException("Long leave needs principal approval");
_leaveRegister.Add($"{student}: {days} day(s), parent signed");
}
}Unify করে pull up করার পরে — abstract step সহ template-style report সমেত:
// AFTER: shared behaviour in the base class, blanks left abstract
abstract class Section
{
protected List<string> _leaveRegister = new();
protected string _schoolAddress = "12 Lake Road, Pune 411001";
public void ApplyForLeave(string student, int days)
{
if (days < 1) throw new ArgumentException("Leave must be at least 1 day");
if (days > 10) throw new ArgumentException("Long leave needs principal approval");
_leaveRegister.Add($"{student}: {days} day(s), parent signed");
}
public string MonthlyReport(string month) =>
$"Sunrise Public School — {_schoolAddress}\n" +
$"{SectionName} — {month}\n{SpecialActivity}";
protected abstract string SectionName { get; }
protected abstract string SpecialActivity { get; }
}
class SectionSevenC : Section
{
protected override string SectionName => "Class 7C";
protected override string SpecialActivity => "Swimming practice: Mon/Thu";
}C#-এর কিছু note মনে রাখার মতো:
- Pull up করা method যে member ব্যবহার করে সেগুলো base class-এ কমপক্ষে
protectedহতে হবে — একই visibility নিয়ম যেমন Pull Up Field-এ। - Subclass যদি shared behavior extend করতে চায় (replace না করে), base method
virtualmark করো আর override-গুলো extra কাজ যোগ করার আগেbase.ApplyForLeave(...)call করুক। কিন্তু তার চেয়ে উপরের abstract-step style prefer করো — "base call করতে হবে নইলে চুপে চুপে ভাঙবে" একটা fragile contract। - Duplicate logic যদি ordinary method-এর বদলে constructor-এ থাকে, plain Pull Up Method সাহায্য করতে পারবে না, কারণ constructor inherit হয় না। সেই special case-এর জন্য আলাদা refactoring আছে — Pull Up Constructor Body, পরের post।
IDE support
এই refactoring এতটাই common যে সব serious IDE এটা automate করে:
- IntelliJ IDEA, Rider, PhpStorm (JetBrains family): Refactor → Pull Members Up… একটা dialog খোলে যেখানে class-এর member-গুলো checkbox সহ listed থাকে। Method-টা tick করো, target superclass choose করো, আর IDE সেটা move করে subclass copy মুছে দেয়। JetBrains-এর নিজের documentation ঠিক আমাদের situation-এর জন্য feature-টা pitch করে: subclass-গুলো similar কাজ করে, আর pull up "duplicate code থেকে মুক্তি পেতে সাহায্য করে।" Dialog method-টা abstract হিসেবেও pull up করতে পারে — template-style result-এর জন্য handy।
- Visual Studio (C#): method-এ cursor রাখো, Ctrl+. press করো, Pull members up to base type choose করো, আর dialog-এ destination pick করো। যদি base class না থাকে, "Pull member(s) up to new base class" Extract Base Class dialog-এর মাধ্যমে একটা তৈরি করে।
- ReSharper: Refactor → Pull Members Up, conflict analysis সহ যেটা warn করে যখন method এমন member depend করে যেটা base type দেখতে পাচ্ছে না — tool literally আমাদের step 2 তোমার জন্য check করে।
- Eclipse (Java): Refactor → Pull Up…, যেটা sibling copy-গুলো একই pass-এ deletion-এর জন্য flag করতে পারে।
আগের post-এর সৎ সতর্কতা এখানে দ্বিগুণ প্রযোজ্য: IDE text move করে; behavior বিচার করে না। এটা notice করবে না যে 7B-র copy-তে একটা extra নিয়ম ছিল যেটা বাকিরা জানত না। Step 1-এর side-by-side reading, আর "দুর্ঘটনা নাকি intention?" সিদ্ধান্ত — এটা তোমার কাজ থেকেই যায়। Tool method সরায়; মানুষ meaning decide করে।
সুবিধা আর ঝুঁকি
| সুবিধা | ঝুঁকি / cost |
|---|---|
| একটা definition, একটা জায়গায় fix — bug fix সব subclass-এ একসাথে লাগে | Intentionally আলাদা body জোর করে merge করলে চুপে চুপে behavior বদলে যায় |
| Superclass shared contract openly document করে: "আমার সব child এটা করে" | Pull up করা method-এর dependency-ও হয়তো hoist করতে হবে, change বড় হয় |
| Total code কমে; hierarchy-র surface area কমে | Subclass-গুলো drift করার স্বাধীনতা হারায় — সাধারণত ভালো, মাঝে মাঝে constraining |
| Drift bug (7C-র missing signature-এর মতো) unification-এর সময় ধরা পড়ে আর fix হয় | শুধু কিছু subclass behavior চাইলে Refused Bequest তৈরি হয় |
| Body skeleton share করলে Form Template Method-এর দরজা খুলে যায় | Pull Up Field-এর চেয়ে বড় review diff — step ছোট রাখো আর commit আলাদা করো |
Direction compass আরেকবার, কারণ Pull Up Method আর Push Down Method exact inverse, ঠিক field cousin-দের মতো। পুরো decision table:
| পরিস্থিতি | সঠিক move | কেন |
|---|---|---|
| প্রতিটা subclass একইভাবে behavior perform করে | Pull Up Method | Office board-এ একটা কাগজ |
| প্রতিটা subclass ছোট intentional blank সহ perform করে | Skeleton pull up + abstract step (Form Template Method) | Shared procedure, local blank |
| শুধু এক-দুটো subclass করে | Push Down Method | শুধু 7C call করে এমন parent method parent-এর প্রতিটা reader-কে mislead করে |
| কিছু-কিন্তু-সব-না subclass share করে | Intermediate parent বা composition consider করো | Subset নিজেই একটা real concept হতে পারে |
Hierarchy তখনই healthy যখন প্রতিটা behavior সবচেয়ে নিচের level-এ থাকে যেখানে নিচের সবাই এখনও এটা চায় — এর নিচে না (duplication), এর উপরে না (refused bequest)। Pull up আর push down দুটো হাত যেটা প্রতিটা member-কে সেই level-এ nudge করতে থাকে।
কোন smell-গুলো cure করে?
| Smell | Pull Up Method কীভাবে সাহায্য করে |
|---|---|
| Duplicate Code | Primary cure — N copy behavior এক definition-এ collapse হয় |
| Shotgun Surgery | নিয়ম বদলালে এখন এক method touch হয়, প্রতিটা sibling class না |
| Long Class / bloated subclass | প্রতিটা subclass copied bulk থেকে মুক্তি পায়, শুধু সত্যিকারের নিজের জিনিস রাখে |
| Refused Bequest | Indirect: direction compass সঠিকভাবে apply করলে (আর কখন push down করতে হবে জানলে) এই smell লাগানো prevent হয় |
Quick revision box
+--------------------------------------------------------------+
| PULL UP METHOD — REVISION |
+--------------------------------------------------------------+
| Story : 3 sections, 3 drifting "How to Apply for Leave" |
| sheets -> wrong absences, missed signatures. |
| Fix: ONE school-wide sheet on the office board. |
| |
| Move : Method duplicated in subclasses ---> superclass |
| |
| Do it when : bodies are identical, or differences are |
| ACCIDENTS you can safely unify |
| Don't when : differences are INTENTIONAL (-> Form Template |
| Method) or only SOME subclasses need it |
| (-> Push Down Method) |
| |
| Safe steps : read copies side by side -> hoist dependencies |
| (Pull Up Field first!) -> make copies identical |
| IN PLACE -> copy body to parent -> delete from |
| subclasses ONE AT A TIME, testing every step |
| |
| Inverse : Push Down Method |
| Friends : Pull Up Field (enabler), Pull Up Constructor |
| Body (for constructors), Form Template Method |
| Cures : Duplicate Code, Shotgun Surgery |
+--------------------------------------------------------------+এখন তোমার পালা
ধরো তুমিই এখন Headmistress। স্কুলের fee software-এ এই hierarchy আছে:
abstract class FeeAccount {
protected balanceDue = 0;
}
class TuitionAccount extends FeeAccount {
recordPayment(amount: number): string {
if (amount <= 0) throw new Error("Payment must be positive");
this.balanceDue -= amount;
return `Receipt: Rs.${amount} received. Balance: Rs.${this.balanceDue}`;
}
}
class TransportAccount extends FeeAccount {
recordPayment(amount: number): string {
if (amount <= 0) throw new Error("Payment must be positive");
this.balanceDue -= amount;
return `Receipt: Rs.${amount} received towards transport. Balance: Rs.${this.balanceDue}`;
}
}
class HostelAccount extends FeeAccount {
recordPayment(amount: number): string {
if (amount < 0) throw new Error("Payment must be positive"); // look closely!
this.balanceDue -= amount;
return `Receipt: Rs.${amount} received towards hostel. Balance: Rs.${this.balanceDue}`;
}
}তোমার কাজ:
- তিনটা body পাশাপাশি রেখে compare করো। প্রতিটা পার্থক্য list করো। (দুই ধরনের পার্থক্য লুকিয়ে আছে।)
HostelAccount<ব্যবহার করেছে যেখানে বাকিরা<=। দুর্ঘটনা নাকি intentional? প্রতিটা choice কী allow করে? স্কুলের আসল নিয়ম কী হওয়া উচিত decide করো, আর কিছু বদলানোর আগে একটা characterization test লেখো।- Receipt-এর wording account ভেদে আলাদা ("towards transport", "towards hostel")। এটা accidental drift নাকি intentional blank? Full pull-up আর skeleton-plus-abstract-step design-এর মধ্যে choose করো, এক বাক্যে justify করো।
- Safe order-এ refactoring করো: in place unify করো (test),
FeeAccount-এ copy করো (test), একটা একটা করে subclass থেকে মুছো (প্রতিবার test)। - Bonus: পরের term-এ একটা নতুন
LibraryFineAccountsubclass আসবে। তোমার refactored design-এ আর original-এ কতটা line দরকার গুনে দেখো। সেই পার্থক্যটাই এই refactoring-এর dividend যা প্রতিটা future developer পাবে।
Duplicate procedure এক জায়গায় থাকলে আর প্রতিটা subclass শুধু নিজের সৎ blank রাখলে, তুমি Pull Up Method আয়ত্ত করেছ। পরে আমরা দেখব এই technique সরাসরি পৌঁছাতে পারে না এমন একটা জায়গা — constructor — Pull Up Constructor Body দিয়ে।
সচরাচর জিজ্ঞাসা
- Pull Up Method এক কথায় কী করে?
- যখন দুই বা তার বেশি subclass-এ একই body-র method থাকে, Pull Up Method সেই method-টা common superclass-এ নিয়ে যায় আর subclass-এর copy মুছে দেয়। ফলে behavior একবার লেখা হয়, সবাই inherit করে।
- method body-গুলো একরকম না হয়ে শুধু কাছাকাছি হলে কী করব?
- আগে দেখো পার্থক্যটা কি ঘটনাক্রমে হয়েছে — অনেক সময় একটা copy-তে bug থাকে বা পুরনো হয়ে যায়, আর একসাথে করলে সেই bug-টা fix হয়ে যায়। কিন্তু পার্থক্যটা যদি সত্যিকারের intentional হয়, তাহলে জোর করে merge করো না। shared skeleton-টা pull up করো, আর আলাদা step-গুলো abstract বা overridable method হিসেবে রাখো — এটাই Form Template Method refactoring।
- যে method pull up করতে চাইছি সেটা subclass-এর একটা field ব্যবহার করে। এখন কী করব?
- আগে dependency-গুলো সরাও। Pull Up Field apply করো যাতে method-এর দরকারি data superclass-এ চলে আসে, অথবা parent-এ abstract method declare করো যেগুলো subclass implement করবে। method-টা যা-যা touch করে সেগুলো parent থেকে visible না হওয়া পর্যন্ত method নিজে উপরে উঠতে পারবে না।
- pull up করা method কি subclass-ভেদে আলাদাভাবে কাজ করতে পারে?
- পারে। shared body superclass-এ থাকে, আর যেটুকু আলাদা হতে হবে সেটা একটা abstract বা virtual method-এর call হিসেবে লেখা হয় যেটা প্রতিটা subclass নিজের মতো করে override করে। common অংশ একবারই লেখা হয়; শুধু সত্যিকারের আলাদা অংশটুকু নিচে থাকে।
- Push Down Method কখন সঠিক সিদ্ধান্ত?
- যখন একটা method superclass-এ আছে কিন্তু শুধু এক-দুটো subclass আসলে সেটা ব্যবহার করে। উপরে রাখলে বাকি সব child অপ্রয়োজনীয় behavior inherit করতে বাধ্য হয় — এটাই Refused Bequest smell। Push Down Method ঠিক সেই subclass-গুলোতে নিয়ে যায় যাদের দরকার; এটা Pull Up Method-এর exact উল্টো।
আরো দেখো
সম্পর্কিত পাঠ
Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা
বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।
Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি
Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।
Pull Up Field: সবার জন্য একটাই নোটিশ বোর্ড
Pull Up Field refactoring শিখো একটা স্কুলের নোটিশ বোর্ডের গল্প দিয়ে — যে field প্রতিটা subclass-এ বারবার কপি হয়েছে সেটাকে superclass-এ তুলে দাও, TypeScript আর C#-এ নিরাপদ ধাপ, IDE সাপোর্ট, আর pull-up বনাম push-down কোনটা কখন করবে।
Pull Up Constructor Body: একটাই সকালের রুটিন, তারপর নিজের কাজ
Pull Up Constructor Body refactoring শেখো school-এর সকালের রুটিনের গল্প দিয়ে — সব subclass constructor-এর শুরুতে যে duplicate initialization আছে, সেটা superclass constructor-এ তুলে নাও আর super/base দিয়ে call করো।