Introduce Local Extension: ভাড়ার দোকানের পাশে নিজের কেবিন বানাও
Introduce Local Extension রিফ্যাক্টরিং শেখো একটা গল্পের মাধ্যমে — যেখানে ভাড়া করা দোকানের পাশে নিজের কেবিন বানানো হয় যেটা পরিবর্তন করা যায় না। যখন একটা লক করা class-এ অনেক method নেই, সেগুলো একটা extension type-এ জড়ো করো — subclass, wrapper, বা আধুনিক C#/Kotlin extension class হিসেবে। TypeScript এবং C#-এ সম্পূর্ণ walkthrough।
🏠 জামাল চাচা এবং ভাড়ার দোকানের পাশের কেবিন
জামাল চাচা ঢাকার একটা ভাড়া করা দোকানে দর্জি ব্যবসা করেন। দোকানটা ভালো — মেইন রোডে, মজবুত দেওয়াল, পনেরো বছর ধরে নিয়মিত খরিদ্দার। কিন্তু এটা ভাড়ার, আর মালিক করিম সাহেবের চুক্তিতে একটা কড়া নিয়ম আছে: ভবনে কোনো পরিবর্তন করা যাবে না। দেওয়ালে নতুন তাক লাগানো যাবে না, বাড়তি রুম বানানো যাবে না, তারের কাজ করা যাবে না। করিম সাহেব প্রতি মাসে আসেন এবং একজন ডাক্তারের মতো দেওয়ালগুলো পরীক্ষা করেন।
কিছুদিন জামাল চাচা ছোট ছোট কৌশলে কাজ চালান। একটা হুক যেটা ড্রিল ছাড়াই দরজার উপর ঝুলানো। একটা ভাঁজ করা টেবিল যেটা প্রতি রাতে স্কুটারে করে বাড়ি নিয়ে যান। কাউন্টারের নিচে বোতামের একটা প্লাস্টিকের তব। এগুলো তার "ব্যাগের স্ট্যাপলার" — ছোট ছোট পোর্টেবল সমাধান, ঠিক আগের পোস্টে দেখা foreign method-এর মতো।
কিন্তু ব্যবসা বাড়তে থাকে। বিয়ের মৌসুমে অর্ডার দ্বিগুণ হয়ে যায়। এখন দরকার কাটিং টেবিল, ইস্ত্রির কোনা, খরিদ্দারের বসার বেঞ্চ, এবং পঞ্চাশটা স্যুটের কাপড়ের জায়গা। সাতটা পোর্টেবল কৌশল এমন একটা দোকানে যেটা পরিবর্তন করা যাচ্ছে না? হুকটা প্রতি মঙ্গলবার পড়ে যায়, ভাঁজ করা টেবিল কাটার মাঝখানে দুলতে থাকে, তার সহকারী রুবেল মাপের বই কখনো খুঁজে পায় না, আর একবার একজন খরিদ্দার বোতামের তবের উপর বসে পড়েছিল।
তাই জামাল চাচা চালাক কাজটা করেন। দোকানের পাশে একটা ফাঁকা জায়গা আছে — তার নিজের জমি, বছর আগে কেনা। তিনি সেখানে একটা ছোট সংযুক্ত কেবিন বানান। ভাড়ার দোকান অপরিবর্তিত থাকে; করিম সাহেব দেওয়াল পরীক্ষা করেন এবং অভিযোগ করার কিছু পান না। কেবিনে দোকানে যা নেই সব আছে — কাটিং টেবিল, ইস্ত্রির স্ট্যান্ড, বেঞ্চ, স্টোরেজ — সব এক জায়গায়, গোছানো। একটা connecting door দোকান এবং কেবিনকে এক ইউনিট হিসেবে কাজ করায়। খরিদ্দাররা বুঝতেও পারেন না কোথায় দোকান শেষ হয় আর কেবিন শুরু হয়।
এটাই হলো Introduce Local Extension রিফ্যাক্টরিং। যখন তুমি পরিবর্তন করতে পারো না এমন একটা class-এ একটা নয় বরং পুরো একটা family of method নেই, তখন তুমি সব জায়গায় helper function ছড়িয়ে রাখা বন্ধ করো। তুমি একটা নতুন type বানাও — তোমার নিজের কেবিন — যেটা সব সংযোজন ধারণ করে এবং original-এর সাথে সুন্দরভাবে সংযুক্ত থাকে। লক করা class লক থাকে। তোমার extension সম্পূর্ণ তোমার: নামকরা, tested, পুনর্ব্যবহারযোগ্য।
🔍 Introduce Local Extension কী?
Introduce Local Extension হলো Introduce Foreign Method-এর বড় ভাই, দুটোই Fowler-এর Refactoring থেকে। পরিস্থিতি: একটা foreign, unmodifiable class-এ অনেকগুলো additional method দরকার। সমাধান: একটা নতুন type তৈরি করো যেটায় সব আছে, এমনভাবে বানানো যে সেটা original-এর মতো আচরণ করে এবং তোমার সংযোজনগুলোও আছে।
Fowler দুটো classic shape বর্ণনা করেছেন:
- Subclass form — foreign class extend করো এবং তোমার method গুলো যোগ করো। কাজ করে যখন class inheritance অনুমতি দেয়। Instance গুলো স্বয়ংক্রিয়ভাবে যেকোনো জায়গায় ব্যবহারযোগ্য যেখানে original আশা করা হয়।
- Wrapper form — একটা নতুন class যেটা একটা field-এ foreign instance ধারণ করে, তোমার method গুলো যোগ করে, এবং প্রয়োজনমতো delegate বা convert করে। Class sealed হলেও কাজ করে।
এখানে wrapper form আছে, ছড়িয়ে থাকা date helper গুলো একটা home-এ জড়ো করে:
class CalendarDate {
constructor(public readonly value: Date) {}
nextDay(): CalendarDate {
const d = new Date(this.value);
d.setDate(d.getDate() + 1);
return new CalendarDate(d);
}
isWeekend(): boolean {
const day = this.value.getDay();
return day === 0 || day === 6;
}
endOfMonth(): CalendarDate {
return new CalendarDate(
new Date(this.value.getFullYear(), this.value.getMonth() + 1, 0)
);
}
}
// Usage — fluent, chainable, readable:
const settlement = new CalendarDate(invoiceDate).nextDay().endOfMonth();তিনটা operation যেগুলো আগে তিনটা আলাদা service-এ private helper ছিল সেগুলো এখন একসাথে আছে, chain করা যায়, এবং একটা unit হিসেবে unit-test করা যায়। value property হলো connecting door যেটা দিয়ে plain Date-এ ফিরে যাওয়া যায় যখন কোনো API original type চায়।
Client code যখন extension ব্যবহার করে, locked type দেওয়ালের পেছন থেকে চুপচাপ কাজ করে — প্রতিটা added method শেষমেশ original-এর public surface পড়ে:
Local extension কে একটা নামকরণের প্রশ্নের উত্তর হিসেবে ভাবো: এই helper-এর cluster টা আসলে কী? ছড়িয়ে থাকা function nextDay, isWeekend, endOfMonth হলো নামহীন যন্ত্রপাতি। CalendarDate-এ জড়ো করা হলে, সেগুলো একটা missing CONCEPT প্রকাশ করে — তোমার domain-এর calendar date-এর ধারণা। ভালো local extension প্রায়ই genuine domain type-এ পরিণত হয়।
🚦 কখন এটা দরকার?
Introduce Local Extension বেছে নাও যখন:
- Foreign method গুলো বেড়ে গেছে। একই locked type-এর জন্য তিন বা তার বেশি helper, বিশেষত বিভিন্ন client class-এ ছড়িয়ে থাকলে, প্রত্যেকটা অন্যগুলোর কাছে অদৃশ্য। হালকা সমাধান পার হয়ে গেছে।
- Helper গুলো duplicate হচ্ছে। দুটো service প্রত্যেকে আলাদাভাবে
isWeekendimplement করেছে — সামান্য ভিন্নভাবে। ছড়িয়ে রাখলে ভিন্নতা বাড়ে; এক home এক সত্য নিশ্চিত করে। - Operation গুলো একটা concept তৈরি করছে। একসাথে তারা এমন কিছু বর্ণনা করে যেটা তোমার domain care করে — একটা calendar date, একটা money amount, একটা phone number — যেটা foreign type শুধু আংশিকভাবে ধারণ করে।
- তুমি সংযোজনগুলো test করতে ও এক unit হিসেবে পুনর্ব্যবহার করতে চাও। Service-এর ভেতরে লুকানো private helper আলাদাভাবে test করা কঠিন; একটা ছোট extension type সহজেই testযোগ্য।
বাদ দাও যখন:
- তুমি class-এর মালিক। শুধু method গুলো যোগ করো, বা Move Method ব্যবহার করো। এই রিফ্যাক্টরিং-এর পুরো বিষয়টাই হলো পরিবর্তন করা যাবে না সীমাবদ্ধতা।
- এক বা দুটো method যথেষ্ট। Introduce Foreign Method-এ থাকো; একটা method-এর জন্য wrapper হলো একটা হুকের জন্য কেবিন বানানো।
- Wrapper বেশিরভাগ forward করবে। তোমার extension দুটো method যোগ করে কিন্তু চল্লিশটা forward করে, তাহলে তুমি অন্যের class-এর চারপাশে একটা Middle Man তৈরি করেছ — client রা delegation noise-এ ডুবে যায়। হয় subclass/extension-method form বেছে নাও (কোনো forwarding লাগে না), নয়তো boundary পুনর্বিবেচনা করো। Hide Delegate territory থেকে একই dial logic প্রযোজ্য: wrapping value যোগ করা উচিত, echo করা নয়। আর Message Chains-এর মতো, লক্ষ্য সবসময় হলো client রা একটা sensible object-এর সাথে কথা বলবে, plumbing-এর layer-এর সাথে নয়।
একত্রিত করার আগে, একটা সাধারণ service codebase-এ date helper গুলো আসলে কোথায় থাকে? inventory চালাও এবং উত্তর সাধারণত বিব্রতকর:
আর এই ছড়িয়ে থাকা স্থির নয় — team বাড়ার সাথে এটা বাড়তে থাকে। প্রতিটা নতুন service যেটার একটা date trick দরকার সেটা আবার implement করে, কারণ কেউ অন্য service-এর private helper দেখতে পায় না:
👀 এক নজরে আগে এবং পরে
// ---------- BEFORE: the same concept, shattered across services ----------
class BillingService {
private nextDay(d: Date): Date { // foreign method #1
const r = new Date(d); r.setDate(r.getDate() + 1); return r;
}
}
class PayrollService {
private isWeekend(d: Date): boolean { // foreign method #2
return d.getDay() === 0 || d.getDay() === 6;
}
private endOfMonth(d: Date): Date { // foreign method #3
return new Date(d.getFullYear(), d.getMonth() + 1, 0);
}
}
class ReportService {
private isWeekend(d: Date): boolean { // DUPLICATE of #2!
return [0, 6].includes(d.getDay());
}
}// ---------- AFTER: one local extension, one truth ----------
class CalendarDate {
constructor(public readonly value: Date) {}
nextDay(): CalendarDate { /* as above */ return this; }
isWeekend(): boolean { /* one definition */ return true; }
endOfMonth(): CalendarDate { /* one definition */ return this; }
}
// Every service now:
const payday = new CalendarDate(rawDate).endOfMonth();
if (payday.isWeekend()) { /* shift to Friday */ }🪜 ধাপে ধাপে, নিরাপদ পথে
ধাপ ১ — ছড়িয়ে থাকা helper গুলোর inventory নাও। Codebase-এ type-কে target করা প্রতিটা foreign method খোঁজো। সেগুলো list করো, duplicate note করো, এবং duplicate-এর মধ্যে behavioral পার্থক্য note করো (এগুলো গুরুত্বপূর্ণ!)।
ধাপ ২ — Form বেছে নাও।
| প্রশ্ন | হ্যাঁ হলে → |
|---|---|
| Class কি inheritance-এর জন্য open, এবং caller রা কি তোমার instance original-এর জায়গায় pass করতে চায়? | Subclass |
| Class কি sealed/final, বা তোমার কি added state এবং enforced rules দরকার? | Wrapper |
| তোমার language-এ কি extension methods/functions আছে, এবং তোমার কি নতুন state দরকার নেই? | Extension class (আধুনিক তৃতীয় form) |
একটা map হিসেবে একই সিদ্ধান্ত — তোমার পরিস্থিতি খোঁজো, form পড়ো:
ধাপ ৩ — Bridge সহ খালি extension type তৈরি করো। Wrapper-এর জন্য, instance store করো এবং expose করো; subclass-এর জন্য, constructor forward করো। এখনো কোনো behavior নেই:
class CalendarDate {
constructor(public readonly value: Date) {}
// bridge back to the foreign type = the .value door
}Compile করো, test করো। সবুজ, সহজ।
ধাপ ৪ — একটা helper extension-এ move করো। PayrollService থেকে isWeekend নাও, CalendarDate-এ নিয়ে আসো, পুরনো first-parameter হয়ে যাবে this.value। পুরনো helper সাময়িকভাবে রেখে দাও, delegate করতে:
class PayrollService {
// temporary shim during migration:
private isWeekend(d: Date): boolean {
return new CalendarDate(d).isWeekend();
}
}Test করো। তারপর service-এর call site গুলো সরাসরি CalendarDate ব্যবহার করতে redirect করো, এবং shim মুছে দাও।
ধাপ ৫ — প্রতিটা helper-এর জন্য repeat করো, সচেতনভাবে duplicate সমাধান করো। যখন দুটো copy আলাদা হয় (ReportService বনাম PayrollService-এর isWeekend version), সচেতনভাবে সঠিক behavior বেছে নাও এবং একত্রিত করার আগে একটা test লিখে সেটা pin করো।
ধাপ ৬ — Seam গুলো wire করো। যেখানে code CalendarDate hold করে কিন্তু কোনো API Date চায়, সেখানে .value pass করো। Back-and-forth বেশি noisy হলে, একটা static factory যোগ করো (CalendarDate.of(raw)) এবং module boundary-তে conversion রাখো।
ধাপ ৭ — Extension কে unit হিসেবে test করো। বড় পুরস্কার: CalendarDate এখন তার নিজস্ব ছোট, দ্রুত test file পায় — ছড়িয়ে থাকা private helper কখনো পেতো না।
পুরো migration, state হিসেবে দেখা হলে মধ্যে নিরাপদ pause point সহ:
একসময় একটা helper move করো এবং move-এর মাঝখানে test চালাও। সবচেয়ে ঝুঁকিপূর্ণ মুহূর্ত হলো duplicated helper একত্রিত করা যাদের behavior চুপচাপ আলাদা — যেমন দুটো isWeekend implementation যারা একটা locale নিয়ে একমত না। behavior পরিবর্তন করে এমন একত্রীকরণ refactoring নয়; এটা একটা feature change যেটা cleanup-এর ভেতরে লুকিয়ে আছে। আগে test দিয়ে current behavior pin করো, তারপর সচেতনভাবে সিদ্ধান্ত নাও কোনটা জিতবে।
💰 একটা বড় বাস্তব উদাহরণ
তোমার app একটা payment SDK থেকে Money-like value পায় plain paise integer হিসেবে একটা locked SdkPayment type-এর সাথে যুক্ত। মাসের পর মাস ধরে, team পাঁচটা ছড়িয়ে থাকা helper বানিয়েছে: rupees-এ format, দুটো payment যোগ করা, GST অংশ, is-refundable, comparison। চলো কেবিন বানাই:
// Locked SDK type — regenerated on every update:
class SdkPayment {
constructor(
public readonly paise: number,
public readonly capturedAt: Date
) {}
}
// The local extension — wrapper form, with added rules:
class Rupees {
private constructor(public readonly paise: number) {
if (!Number.isInteger(paise)) throw new Error("paise must be integer");
}
static fromPayment(p: SdkPayment): Rupees { return new Rupees(p.paise); }
static of(paise: number): Rupees { return new Rupees(paise); }
plus(other: Rupees): Rupees { return new Rupees(this.paise + other.paise); }
gstPortion(ratePercent: number): Rupees {
return new Rupees(Math.round((this.paise * ratePercent) / (100 + ratePercent)));
}
isMoreThan(other: Rupees): boolean { return this.paise > other.paise; }
toString(): string { return `₹${(this.paise / 100).toFixed(2)}`; }
}
// Client code — five old helpers replaced by one fluent type:
const bill = Rupees.fromPayment(payment1).plus(Rupees.fromPayment(payment2));
console.log(`Total: ${bill}, GST inside: ${bill.gstPortion(18)}`);দুটো জিনিস লক্ষ্য করার মতো। প্রথমত, wrapper টা state rule যোগ করেছে যেটা foreign type কখনো পায়নি — constructor-এর integer check — যেটা plain extension method কখনো করতে পারবে না। দ্বিতীয়ত, এই local extension চুপচাপ আরেকটা smell-ও সারিয়ে তুলছে: Primitive Obsession। number-with-meaning (paise) একটা real type-এ পরিণত হয়েছে behavior সহ। primitive-ish foreign value-এর চারপাশে wrapper-form local extension প্রায়ই domain modeling হিসেবেও কাজ করে।
কলেজ কোণা: Wrapper form একটা প্রশ্ন উঠায় যেটা প্রতিটা design course-এর করা উচিত: equality এবং identity-র কী হয়? একই paise value ঘেরা দুটো Rupees object আলাদা object — reference equality বলে তারা আলাদা, যেটা set, map, এবং নিরীহ দেখতে comparison ভেঙে দেয়। একটা serious wrapper তাই সচেতনভাবে value semantics define করে: wrapped value-এর উপর ভিত্তি করে equals (এবং matching hash), ideally immutability যাতে hashing-এর পরে value drift করতে না পারে। Subclass form-এর একটা ভিন্ন theoretical ঝুঁকি আছে: Liskov substitution। একটা subclass extension যেখানে parent আশা করা হয় সেখানে pass করা হয়, তাই তোমার সংযোজনগুলো কখনো parent-এর contract দুর্বল করবে না — আর যদি library parent instance construct এবং return করে (factory, deserialiser), তোমার subclass method গুলো ওই object-এ unreachable। এই "তুমি instantiation নিয়ন্ত্রণ করো না" সমস্যাটাই হলো মূল practical কারণ কেন framework type-এর জন্য wrapper এবং extension class subclassing-এর চেয়ে ভালো।
💻 C#-এ একই রিফ্যাক্টরিং
C# এই section-এর তারকা, কারণ ভাষাটা এই রিফ্যাক্টরিং দুইবার absorb করেছে।
Form 3, classic: extension-method class। C# 3.0 থেকে, তুমি সব missing operation একটা static class-এ জড়ো করতে পারো এবং সেগুলো foreign type-এ যেন ছিলই হিসেবে call করতে পারো — কোনো subclass নেই, wrapper নেই, conversion seam নেই:
public static class DateTimeCalendarExtensions
{
public static DateTime NextDay(this DateTime d) => d.AddDays(1);
public static bool IsWeekend(this DateTime d) =>
d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public static DateTime EndOfMonth(this DateTime d) =>
new(d.Year, d.Month, DateTime.DaysInMonth(d.Year, d.Month));
}
// Call sites — indistinguishable from native members:
var payday = invoiceDate.EndOfMonth();
if (payday.IsWeekend()) payday = payday.NextDay().NextDay();এটাই সম্পূর্ণ "local extension" শূন্য wrapper boilerplate সহ: একটা নামকরা class family জড়ো করে, IntelliSense DateTime-এ নিজেই method advertise করে, এবং chaining স্বাভাবিকভাবে কাজ করে। Kotlin একই কাজ করে extension function দিয়ে (fun LocalDate.endOfMonth(): LocalDate = ...), যেটা compile হয়ে plain static function হয় receiver কে parameter হিসেবে নিয়ে।
Form 4, modern: C# 14 extension members। .NET 10-এর সাথে, C# 14 extension block যোগ করেছে যেটা extension property, operator, এমনকি static extension member support করে — real member-এর সাথে বেশিরভাগ remaining gap বন্ধ করে:
public static class DateTimeCalendarExtensions
{
extension(DateTime d)
{
public DateTime NextDay => d.AddDays(1); // property!
public bool IsWeekend =>
d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public DateTime EndOfMonth =>
new(d.Year, d.Month, DateTime.DaysInMonth(d.Year, d.Month));
}
}
// Reads exactly like the type always had these properties:
if (invoiceDate.EndOfMonth.IsWeekend) { /* shift settlement */ }পুরনো এবং নতুন form source- এবং binary-compatible, তাই team ধীরে ধীরে block syntax adopt করে।
C#-এ কখন wrapper form এখনো দরকার? যখন তোমার নতুন state বা enforced invariant দরকার — extension member field বা constructor check যোগ করতে পারে না। তখন classic Fowler wrapper ফিরে আসে:
public readonly struct Rupees
{
public int Paise { get; }
public Rupees(int paise) =>
Paise = paise >= 0 ? paise : throw new ArgumentOutOfRangeException();
public Rupees Plus(Rupees other) => new(Paise + other.Paise);
public override string ToString() => $"₹{Paise / 100m:F2}";
public static implicit operator int(Rupees r) => r.Paise; // the door back
}আধুনিক C#/Kotlin-এ rule of thumb: শুধু behavior → extension class; behavior plus state বা rules → wrapper; original-এর API-তে substitutability → subclass (যখন অনুমতি আছে)।
কলেজ কোণা: C#/Kotlin extension class কেন Middle Man trap থেকে মুক্ত যেটা wrapper-কে তাড়া করে? কারণ তারা intercept না করে যোগ করে। একটা wrapper client এবং original-এর মাঝখানে দাঁড়ায়, তাই client-এর এখনো দরকার এমন প্রতিটা original operation forward করতে হবে — সেই forwarding হলো middle-man tax। একটা extension class original-এর পাশে দাঁড়ায়: client রা সরাসরি real DateTime member call করতে থাকে, এবং শুধু সংযোজনগুলো static class-এর মাধ্যমে route করে। Compile-time rewrite (d.NextDay() হয়ে যায় DateTimeCalendarExtensions.NextDay(d)) JIT inlining-এর পরে runtime-এ কিছু খরচ করে না এবং কিছু forward করে না। Trade-off টা foreign method-এর মতোই: non-public state-এ কোনো access নেই, কোনো polymorphism নেই, এবং resolution import-এর উপর নির্ভর করে — যেটাই কারণ discoverability convention (প্রতি type-এর জন্য একটা ভালোভাবে নামকরা extension file, predictable namespace) code-এর মতোই গুরুত্বপূর্ণ।
🐍 একবার Python-এ
Python-এ কোনো extension method নেই, এবং built-in monkey-patching blocked (এবং অন্যত্রও অনুচিত)। তাই wrapper হলো idiomatic Python cabin:
class CalendarDate:
def __init__(self, value: date):
self.value = value # the connecting door
def next_day(self) -> "CalendarDate":
return CalendarDate(self.value + timedelta(days=1))
def is_weekend(self) -> bool:
return self.value.weekday() >= 5 # Sat=5, Sun=6
def end_of_month(self) -> "CalendarDate":
last = calendar.monthrange(self.value.year, self.value.month)[1]
return CalendarDate(self.value.replace(day=last))
def __eq__(self, other): # value semantics, deliberately
return isinstance(other, CalendarDate) and self.value == other.value__eq__ লক্ষ্য করো: সেটাই College corner থেকে equality lesson, প্রয়োগ করা। এটা ছাড়া, একই date ধরে রাখা দুটো CalendarDate object unequal তুলনা করবে, এবং payday-এর একটা set চুপচাপ duplicate ধারণ করবে।
⚖️ ভারসাম্য: একটা locked class-এর জন্য কতটুকু কাঠামো?
এই রিফ্যাক্টরিং তার হালকা ভাইয়ের সাথে একটা dial-এ থাকে — ঠিক Hide Delegate যেভাবে Remove Middle Man-এর সাথে share করে:
- মাত্র কয়েকটা missing method → Introduce Foreign Method। ব্যাগের স্ট্যাপলার।
- বাড়তে থাকা, duplicate হওয়া helper family → Introduce Local Extension। সংযুক্ত কেবিন।
- আর যে overshoot লক্ষ্য রাখতে হবে: একটা wrapper যেটা দুটো যোগ করতে চল্লিশটা forward করে — এটাই Middle Man failure mode, এবং সমাধান wrapper slim করা বা extension-class form-এ switch করা।
সবচেয়ে ছোট কাঠামো বানাও যেটা concept কে একটা home দেয়। শুধু তখনই upgrade করো যখন count আর duplication তোমাকে বলে।
✅ সুবিধা এবং ঝুঁকি
| সুবিধা | কেন গুরুত্বপূর্ণ |
|---|---|
| একটা নামকরা home-এ সব missing operation | Concept দৃশ্যমান, খুঁজে পাওয়া যায়, এবং পুনর্ব্যবহারযোগ্য হয়ে ওঠে |
| স্বাধীনভাবে unit-testযোগ্য | ছড়িয়ে থাকা private helper কখনো ছিল না |
| Duplicate গুলো এক সত্যে collapse করে | Divergent copy (এবং তাদের bug) অদৃশ্য হয় |
| Wrapper form state এবং rule যোগ করতে পারে | Foreign type কখনো enforce করেনি এমন invariant |
| Native form (C#/Kotlin) boilerplate মুছে দেয় | Extension class-এর কোনো forwarding দরকার নেই |
| ঝুঁকি | কীভাবে handle করবে |
|---|---|
| সমস্যার চেয়ে বেশি ভারী | ১–২ method-এর জন্য, Introduce Foreign Method-এ থাকো |
| Wrapper forwarding noise → accidental Middle Man | শুধু যা use হয় তাই forward করো; state দরকার না হলে extension class prefer করো |
| Wrapper-এর সাথে equality এবং identity বিস্ময় | Equals/conversion সচেতনভাবে define করো; value semantics prefer করো (readonly struct, ===-safe design) |
| Sealed type দ্বারা Subclass form blocked | বেশিরভাগ framework type sealed — wrapper বা extension class-এ default করো |
| API boundary-তে conversion seam | Conversion module edge-এ রাখো; একটাই obvious door expose করো (.value / implicit operator) |
🧪 কোন smell গুলো এটা সারায়?
| Smell | Introduce Local Extension কীভাবে সাহায্য করে |
|---|---|
| Duplicate Code | একটা foreign type-এর জন্য repeated helper গুলো একটা definition-এ merge করে |
| Scattered foreign methods | পুরো family কে একটা নামকরা type-এ জড়ো করে |
| Primitive Obsession | Wrapper form একটা raw value কে rule সহ real domain type-এ পরিণত করে |
| Shotgun Surgery (ওই concept-এর জন্য) | Concept-এর rule-এর পরিবর্তন অনেক service না ছুঁয়ে একটা type ছোঁয় |
| Middle Man | ⚠️ এটা সারায় না — over-forwarding wrapper তৈরি করে |
🛠️ IDE support
- Rider / ReSharper: Extract Class এবং Make Static এবং "Convert to extension method" context action ছড়িয়ে থাকা helper গুলো দ্রুত extension class-এ পরিণত করে; Move Static Members একাধিক class থেকে helper জড়ো করে। C# 14
extensionblock syntax-এর জন্য সম্পূর্ণ refactoring support .NET 10 tooling-এর পাশাপাশি এসেছে। - Visual Studio: Quick Actions eligible static method কে extension method-এ convert করতে পারে; Find All References একত্রিত করার আগে প্রতিটা ছড়িয়ে থাকা helper map করে; IntelliSense সম্পূর্ণ extension গুলো foreign type-এ surface করে, যেটা consolidated home টাকে পুরো team-এর কাছে discoverable করে।
- IntelliJ IDEA (Kotlin): Extension function-এর জন্য first-class support, Convert receiver to parameter এবং back সহ, এবং Move refactoring একটা file-এ extension জড়ো করতে — local extension-এর Kotlin idiom আক্ষরিক অর্থে "type-এর উপর extension-এর একটা file।"
- VS Code (TypeScript): File জুড়ে automated class-extraction নেই; inventory step-এর জন্য Find All References ব্যবহার করো এবং migration-এর সময় compiler-এর উপর lean করো। TypeScript-এ native extension method নেই (এবং library prototype-এর module augmentation discouraged), তাই wrapper form idiomatic পছন্দ।
📦 দ্রুত revision box
+-----------------------------------------------------------------+
| INTRODUCE LOCAL EXTENSION — CHEAT SHEET |
+-----------------------------------------------------------------+
| Situation : locked class missing SEVERAL methods |
| Move : build ONE extension type holding them all |
| Form 1 : SUBCLASS - class is open, need substitutability |
| Form 2 : WRAPPER - class is sealed, or need state/rules |
| Form 3 : EXTENSION CLASS - C# / Kotlin native support |
| Bridge : keep a clear door back to the original type |
| Cures : Duplicate Code, scattered helpers, Primitive |
| Obsession (wrapper form) |
| Too small? : 1-2 methods -> Introduce Foreign Method instead |
| Overshoot? : forwarding-heavy wrapper -> Middle Man, slim it |
| Own it? : your class -> just add methods (Move Method) |
+-----------------------------------------------------------------+✏️ Practice exercise
তোমার project একটা locked SDK type-এর উপর নির্ভর করে courier tracking-এর জন্য:
// Generated SDK type. DO NOT EDIT.
class TrackingEvent {
constructor(
public readonly pincode: string, // "560001"
public readonly status: string, // "IN_TRANSIT" | "OUT_FOR_DELIVERY" | "DELIVERED"
public readonly timestamp: number // epoch millis
) {}
}চারটা module জুড়ে তুমি এই helper গুলো পাবে (কিছু duplicate, একটা pair inconsistent):
// notifications module:
const isFinal = event.status === "DELIVERED";
// dashboard module:
const city = event.pincode.startsWith("5600") ? "Bengaluru" : "Other";
const when = new Date(event.timestamp).toLocaleString("en-IN");
// sla module:
const isLate = Date.now() - event.timestamp > 48 * 3600 * 1000;
// audit module — different lateness rule!
const isLate2 = Date.now() - event.timestamp > 72 * 3600 * 1000;তোমার কাজ:
TrackingEvent-এ missing operation গুলোর inventory নাও। কোনগুলো duplicate? কোন pair দ্বিমত পোষণ করছে? নিজের চিত্র ৪ pie আঁকো কোথায় তারা থাকে।- দুটো lateness rule আলাদা (৪৮ বনাম ৭২ ঘন্টা)। কোনটা সঠিক তা investigate করো — তারপর method design করো যাতে পার্থক্যটা explicit থাকে (hint:
isOlderThan(hours: number)উভয় caller কে সৎ রাখে)। - তোমার form বেছে নিতে চিত্র ৭ map ব্যবহার করো: TypeScript-এ তুমি wrapper-এ পৌঁছাবে; C# project-এ,
TrackingEventExtensionsএবং wrapper-এর মধ্যে সিদ্ধান্ত নাও, এবং state-or-rules test দিয়ে justify করো। - একটা
Shipmentwrapper (বা extension class) বানাও:isDelivered(),cityFromPincode(),localTime(),isOlderThan(hours)। একসময় একটা module migrate করো, move-এর মাঝখানে test করো, চিত্র ৮-এর প্রতিটা state-এ pause করো। - তোমার নতুন type-এর জন্য একটা dedicated unit-test file লেখো — যেটা ছড়িয়ে থাকা helper কখনো পায়নি। কমপক্ষে পাঁচটা ছোট test, wrapper বেছে নিলে একটা equality test সহ।
- Verdict প্রশ্ন: তোমার wrapper-এ চারটা added method আছে এবং শূন্যটা forwarded। এটা কি Middle Man বিপদে আছে? কেন নয়? একটা বাক্য লেখো।
তোমার এক-বাক্যের উত্তরে যখন বলবে "এটা SDK echo না করে value যোগ করে," তখন তুমি এই series-এর চারটা পোস্ট — chains, middle men, staplers, এবং cabins — একটা picture-এ connect করেছ যে behavior কোথায় থাকা উচিত। জামাল চাচার কেবিন, যাইহোক, এখন নিজের একটা ছোট সাইনবোর্ড পেয়েছে। করিম সাহেব প্রতি মাসে ভাড়ার দেওয়ালগুলো পরীক্ষা করেন এবং যেমনটি ছিল ঠিক তেমনটাই পান। তারা দুজনেই খুশি — এটাই যা পরিবর্তন করা যায় না তা extend করার পুরো বিষয়।
সচরাচর জিজ্ঞাসা
- Introduce Local Extension রিফ্যাক্টরিং কী?
- যখন তুমি পরিবর্তন করতে পারো না এমন একটা class-এ অনেকগুলো method নেই, তখন তুমি একটা নতুন type তৈরি করো — subclass বা wrapper — যেটায় সব missing method থাকে এবং original-এর মতোই কাজ করে। client class জুড়ে helper function ছড়িয়ে রাখার বদলে, সব সংযোজন একটা নামকরা, পুনর্ব্যবহারযোগ্য, testযোগ্য জায়গায় থাকে।
- আমি কি আমার local extension-এর জন্য subclass নাকি wrapper বেছে নেব?
- Subclass বেছে নাও যখন foreign class inheritance অনুমতি দেয় এবং তুমি চাও instance গুলো যেকোনো জায়গায় ব্যবহার করা যাক যেখানে original আশা করা হয়। Wrapper বেছে নাও যখন class sealed বা final (framework type-এ খুব সাধারণ), অথবা যখন তুমি বেশি নিয়ন্ত্রণ চাও। Wrapper টি একটা field-এ original instance ধরে রাখে এবং প্রয়োজনমতো delegate বা convert করে। অনেক আধুনিক team তৃতীয় একটা form বেছে নেয়: extension method-এর একটা class, যেটায় inheritance বা wrapping কিছুই লাগে না।
- কখন Introduce Foreign Method যথেষ্ট?
- এক বা দুটো missing method-এর জন্য, একটা foreign method — foreign object কে parameter হিসেবে নেওয়া একটা সরল helper — হালকা এবং দ্রুত। Local extension-এ যাও শুধুমাত্র যখন helper গুলো বেড়ে যায়, client class জুড়ে duplicate হতে শুরু করে, বা এমন একটা concept তৈরি করে যেটার নিজস্ব নাম দরকার।
- C# এবং Kotlin কীভাবে এই রিফ্যাক্টরিং পরিবর্তন করে?
- ওরা এটাকে native syntax দেয়। C# extension methods (এবং C# 14 extension members, extension properties সহ) তোমাকে সব missing operation একটা static class-এ জড়ো করতে দেয় এবং সেগুলো original type-এর মতো call করতে দেয় — কোনো subclass নেই, wrapper নেই, conversion seam নেই। Kotlin extension functions একই কাজ করে। এই তৃতীয় form এখন ওই language গুলোতে default পছন্দ, wrapper শুধু যখন নতুন state বা enforced invariant দরকার তখনের জন্য।
- Wrapper-style local extension-এর প্রধান ঝুঁকিগুলো কী?
- Identity এবং equality-তে বিস্ময় (সমান value ঘেরা দুটো wrapper স্বয়ংক্রিয়ভাবে সমান নয়), প্রতিটি জায়গায় conversion boilerplate যেখানে plain type আশা করা হয়, এবং wrapper টি Middle Man-এ পরিণত হওয়া যদি তুমি শেষ পর্যন্ত ডজন ডজন method forward করতে থাকো। Equality এবং conversion সচেতনভাবে define করো, এবং wrapper কে তার added value-তে focused রাখো।
আরো দেখো
সম্পর্কিত পাঠ
Introduce Foreign Method: নিজের ব্যাগে রাখা স্ট্যাপলার
স্কুলের ফটোকপি মেশিনে স্ট্যাপলার নেই — এই গল্প দিয়ে Introduce Foreign Method শেখো। যে class তুমি বদলাতে পারছ না সেখানে method নেই? সেই method নিজের class-এ লিখো, foreign object-কে parameter হিসেবে নাও। TypeScript আর C# extension method-এর উদাহরণসহ।
Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা
বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।
Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে
একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।