Replace Delegation with Inheritance: যখন সাহায্যকারীই হয়ে যায় শিক্ষানবিশ
Replace Delegation with Inheritance রিফ্যাক্টরিং শেখো একটা দর্জির দোকানের গল্পের মাধ্যমে — Middle Man smell কী, is-a শর্ত কীভাবে চেক করতে হয়, আর TypeScript ও C#-এ ধাপে ধাপে কীভাবে করতে হয় সব বিস্তারিত দেখো।
✂️ যে সাহায্যকারী সবকিছু শুধু পাঠিয়ে দিত
ধরো পুরান ঢাকার একটা গলিতে আছে ওস্তাদ জামালের দর্জির দোকান — বিয়ের শেরওয়ানির জন্য পাড়ায় বিখ্যাত। ওস্তাদ জামাল হলেন আসল কারিগর। ত্রিশ বছর মেশিনে কাজ করেছেন, হাত দিয়েই কাপড়ের মাপ বোঝেন। আর দোকানের সামনে বসে রহিম — সাহায্যকারী, পাঁচ বছর হলো দোকানে আছে, চোখ তীক্ষ্ণ আর হাত দ্রুত।
একদিন সকালে রহিমকে কাজ করতে দেখো। একজন কাস্টমার এলেন মাপ নিতে। রহিম ঘুরে তাকাল: "ওস্তাদ, মাপ!" ওস্তাদ জামাল মাপ নিলেন। আরেকজন কাপড় নিয়ে জিজ্ঞেস করলেন। রহিম ঘুরল: "ওস্তাদ, কাপড়ের প্রশ্ন!" ওস্তাদ উত্তর দিলেন। একটা বোতাম লাগানো দরকার — "ওস্তাদ, বোতাম!" পরিবর্তন — "ওস্তাদ, পরিবর্তন!" বিল — "ওস্তাদ, বিল!"
রহিম নিজে কী করে একটু হিসাব করো। কিছুই না। প্রতিটা কাজ — প্রতিটা — ঘুরিয়ে ওস্তাদের কাছে পাঠানো হয়। রহিম অলস না; দোকানটা এমনভাবে চলে যে সে শুধু একটা pure message-passer। আর এই ব্যবস্থার বাস্তব খরচ আছে। প্রতিটা কাজে একটা বাড়তি hop লাগে। গত শীতে ওস্তাদ কুর্তায় নতুন এমব্রয়ডারি শিখলেন। যে কাস্টমাররা রহিমকে সেটা জিজ্ঞেস করলেন তারা পুরো দুই সপ্তাহ ফাঁকা উত্তর পেলেন — যতক্ষণ না ওস্তাদ মনে করলেন রহিমকেও সেটা forward করতে শেখাতে হবে। forwarding-এর তালিকা রহিমের মাথায় থাকে, আর সেটা চিরকাল হাতে মেনটেইন করতে হয়। পাড়ার ফাতেমা বেগম এখনও গল্প করেন — রহিমকে এমব্রয়ডারির কথা জিজ্ঞেস করলেন, রহিম বলল "এটা আমরা করি না", আর তখন ওস্তাদ দশ ফুট পেছনে বসে এমব্রয়ডারি করছেন।
একদিন সন্ধ্যায় ওস্তাদ জামাল এই সব দেখে একটা সিদ্ধান্ত নিলেন: "রহিম, তুমি পাঁচ বছর ধরে আমার পাশে আছ। প্রতিটা সেলাই দেখেছ। কাল থেকে তুমি আমার শিক্ষানবিশ। কাজ আমার কাছে পাঠাবে না — তুমি এই দোকানের দর্জি। আমি যা পারি, সেটা তুমিও নিজের মতো করতে পারবে।"
এখন একটু ভাবো — কী পাল্টাল আর কী পাল্টায়নি। কাস্টমাররা এখনও একই ছেলের কাছে একই কাউন্টারে আসে। কিন্তু সম্পর্কটা message-passing থেকে being-এ upgrade হলো: রহিম এখন is-a দর্জি। ওস্তাদের প্রতিটা দক্ষতা তার কাছে স্বয়ংক্রিয়ভাবে আসে — নতুন দক্ষতাও, যেদিন ওস্তাদ শেখেন, forwarding তালিকা আর মেনটেইন করতে হয় না। রহিম শুধু সেই কয়েকটা কাজ "তার নিজের উপায়ে" রাখে যেটা সে সত্যিই আলাদাভাবে করে — তার বিখ্যাত পরিপাটি বোতামের ফুটো, ওস্তাদের চেয়েও শক্ত।
আর একটা বিষয় খুব গুরুত্বপূর্ণ। ওস্তাদ রহিমকে promote করলেন কারণ পাঁচ বছর পর ছেলেটা সত্যিই পারত সব কাজে ওস্তাদের জায়গায় দাঁড়াতে। নতুন কাউকে promote করলে যে শুধু বিলিং সামলাতে পারে সেটা বিপর্যয় হতো — সে এমন প্রত্যাশা "inherit" করত যা পূরণ করতে পারত না, আর ফাতেমা বেগমের শেরওয়ানি বাঁকা হাতা নিয়ে ফিরে আসত। এই সতর্কতাটা মনে রেখো — এটাই আজকের পাঠের মূল কথা: Replace Delegation with Inheritance।
Replace Delegation with Inheritance কী? 🔁
এই রিফ্যাক্টরিং হলো Replace Inheritance with Delegation-এর ঠিক উল্টো। আগের পাঠে একটা false is-a-তে আটকানো class উদ্ধার করা হয়েছিল। এই পাঠে উদ্ধার করব অর্থহীন message-passing-এ আটকানো class।
ব্যাপারটা হলো: একটা class একটা field-এ অন্য object রাখে আর কল একের পর এক সেটাতে forward করে। getX() return করে inner.getX()। setY(v) call করে inner.setY(v)। method-এর পর method, পুরো ফাইলটা one-line pass-through। wrapper মূলত delegate-এর পুরো interface support করে, আর এটা যা করে তার প্রায় কিছুই plain forwarding থেকে আলাদা নয়। এটাই হলো Middle Man smell: এমন একটা class যার মূল কাজ message পাস করা।
সেই forwarding-এর খরচ আছে, ঠিক রহিমের মতো:
- Boilerplate যা হাতে মেনটেইন করতে হয়। delegate-এর প্রতিটা method-এ একটা matching forwarder দরকার — একজন মানুষ লেখে, একজন review করে, একজন sync রাখে।
- নীরব ফাঁক। delegate নতুন method পেলে wrapper পায় না — যতক্ষণ না কেউ pass-through যোগ করতে মনে করে। wrapper-এর client-রা একটা update পিছিয়ে থাকে, আর কাউকে জানানো হয় না। (এমব্রয়ডারি গ্যাপের মতো।)
- noise signal-কে চাপা দেয়। যে দুটো method সত্যিই কিছু আলাদা করে সেগুলো বিশটা নিরর্থক method-এর মধ্যে ডুবে যায়। পাঠকদের প্রতিটা forwarder check করতে হয় আসল আচরণ খুঁজে পেতে।
একটা typical middle-man class খুলে তার contents সৎভাবে দেখো:
সমাধান হলো — যখন এবং শুধুমাত্র যখন — সম্পর্কটা সত্যিই is-a: field মুছো, class-কে delegate extend করাও, প্রতিটা pure forwarder মুছে দাও (inheritance এখন সেগুলো বিনামূল্যে দেয়), আর শুধু যে method-গুলো সত্যিই আলাদা সেগুলো রাখো proper override হিসেবে।
এক লাইনে বললে: যখন একটা class মূলত delegate-এর পুরো interface forward করে আর সত্যিই সেই delegate-এর একটা ধরন হয়, তখন field-এর জায়গায় extends দাও — pure forwarder চলে যায়, নতুন parent method স্বয়ংক্রিয়ভাবে আসে, আর শুধু সত্যিকারের পার্থক্যগুলো override হিসেবে থাকে।
দাঁড়িপাল্লা, উল্টো দিক থেকে পড়া ⚖️
এই দুটো delegation refactoring একটা দাঁড়িপাল্লার মতো কাজ করে। গুরুতর codebase বছরের পর বছর উভয় দিকে চলে। ভারসাম্য বিন্দু ঠিক হয় দুটো প্রশ্ন একসাথে জিজ্ঞেস করে:
১. Is-a পরীক্ষা। wrapper কি delegate-এর যেকোনো জায়গায় বিকল্প হতে পারে, শূন্য বিস্ময় নিয়ে, প্রতিটা operation অর্থবহ? (রহিম কি সত্যিই প্রতিটা কাজে ওস্তাদের জায়গায় দাঁড়াতে পারত?) ২. Coverage পরীক্ষা। wrapper কি মূলত পুরো contract support করে — কোনো বাছাই করা অংশ নয়?
দুটোতে হ্যাঁ উত্তর পেলে → forwarding হলো অপচয়; inheritance সৎ আর সাশ্রয়ী — এই পাঠ। যেকোনো একটায় না পেলে → forwarding হলো ডিজাইন: এটাই ঠিক করে কীভাবে তুমি একটা বাছাই করা অংশ প্রকাশ করো আর delegate পরিবর্তনযোগ্য রাখো — আগের পাঠ। "Favor composition over inheritance" এখনও default হিসেবে সত্য; এই রিফ্যাক্টরিং সেই নীতির বিরোধিতা না, বরং তার সূক্ষ্ম লেখা: যখন is-a প্রমাণিত সত্য আর পুরো contract চাওয়া হয়, তখন composition-এর বাড়তি খরচ দেওয়া বন্ধ করো।
নিচের-বাঁয়ে কে আছে দেখো: আগের পাঠের মিষ্টির দোকানের ছেলে। একই মানচিত্র, বিপরীত কোণে, বিপরীত refactoring। একটা ছবিতেই পুরো দাঁড়িপাল্লা।
কলেজ কর্নার: forwarding cost সুনির্দিষ্ট হিসাব পাওয়ার যোগ্য, কারণ এটাই এই refactoring-এর পুরো কারণ। একটা forwarder-এর runtime cost প্রায় কিছুই না — একটা বাড়তি call যা JIT compiler সাধারণত inline করে দেয়। আসল খরচ হলো একটা maintenance invariant: wrapper-এর interface delegate-এর interface-এর সমান রাখতে হবে হাতে, চিরকাল, কোনো tool ছাড়া। প্রতিটা delegate release হলো পিছিয়ে পড়ার সুযোগ; প্রতিটা ফাঁক অদৃশ্য থাকে যতক্ষণ না কোনো caller সেই missing method দরকার পায় (নিচে দেরিতে আসা refundPartial)। Inheritance সেই হাতে-মেনটেইন করা invariant-কে language-enforced দিয়ে replace করে — compiler নিজেই নিশ্চিত করে যে subclass parent যা দেয় তার সব দেয়। তুমি nanosecond বাঁচাচ্ছ না; তুমি মানবিক ত্রুটির একটা পুরো category মুছে দিচ্ছ। দাম: তুমি আবার fragile base class জগতে প্রবেশ করো কারণ subclassing তোমাকে আবার parent-এর internal-এর সাথে couple করে। সেই trade-off — মানবিক-ত্রুটি category মুছা, implementation coupling গ্রহণ করা — দুটো verification পরীক্ষা আসলে সেটাই মাপছে।
কখন দরকার হয়? 🔍
লক্ষণগুলো:
- One-line forwarder-এর দেয়াল। class খোলো: পনেরটা method, তেরটা ঠিক এরকম দেখতে
foo(a) { return this.inner.foo(a); }। এই দেয়ালটাই হলো Middle Man smell তার বিশুদ্ধতম রূপে। - প্রতিটা delegate আপডেটে বাড়তি কাজ তৈরি হয়। delegate-এর team গত sprint-এ দুটো method যোগ করেছে; তোমার sprint board-এ এখন "add pass-throughs" ঢুকে গেছে। এটা maintenance যা কিছুই উৎপন্ন করে না।
- Client-রা "tunnelling" করতে থাকে। caller-রা যারা delegate-এর নতুন method দরকার পড়ল তারা অপেক্ষা ছেড়ে দিয়ে
wrapper.getInner().newMethod()লিখল — wrapper এখন noise আর bypassed একসাথে। (tunnelling সর্বত্র থাকলে Remove Middle Man-ও বিবেচনা করো: হয়তো client-রা সরাসরি delegate রাখুক।) - সত্যিকারের পার্থক্যগুলো ক্ষুদ্র আর ডুবে আছে। একটা cached method, একটা logged method — আর চারপাশে বিশটা forwarder। এই refactoring-এর পর, class body-তে শুধু সেই দুটো interesting method থাকবে।
- wrapper দুটো পরীক্ষায় পাস করে। Is-a সত্য, coverage প্রায়-সম্পূর্ণ। দুটো ছাড়া, থামো — নিচে দেখো।
কখন ব্যবহার করবে না:
- আংশিক coverage। wrapper যদি delegate-এর interface-এর শুধু একটা অংশ forward করে, inherit করলে অবাঞ্ছিত বাকি অংশ ঢুকে আসবে — একটা নতুন Refused Bequest তৈরি হবে। আগের refactoring ঠিক এরকম বাছাই করা অংশ তৈরি করতে আছে; forwarding বিরক্তিকর লাগলেই বুলডোজ করো না। forwarder-গুলো হলো বেড়া, আর এই বেড়া load-bearing।
- swap-ability দরকার। tests যদি fake delegate inject করে, বা production implementation swap করে, তাহলে field অদৃশ্য কিন্তু মূল্যবান কাজ করছে। Inheritance পছন্দ স্থায়ীভাবে fix করে দেয় — fragile base class সমস্যা আর compile-time coupling আবার ফিরে আসে।
- Multiple delegate। Single inheritance সর্বোচ্চ একটা absorb করতে পারে। বাকিগুলো field হিসেবেই থাকবে।
- delegate
sealed/final, বা constructor satisfy করা যাচ্ছে না। Unsubclassable মানে অকার্যকর — আর প্রায়ই মানে লেখক তোমাকে না করে দিয়েছেন। - wrapper নিজেই থাকার যোগ্য না। inherit করার পর যদি subclass কিছুই যোগ না করে, সৎ শেষ অবস্থা হলো কোনো class নেই — Collapse Hierarchy বা Remove Middle Man কাজ শেষ করে। কিছু-না-করা class হলো Lazy Class নতুন পোশাকে। আর forwarder-গুলোতে delegate logic-এর ছোট copied snippet থাকলে, সেই Duplicate Code আগে পরিষ্কার করো যাতে diff সৎ থাকে।
তিনটা দেখতে একরকম পরিস্থিতি, তিনটা ভিন্ন সমাধান — এগুলো আলাদা রাখো:
| পরিস্থিতি | মূল পর্যবেক্ষণ | সঠিক পদক্ষেপ |
|---|---|---|
| Wrapper সবকিছু forward করে, is-a সত্য, কয়েকটা real override যোগ করে | Forwarding pure waste | Replace Delegation with Inheritance (এই পাঠ) |
| Wrapper সবকিছু forward করে আর কিছুই যোগ করে না, client-রা সরাসরি যেতে পারে | wrapper type-টাই অর্থহীন | Remove Middle Man |
| Wrapper ইচ্ছাকৃতভাবে একটা অংশ forward করে, বিপজ্জনক operation লুকায় | বেড়া load-bearing | Delegation রাখো (আগের পাঠের output) |
| Wrapper inherit করার পর conversion-এ খালি হয়ে গেছে | একটা concept, দুটো class | Collapse Hierarchy |
এক নজরে আগে এবং পরে
ধরো একটা notification sender আছে যা team-এর standard Mailer wrap করে, প্রায় সবকিছু forward করে, শুধু একটা method-এ আলাদা:
// BEFORE: a middle man — five methods, four pure forwarders
class Mailer {
connect(): void { /* SMTP handshake */ }
disconnect(): void { /* ... */ }
send(to: string, subject: string, body: string): void { /* ... */ }
sendBulk(to: string[], subject: string, body: string): void { /* ... */ }
status(): string { return "ready"; }
}
class SchoolMailer {
private inner = new Mailer(); // the delegate
connect(): void { this.inner.connect(); } // pure forwarding
disconnect(): void { this.inner.disconnect(); } // pure forwarding
sendBulk(to: string[], s: string, b: string): void {
this.inner.sendBulk(to, s, b); // pure forwarding
}
status(): string { return this.inner.status(); } // pure forwarding
send(to: string, subject: string, body: string): void {
// the ONE genuine difference: every mail gets the school footer
this.inner.send(to, subject, body + "\n\n— Sunrise Public School");
}
}পরে — চারটা forwarder মুছে গেছে, একটা সৎ override বাকি:
// AFTER: a true subclass — only the genuine difference survives
class SchoolMailer extends Mailer {
override send(to: string, subject: string, body: string): void {
super.send(to, subject, body + "\n\n— Sunrise Public School");
}
// connect(), disconnect(), sendBulk(), status(): inherited, zero upkeep.
// When Mailer gains scheduleSend() next quarter, SchoolMailer
// has it the same day — no forwarding list to maintain.
}class পাঁচটা method থেকে একটায় সংকুচিত হলো। আর যেটা থাকল সেটা pure signal: একটা SchoolMailer একমাত্র যা আলাদাভাবে করে। লক্ষ্য করো honesty check আগে পাস হয়েছে: একটা school mailer সত্যিই is a mailer — প্রতিটা operation অর্থবহ, যেকোনো জায়গায় একটা Mailer expected হলে substitute করা যায়, পুরো contract চাওয়া হয়।
একটা bulk email আগের জগতে কীভাবে যেত দেখো — বাঁয়ের প্রতিটা arrow হলো কোডের একটা লাইন যা কাউকে লিখতে এবং sync রাখতে হয়েছিল:
ধাপে ধাপে, নিরাপদ উপায়ে 🪜
ক্রম গুরুত্বপূর্ণ: আগে verify করো, তারপর rewire করো, শেষে delete করো।
ধাপ ১: দুটো শর্ত verify করো — লিখে। একটা কলামে delegate-এর public method তালিকা করো আর আরেকটায় wrapper-এর। Coverage: মূলত সবকিছু কি forward হচ্ছে? Is-a: প্রতিটা inherited operation কি wrapper-এর উপর অর্থবহ হবে, যেকোনো client call করলে, যেকোনো ক্রমে? Delegate subclassable (sealed/final নয়) আর কোনো test বা config delegate অন্য implementation-এ swap করে না — এটাও নিশ্চিত করো। কিছু fail করলে থামো — forwarding হলো ডিজাইন।
ধাপ ২: wrapper-কে subclass বানাও — field রেখে। কিছু না মুছে extends Delegate যোগ করো। class সাময়িকভাবে দুটো সম্পর্ক রাখে; সবকিছু compile হয় আর একইভাবে কাজ করে।
ধাপ ৩: field-কে this-এ point করো। delegate field-কে নিজের object-এর দিকে refer করাও। প্রতিটা forwarder যেমন this.inner.connect() এখন effectively inherited method call করে। আচরণ অপরিবর্তিত — test চালাও — কিন্তু আলাদা inner object ছবি থেকে চলে গেছে:
class SchoolMailer extends Mailer {
private inner: Mailer = this; // transitional: forwarding to ourselves
connect(): void { this.inner.connect(); } // = inherited connect()
send(to: string, s: string, b: string): void {
this.inner.send(to, s, b + "\n\n— Sunrise Public School");
}
// ...
}(একটা language quirk: এই transitional অবস্থায় একটা pure forwarder যেমন connect() inherited method override করে আর this.inner-এর মাধ্যমে নিজেকে call করে — TypeScript আর C# এটা ঠিকঠাক resolve করে। কিন্তু এই অবস্থায় বেশিক্ষণ থেকো না; শুধু diff reviewable রাখতে এই step আছে।)
ধাপ ৪: pure forwarder একটা একটা করে মুছো। প্রতিটা method যার body শুধু field call করত সেটা সরাও; inheritance তাৎক্ষণিকভাবে সেটা দেয়। প্রতিটা deletion-এর পর compile করো আর test করো। diff method by method গল্প বলে।
ধাপ ৫: সত্যিকারের পার্থক্যগুলোকে override-এ রূপান্তর করো। যে method-গুলো শুধু forward করার চেয়ে বেশি করেছিল — footer যোগ করেছিল, value cache করেছিল, call log করেছিল — সেগুলো থাকে, this.inner.method(...)-এর বদলে super.method(...) call করতে rewritten। override চিহ্নিত করো যাতে compiler signature verify করে।
ধাপ ৬: field সরাও আর construction ঠিক করো। inner field আর delegate তৈরি বা গ্রহণ করা যেকোনো constructor wiring মুছে দাও। এটাই সেই ধাপ যেখানে real fallout আছে: কোড যা inject করে একটা existing delegate instance (new SchoolMailer(existingMailer)) আর তা করতে পারবে না — আচরণ এখন একটা captured object-এর চারপাশে নয়, in-place থাকে। সেই call site-গুলো update করো subclass সরাসরি construct করতে, আর full suite চালাও।
ধাপ ৬ এই রিফ্যাক্টরিং-এর একটা সত্যিকারের আচরণ পরিবর্তন লুকিয়ে রাখে: wrapper আর delegate দুটো আলাদা object থাকে না। আগে SchoolMailer আর তার inner Mailer-এর আলাদা identity ছিল — অন্য কোড সেই একই inner Mailer-এর reference রাখতে পারত আর shared state দেখতে পারত। পরে, একটাই object আছে। পুরনো delegate যদি shared ছিল — singleton connection, registered listener, বাইরে থেকে দেওয়া instance — তাহলে inherit করা নীরবে সেটা un-share করবে। field মুছার আগে প্রতিটা constructor call site audit করো inject করা delegate-এর জন্য — inject করা delegate হলো সবচেয়ে শক্তিশালী চিহ্ন যে composition রাখা উচিত।
একটা বড় বাস্তব উদাহরণ 💳
ধরো একটা payments team বছর আগে তাদের gateway client wrap করেছিল "একদিন logging যোগ করবে বলে।" সেই একদিন পুরোপুরি আসেনি: একটা method logging পেল, এগারোটা forwarder হয়ে গেল। প্রতিটা gateway SDK update মানে নতুন pass-through লেখা। গত মাসে refund দেরি হলো কারণ wrapper-এ refundPartial ছিল না আর QA পর্যন্ত কেউ খেয়াল করেনি।
// BEFORE: twelve methods, eleven of them noise
class GatewayClient {
authorize(card: Card, amount: number): AuthResult { /* ... */ return ok(); }
capture(authId: string): void { /* ... */ }
refund(paymentId: string): void { /* ... */ }
refundPartial(paymentId: string, amount: number): void { /* ... */ }
voidAuth(authId: string): void { /* ... */ }
balance(): number { return 0; }
// ... six more operations
}
class LoggingGatewayClient {
private gateway = new GatewayClient();
authorize(card: Card, amount: number): AuthResult {
console.log(`AUTH attempt: Rs.${amount}`); // the real behaviour
const result = this.gateway.authorize(card, amount);
console.log(`AUTH result: ${result.status}`);
return result;
}
capture(id: string): void { this.gateway.capture(id); } // forward
refund(id: string): void { this.gateway.refund(id); } // forward
refundPartial(id: string, amt: number): void {
this.gateway.refundPartial(id, amt); // forward
}
voidAuth(id: string): void { this.gateway.voidAuth(id); } // forward
balance(): number { return this.gateway.balance(); } // forward
// ... six more forwarders, added whenever someone remembers
}checklist চালাই: is-a — একটা logging gateway client is a gateway client, প্রতিটা operation অর্থবহ, যেকোনো জায়গায় substitute করা যায় কারণ logging caller-দের কাছে অদৃশ্য। Coverage — সম্পূর্ণ; wrapper gateway হতেই আছে, plus একটা logged method। delegate subclassable, test-এ কখনো swap হয় না কারণ তারা নিচের network layer fake করে। দুটো পরীক্ষা পাস; দাঁড়িপাল্লা বলে inherit করো:
// AFTER: one override; eleven methods inherited; zero maintenance
class LoggingGatewayClient extends GatewayClient {
override authorize(card: Card, amount: number): AuthResult {
console.log(`AUTH attempt: Rs.${amount}`);
const result = super.authorize(card, amount);
console.log(`AUTH result: ${result.status}`);
return result;
}
// capture, refund, refundPartial, voidAuth, balance, and the
// other six: inherited. The "missing refundPartial" bug class
// is extinct — new SDK methods arrive automatically.
}class এখন এগারোটা method ছোট। আর যে একটা method বাকি সেটাই ঠিক class-এর থাকার কারণ। যে bug class refund দেরি করাল — forwarder এখনও লেখা হয়নি — এখন structurally অসম্ভব। SDK-এর release history জুড়ে team-কে কতটা বাড়তি কাজ করতে হতো সেটা plot করলে দেখা যায়:
একটা সৎ কথা বলে রাখি: পরের বছর যদি team সিদ্ধান্ত নেয় logging দ্বিতীয় gateway-তেও লাগবে, বা runtime-এ toggle করা যাবে, দাঁড়িপাল্লা আবার ঝুঁকবে — একটা decorator (আবার composition) আরও ভালো আকার হবে, আর Replace Inheritance with Delegation ফেরার যাত্রা করবে। বছরের পর বছর উভয় দিকে দাঁড়িপাল্লা চালানো অস্থিরতা নয়; এটা reality track করা ডিজাইন।
C#-এ একই রিফ্যাক্টরিং 🟣
ক্লাসিক clock উদাহরণ। একটা test-friendly clock wrapper শেষপর্যন্ত একটা cached method ছাড়া সবকিছু forward করছিল:
// BEFORE: a middle man around the system clock
public class SystemClock
{
public virtual DateTime Now() => DateTime.Now;
public virtual DateTime UtcNow() => DateTime.UtcNow;
public virtual long Ticks() => DateTime.Now.Ticks;
public virtual TimeZoneInfo Zone() => TimeZoneInfo.Local;
public virtual DateTime Today() => DateTime.Today;
}
public class CachedClock
{
private readonly SystemClock _inner = new();
private DateTime? _today;
public DateTime Now() => _inner.Now(); // pure forwarding
public DateTime UtcNow() => _inner.UtcNow(); // pure forwarding
public long Ticks() => _inner.Ticks(); // pure forwarding
public TimeZoneInfo Zone() => _inner.Zone(); // pure forwarding
public DateTime Today() // the one real method
=> _today ??= _inner.Today();
}// AFTER: a true subclass; the cache is the whole class body
public class CachedClock : SystemClock
{
private DateTime? _today;
public override DateTime Today()
=> _today ??= base.Today();
// Now(), UtcNow(), Ticks(), Zone(): inherited — zero boilerplate.
}C#-নির্দিষ্ট কয়েকটা কথা:
- Parent-এর method-গুলো
virtualহতে হবে genuine পার্থক্যগুলোoverrideহতে। delegate-এর method-গুলো non-virtual হলে, তুমি forwarder-মুক্ত interface inherit করতে পারো কিন্তু আচরণ পরিবর্তন করতে পারো না — এটা sign যে class subclassing-এর জন্য design করা হয়নি। sealedহলো hard stop। একটা sealed delegate extend করা যায় না, সম্পূর্ণ থামো — আর sealing প্রায়ই লেখকের explicit "compose, inherit করো না" বার্তা। সম্মান করো।- Constructor chaining ফিরে আসে।
SystemClock-এ constructor parameter থাকলেCachedClock-কে: base(...)দিয়ে সেগুলো satisfy করতে হবে। parent construct করা না গেলে (factory বা DI container থেকে আসে শুধু), inheritance সরিয়ে রাখো — composition থাকে। - DI registration দেখো। container আগে
SystemClockআরCachedClockআলাদাভাবে register করলে এবং একটা অন্যটার মধ্যে inject করলে, registration একটাCachedClock-এ পরিণত হয় (optionallySystemClockহিসেবে exposed)। এটা ধাপ ৬-এর "দুটো object একটা হয়" নিয়ম dependency-injection পোশাকে।
আর Python-এ একই আকার — যেখানে রূপান্তর আরও সংক্ষিপ্ত কারণ update করার override keyword নেই, শুধু method resolution order কাজ করে:
class GatewayClient:
def authorize(self, card, amount): ...
def capture(self, auth_id): ...
def refund(self, payment_id): ...
# ... nine more operations
class LoggingGatewayClient(GatewayClient):
"""Was a wrapper with eleven forwarders. Now: one override."""
def authorize(self, card, amount):
print(f"AUTH attempt: Rs.{amount}")
result = super().authorize(card, amount)
print(f"AUTH result: {result.status}")
return result
# Everything else: inherited. No forwarding list to maintain.Python-এর সতর্কতা সর্বত্রের মতোই: super() তোমাকে আবার parent-এর internal-এর সাথে weld করে দেয়। GatewayClient.authorize internally self.capture(...) call করতে শুরু করলে, তোমার subclass এখন সেই লুকানো choreography-র অংশ — fragile base class সমস্যা, schedule মতো।
IDE support 🛠️
তার inverse-এর মতো নয়, এই দিকে mainstream IDE-তে one-click automation নেই। কিন্তু ingredient move-গুলো সব assisted:
- IntelliJ IDEA / Rider: হাতে class declaration পরিবর্তন করো, তারপর IDE-কে ভারী কাজ করতে দাও। প্রতিটা pure forwarder-এ Safe Delete নিশ্চিত করে inheritance তার caller-দের cover করে, আর Inline Method যেকোনো forwarder collapse করে যেটার অন্য কোড সরাসরি reference দিত। পুরনো delegate field-এ Find Usages view প্রতিটা constructor আর injection site বের করে field মুছার আগে।
- ReSharper / Visual Studio (C#): Generate → Overriding Members dialog genuine পার্থক্যগুলো
base-এর বিপরীতে rewrite করে, আর ReSharper-এর "Member hides inherited member" / redundancy inspection সেই forwarder highlight করে যেগুলো inheritance এখন যা দেয় তার সাথে identical হয়ে গেছে — delete-with-confidence marker। - VS Code (TypeScript): compiler হলো tool।
extendsযোগ করো, রাখা method-গুলোoverrideচিহ্নিত করো (tsconfig-এnoImplicitOverrideচালু করলে প্রতিটা অসতর্ক shadow compile error হয়), forwarder একটা একটা করে মুছো, আর red squiggles দেখো যেগুলো construction site তালিকা করে।
যেকোনো editor ব্যবহার করো না কেন: verification — is-a, coverage, swap-ability, subclassability — এটা তোমারই করতে হবে। কোনো tool সততা পরীক্ষা করতে পারে না।
উপকারিতা আর ঝুঁকি ⚖️
এই table-টা Replace Inheritance with Delegation-এর পাশে পড়ো — এগুলো বিপরীত দিক থেকে তোলা একই দাঁড়িপাল্লার ছবি। is-a পরীক্ষা আর coverage পরীক্ষা ঠিক করে কোন ছবিটা তোমার class-এর সাথে মেলে।
| উপকারিতা | ঝুঁকি / খরচ |
|---|---|
| হাতে-মেনটেইন করা forwarding method-এর পুরো দেয়াল মুছে দেয় | শুধু near-total coverage সহ true is-a-র জন্য valid — আংশিক ব্যবহার inherit করলে Refused Bequest হয় |
| parent-এ নতুন method স্বয়ংক্রিয়ভাবে আসে — "forgotten forwarder" bug class বিলুপ্ত | tight compile-time coupling আবার weld করে: parent internal এখন তোমাকে ভাঙতে পারে (fragile base class ফিরে আসে) |
| class body pure signal-এ সংকুচিত: শুধু সত্যিকারের আলাদা আচরণ বাকি থাকে | delegate আর swap, এই seam-এ mock, বা runtime-এ বেছে নেওয়া যাবে না |
| দুটোর বদলে একটা object — কম allocation, wrapper আর inner-এর মধ্যে identity confusion নেই | দুটো object একটা হওয়া caller-দের ভাঙে যারা পুরনো inner instance inject বা share করেছিল |
| Middle Man smell সমাধান করে wrapper-কে public face হিসেবে রেখে | Single inheritance: মাত্র একটা delegate কখনো absorb করা যাবে; sealed/final parent সম্পূর্ণ অস্বীকার করে |
কোন smell সারায়? 👃
| Smell | Replace Delegation with Inheritance কীভাবে সাহায্য করে |
|---|---|
| Middle Man | class message-passer হওয়া বন্ধ করে; forwarding method বিনামূল্যে inheritance দিয়ে replace হয় |
| Duplicate Code (structural) | ডজন ডজন একই আকারের one-line forwarder — pattern-এর duplication — সম্পূর্ণ অদৃশ্য হয় |
| Lazy Class (সীমান্ত ক্ষেত্র) | mostly ceremony wrapper হয় meaningful override-এ সংকুচিত হয় অথবা প্রকাশ করে যে থাকারই দরকার নেই |
| Shotgun Surgery (delegate আপডেট) | delegate-এ method যোগ করলে wrapper-এ matching edit করতে হয় না |
| Refused Bequest (সতর্কতা, সমাধান নয়) | coverage পরীক্ষা ছাড়া প্রয়োগ করলে এই refactoring Refused Bequest তৈরি করে — inverse move ক্ষতি ঠিক করে |
দ্রুত revision box 📦
+------------------------------------------------------------------+
| REPLACE DELEGATION WITH INHERITANCE - REVISION CARD |
+------------------------------------------------------------------+
| Problem : class forwards (nearly) EVERY call to a held |
| delegate. Pure boilerplate, silent gaps when the |
| delegate grows, real behaviour buried in noise. |
| (Chhotu turning every task around to Masterji.) |
| |
| Verify : 1. IS-A: substitutable with zero surprises |
| 2. COVERAGE: essentially the WHOLE contract wanted |
| 3. delegate subclassable, single, never injected/ |
| swapped |
| |
| Solution : extends the delegate -> point field at this -> |
| delete pure forwarders one by one -> |
| convert real differences to overrides (super.x) -> |
| remove field + fix construction sites |
| |
| Inverse : Replace Inheritance with Delegation (the seesaw) |
| Cousins : Remove Middle Man (clients go direct), |
| Collapse Hierarchy (if the subclass ends up empty) |
| Trap : partial coverage inherited = NEW Refused Bequest |
+------------------------------------------------------------------+অনুশীলন ✏️
তোমার পালা। ধরো একটা inventory service team-এর storage client wrap করেছে, আর wrapper হয়ে উঠেছে সবার সবচেয়ে অপছন্দের ফাইল:
class StorageClient {
get(key: string): string | null { /* ... */ return null; }
put(key: string, value: string): void { /* ... */ }
delete(key: string): void { /* ... */ }
exists(key: string): boolean { /* ... */ return false; }
listKeys(prefix: string): string[] { /* ... */ return []; }
flush(): void { /* ... */ }
}
class InventoryStore {
private client: StorageClient;
constructor(client?: StorageClient) {
this.client = client ?? new StorageClient(); // sometimes injected!
}
get(key: string): string | null { return this.client.get(key); }
delete(key: string): void { this.client.delete(key); }
exists(key: string): boolean { return this.client.exists(key); }
listKeys(prefix: string): string[] { return this.client.listKeys(prefix); }
flush(): void { this.client.flush(); }
put(key: string, value: string): void {
if (key.startsWith("sku:") === false) {
throw new Error("Inventory keys must start with sku:"); // real behaviour
}
this.client.put(key, value);
}
}কাজ করো:
১. coverage পরীক্ষা চালাও (forwarder বনাম মোট interface গণনা করো) আর is-a পরীক্ষা। একটায় একটু বেশি ভাবতে হবে: put এমন key প্রত্যাখ্যান করে যা parent গ্রহণ করত। একটা store যা কিছু valid parent input ছুঁড়ে দেয় সে কি "শূন্য বিস্ময় নিয়ে" substitute করা যায়? দুই বাক্যে তোমার রায় লেখো — এটা এই পৃষ্ঠার সবচেয়ে কঠিন আর মূল্যবান প্রশ্ন। তারপর চিত্র ৩-এর মানচিত্রে InventoryStore রাখো।
২. constructor investigate করো: client কখনো কখনো inject করা হয়। (কাল্পনিক) codebase search করো: unit test একটা fake StorageClient pass করে। এটা তোমাকে কী বলে — class-টা দাঁড়িপাল্লার কোন দিকে?
৩. ধরো team সিদ্ধান্ত নেয় put-এর validation নিচে একটা নতুন StorageClient.putValidated hook-এ যাবে, test network layer fake করবে, আর injection সরানো হবে। এখন চিত্র ৭-এর state-গুলো দিয়ে রূপান্তর চালাও: extends, field থেকে this, পাঁচটা forwarder একটা করে test-run করে মুছো, put-কে super.put call করা override-এ রূপান্তর করো, field আর constructor সরাও।
৪. রূপান্তরের পর InventoryStore-এ ঠিক একটা method আছে। এই series তোমাকে যে follow-up প্রশ্ন শিখিয়েছে সেটা জিজ্ঞেস করো: একটা এক-override subclass কি থাকার যোগ্য, নাকি পরবর্তী refactoring Collapse Hierarchy? এক বাক্যে উত্তর দাও।
৫. Bonus: তোমার teammate বলে "আসো ReportStore আর UserStore-কেও StorageClient inherit করাই, তারাও অনেক forward করে।" UserStore শুধু get আর put forward করে আর ইচ্ছাকৃতভাবে delete লুকায়। প্রতিটা store আসলে কোন refactoring দরকার, আর কেন উত্তরগুলো আলাদা?
যদি তোমার ধাপ ১-এর রায় সৎভাবে এটার সাথে লড়াই করে — "parent গ্রহণ করে এমন input-এ throw করা substitutability দুর্বল করে, তাই is-a questionable যতক্ষণ না সেই নিয়ম parent-এর নিজের contract-এ যায়" — তাহলে তুমি শুধু দুটো refactoring নয়, তাদের মধ্যের পুরো দাঁড়িপাল্লাটাই বুঝেছ। এই বিচার — mechanics নয় — হলো আসল দক্ষতা। ওস্তাদ জামাল রহিমকে পাঁচ বছর দেখার পর promote করেছিলেন; তোমার wrapper-দেরও একই ধৈর্যে promote করো। শাবাশ।
সচরাচর জিজ্ঞাসা
- 'composition over inheritance' নিয়মের বিরুদ্ধে না এই রিফ্যাক্টরিং?
- না — এটা সেই নিয়মকে পূর্ণ করে। নীতিটা বলে composition-কে প্রাধান্য দাও, সবসময় composition ব্যবহার করো এমন না। যখন একটা wrapper সত্যিই তার delegate-এর একটা ধরন, মূলত পুরো contract support করে, আর এই indirection থেকে কিছুই পাচ্ছে না — তখন inheritance হলো সৎ আর সাশ্রয়ী ডিজাইন। নীতিটা default ঠিক করে দেয়; এই রিফ্যাক্টরিং প্রমাণিত ব্যতিক্রম সামলায়।
- কীভাবে বুঝব wrapper inheritance-এর যোগ্য?
- দুটো শর্ত একসাথে মিলতে হবে। প্রথমত is-a পরীক্ষা: wrapper যেকোনো জায়গায় delegate-এর বিকল্প হতে পারবে, কোনো বিস্ময় ছাড়াই। দ্বিতীয়ত coverage: wrapper মূলত delegate-এর পুরো public interface forward করে, কোনো বাছাই করা অংশ নয়। যদি শুধু একটা অংশ forward করে, তাহলে inherit করলে অবাঞ্ছিত বাকি অংশ Refused Bequest হয়ে ঢুকবে — delegation রেখে দাও।
- এই রিফ্যাক্টরিং আর Remove Middle Man-এর মধ্যে পার্থক্য কী?
- দুটোই এমন class আক্রমণ করে যেটা forwarding method-এ ডুবে আছে। Remove Middle Man wrapper-এর pass-through মুছে দেয় আর client-দের সরাসরি delegate-এর সাথে কথা বলতে দেয় — wrapper সরে যায়। Replace Delegation with Inheritance wrapper-কে public face হিসেবে রাখে কিন্তু subclass বানায়, তাই forwarding বিনামূল্যে inheritance হয়ে যায়। সিদ্ধান্ত নাও এটা ভেবে — wrapper আসলেই একটি type হিসেবে থাকার দরকার আছে কিনা।
- class যদি দুটো ভিন্ন object-এ delegate করে?
- Single inheritance একটাই absorb করতে পারে। সেই delegate বেছে নাও যার পুরো contract class সত্যিই support করে — যদি কোনোটা করে — আর সেটা থেকে inherit করো; অন্য collaborator field হিসেবে থাকবে। আর যদি কোনো delegate পুরো is-a-plus-coverage পরীক্ষায় পাস না করে, তাহলে forwarding, যদিও বিরক্তিকর, সঠিক ডিজাইন।
- delegate class sealed বা final হলে কি এই রিফ্যাক্টরিং করা যাবে?
- না — sealed বা final class subclass করা যায় না, আর এটা প্রায়ই লেখকের ইচ্ছাকৃত বার্তা যে inheritance তার design contract-এর অংশ ছিল না। delegation রেখে দাও, অথবা boilerplate সত্যিই কষ্টের হলে forwarder-গুলোর জন্য Remove Middle Man বা code-generation দেখো।
আরো দেখো
সম্পর্কিত পাঠ
Middle Man: যে helper শুধু তোমার message পৌঁছে দেয়, নিজে কিছু করে না
Middle Man code smell টা বোঝো একটা school-এর সেই পিয়নের গল্প দিয়ে — যে শুধু চিরকুট বহন করে, নিজে কিছু যোগ করে না। যখন একটা class শুধু সব call forward করে, সেটা সরিয়ে দাও। কিন্তু Proxy, Facade, আর Adapter কেন জেনেশুনে middle man হয় — সেটাও জানো।
Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি
Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।
Replace Inheritance with Delegation: কাউন্টার ভাড়া নাও, দোকান উত্তরাধিকারে নিও না
Replace Inheritance with Delegation রিফ্যাক্টরিং শেখো একটা মিষ্টির দোকানের গল্প দিয়ে — composition over inheritance-এর আসল মানে, fragile base class সমস্যা, আর TypeScript ও C#-এ ধাপে ধাপে রূপান্তর।
Remove Middle Man: পিয়ন শুধু ফরওয়ার্ড করলে, সরাসরি হেড স্যারের কাছে যাও
Remove Middle Man রিফ্যাক্টরিং শেখো একটা স্কুলের পিয়নের গল্প দিয়ে — যে প্রতিটা প্রশ্ন হেডমাস্টারের কাছে ফরওয়ার্ড করে, নিজে কিছু যোগ না করেই। যখন একটা class শুধু delegate-কে call ফরওয়ার্ড করে, তখন সেই ফরওয়ার্ডিং মুছে দাও আর client-দের সরাসরি delegate-এর সাথে কথা বলতে দাও। TypeScript আর C#-এ ধাপে ধাপে walkthrough।