Replace Inheritance with Delegation: কাউন্টার ভাড়া নাও, দোকান উত্তরাধিকারে নিও না
Replace Inheritance with Delegation রিফ্যাক্টরিং শেখো একটা মিষ্টির দোকানের গল্প দিয়ে — composition over inheritance-এর আসল মানে, fragile base class সমস্যা, আর TypeScript ও C#-এ ধাপে ধাপে রূপান্তর।
🍬 যে ছেলে মিষ্টির দোকান উত্তরাধিকারে পেল
ধরো পুরান ঢাকার চকবাজারে জামালদের পরিবারের বিখ্যাত মিষ্টির দোকান — জামাল মিষ্টান্ন ভান্ডার। তিন প্রজন্ম ধরে চলছে। দোকানটা একটা পুরো জগৎ: ভোর চারটায় রসগোল্লা বানানোর কারিগর, উৎসবের অর্ডার বই, সরবরাহকারীর খাতা, ঋণের রেজিস্টার, লোহার আলমারিতে তালাবদ্ধ গোপন সন্দেশের রেসিপি। আর সামনে একটা দারুণ বিলিং কাউন্টার — ক্যাশ ড্রয়ার, কার্ড মেশিন, প্রিন্টেড-বিল সিস্টেম। খদ্দেররা মিষ্টির মতোই বিলের প্রশংসা করে: আইটেম ধরে ধরে, দ্রুত, কোনো ভুল নেই।
এখন পরিচয় হও সালাম-এর সাথে। সে জামালের ছোট ছেলে। মিষ্টি বানাতে চায় না — সে কলেজ গেটের পাশে একটা ছোট চাট স্টল চালাতে চায়। ফুচকা, ঝালমুড়ি, পাপড়ি চাট। তার নিজের কিছু।
রবিবারের দুপুরের খাবারে চাচা তারিক বড় একটা প্রস্তাব দেন: "বাবা, পারিবারিক ব্যবসা উত্তরাধিকারে নাও! সব তোমার হয়ে যাবে।" কিন্তু মনোযোগ দিয়ে শোনো — সব মানে কী? সালাম যদি ব্যবসা উত্তরাধিকারে নেয়, সে সবটাই পাবে। ভোর চারটার কিচেন শিফট, উৎসবের রসগোল্লার অর্ডার যা এখন তার কাছে আসবে, সরবরাহকারীর ঋণ, গোপন রেসিপির দায়িত্ব। জামালের নাম চেনে এমন খদ্দেররা সালামের চাট স্টলে এসে বিয়ের জন্য দুইশো সন্দেশ চাইবে — কারণ কাগজে-কলমে সে এখন মিষ্টির দোকান। বিয়ের পার্টি এলে সালাম কী করবে? একটা সাইনবোর্ড লাগাবে: "আমরা এখানে মিষ্টি বানাই না!" একটা স্টল নিজের উত্তরাধিকারের বিরুদ্ধেই লড়ছে। সালাম একটাই জিনিস চেয়েছিল — সেই দারুণ বিলিং কাউন্টার — আর উত্তরাধিকার পুরো ব্যবসাটাই তার উপর চাপিয়ে দেয়।
সালাম বুদ্ধিমানের কাজ করে। সে চাচার কাছে ফিরে বলে: "চাচা, আমি মিষ্টির দোকান হতে চাই না। আমাকে শুধু বিলিং কাউন্টারটা ভাড়া দাও আমার স্টলের জন্য।" তারিক চাচা হাসেন, হাত মেলান, একটা ছোট মাসিক ভাড়ায় রাজি হয়ে যান।
এখন এই ব্যবস্থাটা দেখো। কাউন্টারটা সালামের স্টলের ভেতরে বসে তার পেমেন্ট নিখুঁতভাবে সামলায়। কেউ তার কাছে রসগোল্লা অর্ডার করতে পারে না — সেই দরজা তার স্টলে নেই। বিয়ের পার্টি কখনো আসে না, কারণ স্টলের কোথাও দাবি নেই যে এটা মিষ্টির দোকান। আগামী বছর বাজারে আরও ভালো ডিজিটাল বিলিং মেশিন এলে, সালাম পুরনো কাউন্টার ফেরত দিয়ে নতুনটা ভাড়া নেয়। তারিক চাচা কিছুই মনে করবেন না। সালাম পারিবারিক ব্যবসার সাথে আটকে নেই — সে শুধু একটা ভালো-বানানো অংশ ব্যবহার করছে, নিজের শর্তে।
সালাম "is-a মিষ্টির দোকান" থেকে "has-a বিলিং কাউন্টার"-এ পরিবর্তন করল। এই একই পদক্ষেপ হলো আজকের refactoring: Replace Inheritance with Delegation।
Replace Inheritance with Delegation কী? 🔁
এই refactoring একটা inheritance link বাতিল করে যেটা কখনো হওয়া উচিত ছিল না। একটা class অন্যটাকে extend করে শুধু কিছু code ধার নিতে — সত্যিকারের অর্থে সেই class-এর একটা ধরন বলে নয়। সমাধান হলো: extends সরাও, আগের parent-এর একটা instance একটা private field-এ রাখো, আর ছোট ছোট method লেখো যা সেখানে delegate (ফরওয়ার্ড) করে — কিন্তু শুধু সেই operations-এর জন্য যেগুলো তুমি আসলে offer করতে চাও।
Inheritance version কেন এত ক্ষতিকর? কারণ inherit করা একটা সব-বা-কিছুই-না চুক্তি। Subclass parent-এর পুরো public surface পায়, চাও বা না চাও। তিনটা খারাপ ঘটনা ঘটে:
- Leaked operations। Client যেকোনো inherited method call করতে পারে — এমনগুলোও যা তোমার class-এর নিয়ম ভাঙে। ক্লাসিক বিপর্যয়:
class Stack extends ArrayList। প্রতিটি callerstack.add(0, item)বাstack.clear()বাstack.get(5)করতে পারে, stack-এর নিয়ম বাইরে থেকে ভেঙে দিচ্ছে। Class নিজের invariant রক্ষা করতে পারে না, কারণ বিপজ্জনক দরজাগুলো কার্যকর দরজাগুলোর সাথেই উত্তরাধিকারে এসেছে। - পাঠকদের কাছে মিথ্যা প্রতিশ্রুতি।
extendsএকটা public দাবি: "আমি আমার parent-এর একটা ধরন — আমাকে যেকোনো জায়গায় ব্যবহার করো।" যখন সেই দাবি মিথ্যা, তখন প্রতিটি পাঠক আর প্রতিটি type checker বিভ্রান্ত হয়। এটা হলো Refused Bequest smell: উত্তরাধিকার গ্রহণ করছ কিন্তু বেশিরভাগ চুপচাপ প্রত্যাখ্যান করছ। - Fragile base class সমস্যা। Subclass শুধু parent-এর interface-এর সাথে না, তার implementation-এর সাথেও জুড়ে আছে। Parent-এর লেখক যদি methods একে অপরকে ভেতরে কীভাবে call করে তা বদলান — ধরো,
addAllআর loop-এaddcall করে না — সেই লুকানো choreography-র উপর নির্ভর করা subclass override চুপচাপ ভেঙে যায়। যদিও parent-এর public আচরণ কখনো পরিবর্তন হয়নি। Joshua Bloch-এর Effective Java (Item 18) একটাHashSetsubclass দিয়ে এটা দেখিয়েছে যা শুধু parent-এর internal self-call-এর কারণে element দ্বিগুণ গণনা করে। আর তার উপসংহার হলো সেই বিখ্যাত নীতি যা আমরা আজ শেখাচ্ছি।
Delegation তিনটা একসাথে ঠিক করে। Field private, তাই কিছু leak হয় না: client ঠিক সেই method-ই দেখে যা তুমি লিখেছ, আর কিছু নয়। আত্মীয়তার কোনো public দাবি নেই, তাই কেউ বিভ্রান্ত হয় না। আর তোমার class শুধু delegate-এর public contract-এর উপর নির্ভর করে, কখনো তার internals-এর উপর নয় — delegate-এ ভেতরের পরিবর্তন তোমার কাছে পৌঁছাতে পারে না।
এক লাইনে সারসংক্ষেপ: যখন একটা class parent extend করে শুধু code পুনরায় ব্যবহার করতে — সত্যিকারের parent-এর একটা ধরন বলে নয় — inheritance সরাও, parent-কে একটা private field-এ রাখো, আর শুধু সেই calls ফরওয়ার্ড করো যা তুমি সত্যিই offer করতে চাও। একটা মিথ্যা is-a-কে সৎ has-a-তে পরিণত করো।
Favor composition over inheritance — সৎ সংস্করণ 🪞
এই refactoring হলো object-oriented programming-এ সবচেয়ে বেশি উদ্ধৃত design নীতির হাতে-কলমে রূপ: favor composition over inheritance। এটাকে সৎভাবে শেখানো দরকার, কারণ নীতিটা প্রায়ই তার কারণ বা সীমা ছাড়াই বলা হয়।
কেন composition নিরাপদ default। Inheritance হলো দুটো class-এর মধ্যে সবচেয়ে শক্ত coupling: পুরো interface, পুরো implementation, দৃশ্যমান protected internals, স্থায়ী আর compile time-এ নির্ধারিত। Composition কাঠামোগতভাবেই আলগা। তুমি ঠিক করো কোন operations expose করবে, delegate একটা private field-এর পেছনে লুকিয়ে থাকে, আর তুমি এটা বদলাতে পারো — একটা subclass-এর জন্য, test-এ mock, বা interface-এর পেছনে সম্পূর্ণ ভিন্ন implementation — তোমার public face না বদলেও। সালাম যেকোনো বছর বিলিং মেশিন বদলাতে পারে; সত্যিকারের উত্তরসূরি দাদা বদলাতে পারে না।
কেন নীতিটা "favor" বলে, "always" নয়। Inheritance হলো সঠিক tool যখন দুটো শর্ত একসাথে পূরণ হয়: সম্পর্কটা একটা সত্যিকারের is-a (substitution test পাস — child যেকোনো জায়গায় parent-এর পরিবর্তে দাঁড়াতে পারে, শূন্য অবাক করা ঘটনা নিয়ে), আর child সত্যিই parent-এর মূলত পুরো contract চায়। একটা SavingsAccount যা সত্যিই একটা Account, প্রতিটি account operation অর্থপূর্ণভাবে support করে, আর উপরে সুদ যোগ করে — সেখানে inheritance তার কাজ ঠিকঠাক করছে, শূন্য forwarding boilerplate নিয়ে। এটা সরানো হবে ideology, engineering নয়। পরের পাঠ Replace Delegation with Inheritance ঠিক সেই দিনের জন্য আছে যখন পাল্লা অন্য দিকে ঝোঁকে।
তাই নীতিটা পুরোপুরি বললে: has-a-তে default করো; is-a-র জন্য দাম দাও শুধু যখন এটা সত্য। পাশাপাশি, দুটো সম্পর্ক বিপরীত মুদ্রায় লেনদেন করে:
| বৈশিষ্ট্য | Inheritance (is-a) | Delegation (has-a) |
|---|---|---|
| Caller-দের কাছে expose করা surface | Parent-এর পুরো public interface, চাও বা না চাও | ঠিক সেই forwarding method যা তুমি লিখতে বেছেছ |
| Coupling | Interface + implementation + protected internals | শুধু delegate-এর public contract |
| Helper বদলানো বা mock করা যাবে? | না — runtime-এ parent বদলাতে পারবে না | হ্যাঁ — ভিন্ন instance, subclass, বা test fake |
| Boilerplate | শূন্য forwarder; সবকিছু বিনামূল্যে আসে | প্রতিটি offered operation-এর জন্য একটা ছোট method |
| নতুন parent/delegate methods | তোমার উপর স্বয়ংক্রিয়ভাবে আসে (এমনকি অযাচিত) | শুধু যখন তুমি একটা forwarder যোগ করো (এমনকি চাওয়া) |
| পাঠকদের কাছে দাবি | "আমি parent-এর একটা ধরন — অবাধে বদলাও" | "আমি এই object-কে একটা অংশ বা হাতিয়ার হিসেবে ব্যবহার করি" |
| কখন ভাঙে | Parent internals পরিবর্তন হলে (fragile base class) | Delegate-এর public contract পরিবর্তন হলে (বিরল, দৃশ্যমান) |
কলেজ কর্নার: has-a বনাম is-a পার্থক্যটা নীতির চেয়ে পুরনো আর আনুষ্ঠানিকভাবে বলার যোগ্য। Is-a (inheritance, subtyping) হলো substitutability সম্পর্কে একটা দাবি: child-এর প্রতিটি instance আচরণগতভাবে parent-এর একটা instance — এটা Liskov Substitution Principle শুধু পরীক্ষার উত্তর হিসেবে নয়, design test হিসেবে। Has-a (composition, aggregation) হলো কাঠামো সম্পর্কে একটা দাবি: object-টা অন্য একটা object-কে একটা অংশ বা হাতিয়ার হিসেবে ধারণ করে বা ব্যবহার করে। ছাত্রছাত্রীরা যে ফাঁদে পড়ে তা হলো is-a পরীক্ষা ইংরেজি বাক্যে করা, আচরণে নয়। "A square is a rectangle" ইংরেজিতে ঠিক শোনায় — কিন্তু একটা mutable Square extends Rectangle ভেঙে পড়ে যখন একজন caller width আর height আলাদাভাবে set করে। Substitution test সবসময় আচরণগত: "আমি কি এটা বাক্যে বলতে পারি?" নয়, বরং "parent-এর প্রতিটি caller কি child পেয়ে কখনো অবাক হবে না?"
কখন দরকার? 🔍
একটা inheritance link এই চিকিৎসার যোগ্য কিনা বোঝার লক্ষণ:
- Subclass parent-এর শুধু একটা অংশ ব্যবহার করে। তিনটা inherited method call করে আর ত্রিশটা উপেক্ষা করে। উদ্দেশ্য ছিল code reuse, আত্মীয়তা নয়। এটা Refused Bequest-এর হালকা রূপ — আর এটা খুব কমই হালকা থাকে।
- "Not supported" override আছে। সবচেয়ে জোরে বেজে ওঠা অ্যালার্ম: subclass inherited method override করে
NotSupportedExceptionthrow করতে বা অর্থহীন কিছু return করতে, নিজের parent-এর বিরুদ্ধে সক্রিয়ভাবে লড়ছে। Class চিৎকার করে বলছে is-a মিথ্যা। - Inherited method ক্লাসের নিয়ম ভাঙতে পারে।
Stack extends ArrayList-এর মতো — যেকোনো caller একটা inherited দরজা দিয়ে invariant লঙ্ঘন করতে পারে। যদি "এতে সরাসরি add() call করো না" এরকম defensive মন্তব্য পাও, design ইতিমধ্যে হেরে গেছে। - Substitution test ব্যর্থ হয়। Parent type-এর বিরুদ্ধে লেখা সৎ code-এ একটা child object দাও। যদি কিছু অবাক করা ঘটতে পারে, inheritance type system-এর কাছে মিথ্যা বলছে।
- Parent একটা utility grab-bag।
class OrderService extends BaseHelperশুধুformatDate()আরlog()পৌঁছাতে — inheritance একটা import statement হিসেবে ব্যবহার। Delegation (বা সাধারণ import) সৎ আকৃতি। - Parent upgrade বারবার তোমায় ভাঙছে। তুমি extend করা framework class-এর প্রতিটি নতুন version তোমার subclass-এ মাসিক fix করতে বাধ্য করছে — fragile base class সমস্যা নিয়মিত আসছে।
আর কখন ব্যবহার করবে না:
- Is-a সত্য আর পুরো contract চাওয়া হয়েছে। পূর্ণ substitutability সহ সত্যিকারের specialization হলো inheritance-এর স্বাভাবিক জায়গা। এটা একা থাকতে দাও।
- শুধু এক বা দুটো parent method misplaced। হয়তো parent ভুল, link নয় — Push Down Method হয়তো hierarchy আরও সস্তায় ঠিক করবে।
- Subclass এমনিতেও প্রায় খালি। Child যদি কিছুই না যোগ করে, সমাধান হতে পারে Collapse Hierarchy — delegate করা নয়, merge করা। যে class কিছু অর্জন করে না সে Lazy Class; যে class সবকিছু ফরওয়ার্ড করে সে Middle Man-এর পথে। এই refactoring সেই দুই খাড়া পাহাড়ের মাঝে থাকে। আর "reuse হারানো"-র ভয় পেও না: Duplicate Code কখনো আসে না, কারণ delegate এখনো shared logic ঠিক একবার রাখে — তুমি একটা bloodline-এর পরিবর্তে একটা field-এর মাধ্যমে এটা পুনরায় ব্যবহার করো।
যে audit সিদ্ধান্ত নেয় তা আক্ষরিকভাবে একটা গণনা হতে পারে। Subclass আসলে parent-এর কতটা ব্যবহার করে? সালামের স্টল পারিবারিক ব্যবসার ঠিক billing অংশটা ব্যবহার করেছিল:
একই গণনা তুলনা হিসেবে দেখো — inherit করা subclass-কে পুরো surface দেয়; delegate করা ঠিক যা চেয়েছিল তাই দেয়:
দুটো class একই তিনটা billing operation ব্যবহার করে। পার্থক্য হলো caller-রা আর কী পৌঁছাতে পারে। আঠারোটা দরজা বনাম তিনটা — আর সেই আঠারোটার মধ্যে পনেরোটা হলো দরজা যার জন্য class-টাকে ক্ষমা চাইতে হবে।
এক নজরে আগে আর পরে
TypeScript-এ ক্লাসিক উদাহরণ। Array extend করে তৈরি একটা stack — array-এর সব দরজা খোলা:
// BEFORE: "a stack IS an array" — a lie with consequences
class TicketStack extends Array<string> {
pushTicket(id: string): void { this.push(id); }
popTicket(): string | undefined { return this.pop(); }
}
const stack = new TicketStack();
stack.pushTicket("T-101");
stack.pushTicket("T-102");
// Every inherited door is open. All of these compile and run:
stack.unshift("T-999"); // jumps in from the bottom!
stack.splice(0, 1); // removes from the middle!
stack[0] = "T-777"; // overwrites by index!
// The "stack" cannot defend its own rules.আর পরে — array একটা private, ভাড়া করা কাউন্টার হয়ে যায়:
// AFTER: "a stack HAS an array" — the truth, enforced by the compiler
class TicketStack {
private items: string[] = []; // the delegate: held, not inherited
pushTicket(id: string): void { this.items.push(id); }
popTicket(): string | undefined { return this.items.pop(); }
peek(): string | undefined { return this.items[this.items.length - 1]; }
get size(): number { return this.items.length; }
}
const stack = new TicketStack();
stack.pushTicket("T-101");
// stack.unshift("T-999"); // compile error — the door does not exist
// stack.splice(0, 1); // compile error
// stack[0] = "T-777"; // compile errorসমস্ত উন্নতি যা নেই তার মধ্যে লুকিয়ে আছে। বিপজ্জনক operations documentation বা দলীয় নিয়মানুবর্তিতা দিয়ে নিষিদ্ধ নয় — সেগুলো type-এ একদমই নেই। Compiler এখন সেই invariant রক্ষা করে যা inheritance version শুধু পাঠকদের সম্মান করতে অনুরোধ করতে পারত।
পরিবর্তনের পর সালামের স্টলে একজন খদ্দেরের সাথে কী হয় দেখো — delegation খদ্দেরের কাছে অদৃশ্য, আর বিপজ্জনক অনুরোধ এমন একটা দরজায় ধাক্কা খায় যা আদতে নেই:
ধাপে ধাপে, নিরাপদ পদ্ধতিতে 🪜
Conversion করা যায় build দীর্ঘ সময় না ভেঙে। কৌশল হলো: class এখনো parent extend করার সময়, delegate field আর inheritance একসাথে থাকতে পারে।
ধাপ ১: Delegate field যোগ করো। Subclass-এর ভেতরে parent type-এর একটা private field তৈরি করো। Transition-এর সময় তুমি এটা this দিয়ে initialize করতে পারো — object নিজেকেই delegate করছে — তাই আচরণ এখনো পরিবর্তন হতে পারে না:
class TicketStack extends Array<string> {
private items: Array<string> = this; // temporary: delegate IS the object
pushTicket(id: string): void { this.items.push(id); }
popTicket(): string | undefined { return this.items.pop(); }
}ধাপ ২: প্রতিটি internal use field-এর মাধ্যমে route করো। প্রতিটি জায়গা খুঁজে বের করো যেখানে class একটা inherited method call করে বা inherited state ছুঁয়ে যায়। সেগুলো this.items.<method> হিসেবে পুনরায় লেখো। প্রতিটার পরে compile আর test করো। আচরণ এখনো একই — field হলো this — কিন্তু parent-এর উপর প্রতিটি dependency এখন একটা named doorway-এর মাধ্যমে যাচ্ছে।
ধাপ ৩: সংযোগ কাটো। Field-এর initializer একটা real, আলাদা instance-এ পরিবর্তন করো (= [] বা new Parent(...)) আর একই সাথে extends clause মুছে দাও।
ধাপ ৪: Fallout ঠিক করো, একটা compile error এক সময়ে। Compiler এখন প্রতিটি জায়গা তালিকাভুক্ত করবে যা inheritance-এর উপর নির্ভর করত: inherited method ব্যবহার করা external caller, parent প্রত্যাশা করা type annotation, instanceof check। প্রতিটি external call যা তুমি offer করতে চাও তার জন্য একটা ছোট delegating method যোগ করো। প্রতিটির জন্য যা তুমি হারাতে খুশি — সেটাই ছিল পুরো উদ্দেশ্য — caller আপডেট করো।
ধাপ ৫: যেখানে legitimate ছিল সেখানে polymorphism পুনরুদ্ধার করো। যদি কিছু caller সত্যিই তোমার class আর parent-কে বিনিময়যোগ্যভাবে ব্যবহার করার দরকার রাখত, তাহলে একটা minimal interface বের করো — Countable, Billable, যাই হোক — যা উভয়ই implement করে। সেই caller-দের interface-এ retarget করো। তুমি যে operations-এর জন্য এটা প্রাপ্য সেগুলোর জন্য substitutability রাখো, পুরো contract পুনরায় না এনে।
ধাপ ৬: পুরো suite চালাও, তারপর tighten করো। Test green হলে, তোমার delegating method-গুলো পর্যালোচনা করো: প্রতিটি কি এমন একটা operation যা এই class offer করা উচিত? অভ্যাসের কারণে লেখা কোনোটা মুছে দাও, প্রয়োজনের কারণে নয়। সেই তালিকা যত ছোট, design তত শক্তিশালী।
দুটো সূক্ষ্মতা এখানে মানুষকে কামড় দেয়। প্রথমত, identity: cord কাটার আগে, this আর delegate ছিল একটা object; পরে, এগুলো দুটো। যে code reference তুলনা করত, object-কে map key হিসেবে ব্যবহার করত, বা parent-এর machinery-র মাধ্যমে this-কে listener হিসেবে register করত — সেগুলো সাবধানে দ্বিতীয়বার দেখার দরকার। দ্বিতীয়ত, state duplication: subclass যদি inherited state নিজের field-এর সাথে মিশিয়ে রাখত, নিশ্চিত করো যে প্রতিটি data ঠিক একটা জায়গায় থাকে — delegate-এ বা class-এ, কখনো উভয়ে নয়। একটা অর্ধেক-migrate করা value যা উভয় জায়গায় আছে সেটা হলো "test-এ কাজ করে, production-এ ব্যর্থ হয়"-এর ক্লাসিক উৎস।
একটা বড় বাস্তব উদাহরণ 🛺
সালামের আসল পরিস্থিতি code করা যাক। অনেক আগে কেউ পারিবারিক ব্যবসাকে এভাবে model করেছিল:
// BEFORE: the whole family business, forced on the chaat stall
class SweetShop {
makeRosogolla(qty: number): void { /* 4 a.m. kitchen work */ }
takeFestivalOrder(order: string, qty: number): void { /* wedding-scale orders */ }
payKarigars(): void { /* staff salaries */ }
secretSondeshRecipe(): string { return "...three generations of secrets..."; }
// the genuinely excellent part:
addToBill(item: string, price: number): void { /* ... */ }
printBill(): string { return "...formatted bill..."; }
acceptCard(amount: number): boolean { return true; }
}
// Raju only wanted the billing counter. He got the rosogolla orders too.
class ChaatStall extends SweetShop {
servePaniPuri(plates: number): void {
this.addToBill("Pani Puri", plates * 30); // uses billing — good
}
// Forced to fight his own inheritance:
makeRosogolla(_qty: number): void {
throw new Error("We don't make sweets here!"); // Refused Bequest, loudly
}
}
// And the type system happily betrays everyone:
function placeWeddingOrder(shop: SweetShop) {
shop.takeFestivalOrder("sondesh", 200); // a ChaatStall can be passed in!
}আমাদের checklist থেকে প্রতিটি অ্যালার্ম বাজছে: একটা "not supported" override, parent-এর ক্ষুদ্র একটা অংশ আসলে ব্যবহৃত, আর একটা substitution test (placeWeddingOrder) যা compile হয় কিন্তু অর্থহীন। এখন refactoring — সালাম আসলে যে অংশটা চেয়েছিল সেটা বের করো, আর স্টলকে এটা ভাড়া নিতে দাও:
// AFTER: the counter is its own thing; the stall rents it
class BillingCounter {
private lines: { item: string; price: number }[] = [];
add(item: string, price: number): void { this.lines.push({ item, price }); }
print(): string { return this.lines.map(l => `${l.item}: Rs.${l.price}`).join("\n"); }
acceptCard(amount: number): boolean { /* card machine */ return true; }
total(): number { return this.lines.reduce((s, l) => s + l.price, 0); }
}
class SweetShop {
private counter = new BillingCounter(); // the shop has-a counter too
makeRosogolla(qty: number): void { /* ... */ }
takeFestivalOrder(order: string, qty: number): void { /* ... */ }
billSweets(item: string, price: number): void { this.counter.add(item, price); }
}
class ChaatStall {
private counter = new BillingCounter(); // rented, not inherited
servePaniPuri(plates: number): void {
this.counter.add("Pani Puri", plates * 30);
}
serveJhalMuri(cups: number): void {
this.counter.add("Jhal Muri", cups * 25);
}
customerBill(): string { return this.counter.print(); }
}
// placeWeddingOrder(new ChaatStall()) -> compile error. The lie is gone.একটু ভাবো — স্পষ্ট উন্নতির বাইরে কী আরও ভালো হয়েছে। কোথাও throw new Error("We don't make sweets") নেই — class-গুলো আর নিজের ancestry-র বিরুদ্ধে লড়ছে না। SweetShop নিজেও ভালো হয়েছে: এটাও এখন counter compose করে, তাই billing logic উভয় ব্যবসার share করা একটা focused class-এ একবারই থাকে। আর stall isolation-এ testable: এটাকে একটা mock BillingCounter দাও (চাইলে একটা ছোট interface-এর পেছনে) আর কোনো sweet-shop machinery ছাড়াই chaat logic test করো। সেই swap-ability হলো সেই উপহার যা inheritance কখনো দিতে পারত না — তুমি নিজের parent mock করতে পারো না।
বাস্তব class-গুলো এই পাল্লায় কোথায় পড়ে? দেখো দুটো অক্ষে plot করলে কী হয় — base কতটা ব্যবহৃত হয়, আর is-a কতটা সত্য:
লক্ষ্য করো SchoolMailer inherit-happily কোণে বসে আছে — একটা wrapper যা প্রায় সবকিছু delegate করে আর সত্যিই is-a ধরনের। সেই class পরের পাঠের নায়ক, যেখানে পাল্লা অন্য দিকে দুলবে।
C#-এ একই refactoring 🟣
C# দুটো সুন্দর tool যোগ করে: polymorphism সৎ রাখতে interface, আর sealed/composition idioms যা ecosystem ইতিমধ্যে পছন্দ করে। আগে:
// BEFORE: report generator inherits a database session for "convenience"
public class DbSession
{
public void Open() { /* ... */ }
public void Close() { /* ... */ }
public List<T> Query<T>(string sql) { /* ... */ return new(); }
public void Execute(string sql) { /* ... */ } // writes!
public void DropTable(string name) { /* ... */ } // disaster door
}
public class SalesReport : DbSession // a report IS a database session??
{
public string Generate()
{
Open();
var rows = Query<SaleRow>("SELECT * FROM sales");
Close();
return Format(rows);
}
private string Format(List<SaleRow> rows) => "...";
// Inherited and exposed: Execute(), DropTable() — on a REPORT.
}পরে — session একটা injected, swappable collaborator হয়ে যায়:
// AFTER: the report has-a session, ideally behind an interface
public interface IReadOnlySession
{
List<T> Query<T>(string sql);
}
public sealed class SalesReport
{
private readonly IReadOnlySession _db; // the delegate
public SalesReport(IReadOnlySession db) => _db = db;
public string Generate()
{
var rows = _db.Query<SaleRow>("SELECT * FROM sales");
return Format(rows);
}
private string Format(List<SaleRow> rows) => "...";
// Execute() and DropTable() simply do not exist here.
}C#-নির্দিষ্ট কিছু জিনিস:
- Constructor injection হলো delegate deliver করার স্বাভাবিক পদ্ধতি। DI container production-এ একটা real session wire করে আর তোমার test একটা fake pass করে — composition আর testability একসাথে আসে।
- Interface contract সংকুচিত করে।
IReadOnlySessionপাঁচটার মধ্যে একটা method expose করে। Report ঘটনাক্রমেও table drop করতে পারে না, আর compiler হলো enforcer। sealedসিদ্ধান্ত document করে। Report আর কোনো hierarchy-তে অংশগ্রহণ করে না; sealing পাঠকদের বলে design ইচ্ছাকৃতভাবে composition।- BCL নিজেই এই পাঠ model করে।
System.Collections.Generic.Stack<T>আরQueue<T>List<T>থেকে inherit করে না — এগুলো তাদের storage privately wrap করে, ঠিক যে আকৃতি আমরা এইমাত্র তৈরি করলাম। Standard library has-a বেছে নিয়েছে; এর অনুসরণ করো।
কলেজ কর্নার: ছাত্রছাত্রীরা প্রায়ই forwarding cost নিয়ে চিন্তা করে — প্রতিটি delegated call কি runtime penalty দেয়? সৎভাবে বলতে গেলে: একটা forwarding method হলো একটা অতিরিক্ত call frame, আর আধুনিক JIT compiler ছোট forwarder প্রায় সবসময় inline করে দেয়; TypeScript আর Python-এ পার্থক্য interpreter noise-এ মিলিয়ে যায়। Delegation-এর আসল খরচ nanosecond-এ নয় বরং keystroke আর রক্ষণাবেক্ষণে: প্রতিটি forwarder একটা লাইন যা একজন মানুষ লেখে, review করে, আর sync-এ রাখে। সেই খরচ বাস্তব, আর এটাই ঠিক কারণ বিপরীত refactoring সেই ক্ষেত্রের জন্য আছে যখন তুমি দেখো পুরো interface ফরওয়ার্ড করছ। Forwarding tax দাও যখন এটা একটা guarded boundary কেনে; বন্ধ করো যখন boundary কিছু রক্ষা করছে না।
IDE সাপোর্ট 🛠️
এটা এমন কয়েকটা refactoring-এর একটা যার first-class, one-click automation আছে:
- IntelliJ IDEA / Rider: Refactor → Replace Inheritance with Delegation একটা নিবেদিত, dialog-driven refactoring। তুমি বেছে নাও কোন inherited member class offer করতে থাকবে; IDE
extendsসরায়, delegate introduce করে (field বা inner instance হিসেবে), আর সমস্ত forwarding method স্বয়ংক্রিয়ভাবে generate করে। এটা polymorphic caller রক্ষা করতে interface implement করতেও পারে। - Kotlin: ভাষাটা destination-কে বেক করে রাখে —
class TicketStack(private val items: MutableList<String>) : List<String> by itemsbykeyword দিয়ে একটা পুরো interface delegate করে, শূন্য hand-written forwarder। - ReSharper / Visual Studio (C#): single-click version নেই, কিন্তু recipe ভালোভাবে সমর্থিত: parent-এ Extract Interface, base list পরিবর্তন করো, তারপর নতুন field-এর উপর forwarding method তৈরি করতে Generate → Delegating Members ব্যবহার করো।
- VS Code (TypeScript): manual, compiler দ্বারা guided —
extendsমুছে দাও, তারপর reported error একে একে ঠিক করো, ঠিক আমাদের step-by-step-এর মতো।
সুবিধা আর ঝুঁকি ⚖️
এই table আর পরের পাঠের table-টা mirror image — একই পাল্লা বিপরীত প্রান্ত থেকে পড়া। is-a test আর contract-এর ব্যবহৃত ভাগ বলে তোমার class পাল্লার কোন দিকে।
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| শুধু সেই operations expose করে যা class সত্যিই সমর্থন করে — কোনো leaked, refused, বা stubbed member নেই | Boilerplate: প্রতিটি offered operation একটা forwarding method যা তুমি লেখো আর রক্ষণাবেক্ষণ করো |
| Invariant enforceable হয় — বিপজ্জনক inherited দরজা বিদ্যমান থাকা বন্ধ হয় | পুরনো base type ব্যবহার করা caller ভেঙে পড়ে; legitimate polymorphism-এর জন্য rescued interface দরকার |
| Parent-এর internals থেকে decouple হয় — fragile base class সমস্যা থেকে মুক্ত | যদি is-a সত্যিই ছিল আর প্রায় পুরো contract ব্যবহৃত হতো, তুমি বিনামূল্যের inheritance busywork-এর জন্য trade করেছ — বিপরীত refactoring এটা undo করে |
| Delegate swappable: ভিন্ন implementation, subclass, বা test double | এখন যেখানে একটা ছিল সেখানে দুটো object — identity তুলনা আর listener registration পর্যালোচনা দরকার |
| সত্য বলে: false is-a-এর পরিবর্তে has-a, তাই পাঠক আর type checker বিভ্রান্ত হয় না | প্রতিটি forwarded call-এ পড়ার সময় সামান্য indirection খরচ (আর নগণ্যভাবে runtime-এ) |
এটা কোন smell ঠিক করে? 👃
| Smell | Replace Inheritance with Delegation কীভাবে সাহায্য করে |
|---|---|
| Refused Bequest | Class সেই member উত্তরাধিকারে নেওয়া বন্ধ করে যা সে প্রত্যাখ্যান করেছিল — এখন শুধু সৎভাবে সমর্থন করা জিনিস offer করে |
| Inappropriate Intimacy (subclass–parent) | Class parent-এর protected internals দেখা হারায়; শুধু public contract নাগালে থাকে |
| Leaky abstraction / broken invariants | বিপজ্জনক inherited operations type থেকে অদৃশ্য হয় — comment দিয়ে police করার পরিবর্তে |
| Fragile base class breakage | আগের parent-এ internal পরিবর্তন আর চুপচাপ override ভাঙতে পারে না — কোনো override নেই |
| Middle Man (সতর্কতা, সমাধান নয়) | এই refactoring অতিরিক্ত প্রয়োগ করলে Middle Man তৈরি হয় — যদি forwarding প্রায় সবকিছু cover করে আর is-a সত্য, বিপরীত refactoring দিয়ে ফিরে যাও |
দ্রুত রিভিশন বক্স 📦
+------------------------------------------------------------------+
| REPLACE INHERITANCE WITH DELEGATION - REVISION CARD |
+------------------------------------------------------------------+
| Problem : class EXTENDS a parent only to reuse some code. |
| False is-a -> leaked doors, refused bequest, |
| fragile base class. (Raju forced to inherit the |
| whole sweet shop for one billing counter.) |
| |
| Solution : 1. add private field of parent type (start = this) |
| 2. route all internal calls through the field |
| 3. delete extends; give the field its own instance |
| 4. add forwarding methods ONLY for wanted operations |
| 5. rescue real polymorphism with a small interface |
| |
| Maxim : FAVOR COMPOSITION OVER INHERITANCE |
| default to has-a; pay for is-a only when TRUE |
| Is-a test: child substitutes for parent with ZERO surprises |
| and wants essentially the WHOLE contract |
| Inverse : Replace Delegation with Inheritance (next lesson) |
+------------------------------------------------------------------+অনুশীলনী ✏️
তোমার পালা। ধরো একটা স্কুল লাইব্রেরি সিস্টেমে এই hierarchy আছে, "search code পুনরায় ব্যবহার করতে" লেখা:
class BookCatalog {
protected books: Book[] = [];
addBook(b: Book): void { this.books.push(b); }
removeBook(isbn: string): void { /* ... */ }
findByTitle(t: string): Book[] { /* good search logic */ return []; }
findByAuthor(a: string): Book[] { /* good search logic */ return []; }
exportCatalog(): string { /* full catalog dump */ return ""; }
}
// The kiosk only SEARCHES. But it inherited everything.
class StudentSearchKiosk extends BookCatalog {
showResults(title: string): string {
return this.findByTitle(title).map(b => b.title).join("\n");
}
addBook(_b: Book): void {
throw new Error("Students cannot add books!"); // fighting the parent
}
removeBook(_isbn: string): void {
throw new Error("Students cannot remove books!"); // fighting the parent
}
// exportCatalog() is still inherited and exposed. Oops — privacy leak.
}এর মধ্য দিয়ে কাজ করো:
- দুই-অংশের test চালাও: একটা kiosk কি সত্যিই একটা catalog-এর ধরন (substitution with zero surprises)? এটা contract-এর কোন ভাগ সৎভাবে সমর্থন করে? code স্পর্শ করার আগে এক বাক্যে তোমার রায় লেখো, তারপর চিত্র ৯-এর map-এ kiosk স্থাপন করো।
- নিরাপদ sequence প্রয়োগ করো: একটা private
catalog: BookCatalogfield যোগ করো, এর মাধ্যমেfindByTitleroute করো,extendsসরাও, আর compiler-কে fallout তালিকাভুক্ত করতে দাও। - Kiosk-এর সৎ public surface নির্ধারণ করো। এটা শেষ পর্যন্ত
showResults(আর হয়তোfindByAuthorforwarding) নিয়ে থাকা উচিত — আর আর কিছু নয়। নিশ্চিত করোaddBook,removeBook, আরexportCatalogkiosk-এর type-এ আর নেই, আর design-smell apology হিসেবে উভয়throwoverride মুছে দাও। - Library-এর admin screen legitimately পুরো
BookCatalogpolymorphically ব্যবহার করে। এর জন্য কিছু ভাঙে কি? কেন নয়? - আরও tighten করো: শুধু দুটো find method সহ একটা
BookFinderinterface বের করো,BookCatalog-কে এটা implement করতে দাও, আর kiosk-কে concrete catalog-এর পরিবর্তেBookFinder-এর উপর নির্ভর করতে দাও। এখন kiosk-এর unit test-এর জন্য এক লাইনের fakeBookFinderলেখো। - Bonus চিন্তা: ধরো একটা ভবিষ্যৎ
AdminTerminalclassBookCatalogwrap করে আর শেষ পর্যন্ত প্রতিটি single method ফরওয়ার্ড করে, কিছু যোগ করে না। সেই পরিস্থিতি কোন refactoring চায়, আর এটাকে প্রথমে কোন দুটো শর্ত verify করতে হবে? (এক বাক্য — আর এটা ঠিক পরের পাঠের বিষয়।)
যদি তোমার ধাপ ১-এর রায় ছিল "false is-a: kiosk অর্ধেক contract প্রত্যাখ্যান করে আর বাকিটা leak করে, তাই এটা একটা catalog হতে নয়, রাখতে হবে" — তুমি এই series-এর সবচেয়ে গভীর design নিয়ম internalize করেছ। সালাম তোমার হাত মেলাবে এক প্লেট ফুচকার সাথে। ভালো করেছ।
সচরাচর জিজ্ঞাসা
- 'Favor composition over inheritance' মানে আসলে কী?
- মানে হলো: যখন তুমি শুধু অন্য একটা class-এর কিছু কাজ ব্যবহার করতে চাও, তখন সেই class-কে একটা field-এ রাখো আর call ফরওয়ার্ড করো — extend করো না। extend রাখো শুধু সত্যিকারের is-a সম্পর্কের জন্য, যেখানে child সত্যিই parent-এর একটা ধরন আর parent-এর পুরো contract সৎভাবে সমর্থন করে। নীতিটা 'favor' বলে, 'always' নয় — composition হলো নিরাপদ default, inheritance হলো বিশেষ ক্ষেত্র।
- fragile base class সমস্যা সহজ কথায় কী?
- যখন তুমি inherit করো, তোমার class শুধু parent-এর public contract-এর সাথে না, তার ভেতরের implementation-এর সাথেও আটকে যায়। parent-এর লেখক যদি method-গুলো ভেতরে ভেতরে কীভাবে একে অপরকে call করে তা বদলে দেন — কোনো public আচরণ না বদলেও — তোমার override চুপচাপ ভেঙে যেতে পারে। তোমার class ভঙ্গুর হয়ে পড়ে এমন code-এর কারণে যা তুমি লেখোনি আর পরিবর্তন হতে দেখতেও পাও না।
- একটা সম্পর্ক সত্যিকারের is-a কিনা কীভাবে বুঝবো?
- substitution test করো: child-এর একটা object কি parent প্রত্যাশা করা যেকোনো code-এ দেওয়া যাবে, একদমই অবাক না করে, প্রতিটি inherited operation অর্থপূর্ণভাবে support করবে? যদি একটা inherited method-ও child-এ অর্থহীন, বিপজ্জনক, বা 'not supported' stub দরকার হয় — তাহলে is-a মিথ্যা, আর delegation হলো সৎ design।
- delegation কি অনেক বোরিং forwarding method তৈরি করে না?
- হ্যাঁ, আর এটাই ইচ্ছাকৃত মূল্য। প্রতিটি forwarding method একটা সিদ্ধান্তের জায়গা: তুমি ঠিক সেই operations expose করো যেগুলো তুমি বেছেছ, আর কিছু নয়। যদি দেখো প্রায় parent-এর পুরো interface ফরওয়ার্ড করছ আর is-a আসলেই সত্য — তখন বিপরীত রিফ্যাক্টরিং Replace Delegation with Inheritance আছে ঠিক সেই ক্ষেত্রের জন্য।
- extends clause সরালে কি polymorphism হারিয়ে যাবে?
- যে code তোমার class-কে পুরনো base type হিসেবে ব্যবহার করত, সেটা compile হওয়া বন্ধ হবে, হ্যাঁ। যদি caller-দের সত্যিই substitutability দরকার ছিল, তাহলে একটা ছোট interface বের করো যা তোমার class আর delegate দুটোই implement করে, আর caller-দের সেই interface-এর উপর নির্ভর করতে দাও। polymorphism থাকবে, inherited implementation থেকেও বের হয়ে আসবে।
আরো দেখো
সম্পর্কিত পাঠ
Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি
Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।
Middle Man: যে helper শুধু তোমার message পৌঁছে দেয়, নিজে কিছু করে না
Middle Man code smell টা বোঝো একটা school-এর সেই পিয়নের গল্প দিয়ে — যে শুধু চিরকুট বহন করে, নিজে কিছু যোগ করে না। যখন একটা class শুধু সব call forward করে, সেটা সরিয়ে দাও। কিন্তু Proxy, Facade, আর Adapter কেন জেনেশুনে middle man হয় — সেটাও জানো।
Replace Delegation with Inheritance: যখন সাহায্যকারীই হয়ে যায় শিক্ষানবিশ
Replace Delegation with Inheritance রিফ্যাক্টরিং শেখো একটা দর্জির দোকানের গল্পের মাধ্যমে — Middle Man smell কী, is-a শর্ত কীভাবে চেক করতে হয়, আর TypeScript ও C#-এ ধাপে ধাপে কীভাবে করতে হয় সব বিস্তারিত দেখো।
Hide Delegate: মনিটরকে জিজ্ঞেস করো, মনিটর নিজেই দৌড়াবে
Hide Delegate রিফ্যাক্টরিং শেখো একটা মজার গল্পের মাধ্যমে। employee.department.manager-এর মতো chain লেখা বন্ধ করো — প্রথম object-কে একটা সহজ method দাও আর ভেতরের জার্নি লুকিয়ে রাখো। TypeScript আর C#-এ ধাপে ধাপে উদাহরণসহ।