Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি
Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।
মিষ্টির দোকান আর অনিচ্ছুক উত্তরাধিকারী
ধরো পুরান ঢাকায় একটা ষাট বছরের পুরনো মিষ্টির দোকান আছে: করিম মিষ্টান্ন ভান্ডার। দাদা করিম সাহেব একটা ছোট্ট কড়াই নিয়ে শুরু করেছিলেন রাস্তার পাশে। তাঁর ছেলে সালাম ভাই সেটাকে এমন দোকানে বানিয়েছেন যেটা শহরের প্রতিটা বিয়ের ক্যাটারার চেনে। পরিবারের সবাই জানে পরিকল্পনাটা — সালাম ভাইয়ের বড় ছেলে রুবেল একদিন দোকান নেবে। দোকানের সাথে আসে bequest: গোপন রেসিপি বই (২৪০টা রেসিপি!), বিশাল পিতলের কড়াই, ঈদ-পূজার কাস্টমার লিস্ট, আর ভোর পাঁচটার দুধ সাপ্লায়ারের নম্বর।
একটাই সমস্যা। রুবেল মিষ্টি বানাতে চায় না। রুবেল চায় একটা জিম চালাতে।
কিন্তু পরিবার তো পরিবারই, তাই রুবেল দোকান "inherit" করে। আর তারপর কী হয়?
- একজন কাস্টমার কালো জামের মিষ্টি চায়। রুবেল বলে, "সরি, আমরা এটা আর বানাই না।" (২৪০টা রেসিপির মধ্যে ২৩০টার জন্যই এটা বলে।)
- দুধ সাপ্লায়ার জামাল ভাই ত্রিশ বছর ধরে রোজ ভোর পাঁচটায় ফোন করতেন। রুবেল নম্বর ব্লক করে দিয়েছে।
- পিতলের কড়াই? সে সেটা ট্রেডমিলের পাশে ফুলদানি বানিয়ে রেখেছে।
- bequest থেকে সে আসলে যা ব্যবহার করে: বিল্ডিং, ক্যাশ কাউন্টার, আর সাইনবোর্ডে দাদার সুনাম।
এখন কাস্টমারদের কথা ভাবো। সাইনবোর্ডে এখনও লেখা করিম মিষ্টান্ন ভান্ডার। মানুষ ঢোকে মিষ্টির দোকান ভেবে — নামটাই সেই প্রতিশ্রুতি দেয় — আর পায় প্রোটিন শেক। নাসরিন বেগম, চল্লিশ বছরের পুরনো কাস্টমার, ঈদের তিন দিন আগে দুইশো লাড্ডুর বাক্সের অর্ডার নিয়ে আসেন আর রাগে কাঁপতে কাঁপতে বের হন। জিম খারাপ বলে না — তাঁর নিজের নাতিও জিমে যায় — বরং কারণটা হলো সাইনবোর্ডের প্রতিশ্রুতি আর ভেতরের বাস্তবতা মেলে না। কেউ কেউ এমন একটা দুঃখের কৌশল শিখে নিয়েছে: ঢোকার আগে জানালায় উঁকি দিয়ে দেখে "এটা এখনও মিষ্টির দোকান আছে কি?" — declared type বিশ্বাস করা যাচ্ছে না তাই actual type চেক করতে হচ্ছে।
এটাই হলো Refused Bequest কোড স্মেল। একটা subclass parent-এর "এক ধরন" হিসেবে সাইন আপ করে, সব কিছু inherit করে, তারপর বেশিরভাগই প্রত্যাখ্যান করে — method গুলো "not supported" throw করে, field গুলো কখনো ব্যবহার হয় না, প্রতিশ্রুতি কখনো রাখা হয় না। Caller-রা, নাসরিন বেগমের মতো, instanceof চেক লিখতে শুরু করে — সাইনবোর্ড মিথ্যা বলে তাই জানালায় উঁকি দিতে হয়। রুবেল, সালাম ভাই, নাসরিন বেগম আর সেই ঈদের অর্ডার — এই পুরো পোস্টটা তাদের গল্প।
এই smell টা আসলে কী?
Refused Bequest হলো Object-Orientation Abuser smell গুলোর একটা। এটা OOP-এর সবচেয়ে শক্তিশালী — আর সবচেয়ে বিপজ্জনক — ফিচারটার অপব্যবহার করে: inheritance।
যখন তুমি class Child extends Parent লেখো, তুমি একটা জোরালো public statement করছো: "Child হলো Parent। যেখানে Parent কাজ করে, Child-ও কাজ করবে।" Parent-এর public method গুলো একটা contract, আর inherit করার মাধ্যমে child সেই contract-এ সই করে। পুরো codebase-এর প্রতিটা caller সেটার উপর নির্ভর করতে পারে।
smell টা দেখা দেয় যখন child contract-এ সই করে কিন্তু মানতে রাজি হয় না:
- Inherited method গুলো throw করতে override করা হয় (
NotSupportedException,UnsupportedOperationException) বা চুপচাপ অর্থহীন কিছু করে। - Inherited field গুলো অব্যবহৃত পড়ে থাকে বা অদ্ভুত মান দিয়ে জোর করে চালানো হয়।
- Child প্রায় সব কিছু override করে, শুধু
extendskeyword রেখে দেয় কিন্তু কোনো real behaviour শেয়ার করে না। - Parent-এর সাথে পুরোপুরি কাজ করা কোড এই child পেলে ভেঙে পড়ে।
কেন এটা হয়? প্রায় সবসময়ই কারণ হলো true "is-a" relationship-এর জন্য নয়, code reuse-এর জন্য inheritance ব্যবহার করা হয়েছে। Parent-এর একটা বা দুটো কাজের method ছিল। কেউ শুধু সেগুলো নেওয়ার জন্য subclass করেছে — ঠিক যেন শুধু বিল্ডিংটা পেতে পুরো মিষ্টির দোকান inherit করার মতো। বাকি ২৩৮টা রেসিপি বিনা নিমন্ত্রণে এসে গেছে, আর এখন একে একে "সরি, এটা করি না" বলে প্রত্যাখ্যান করতে হচ্ছে।
এক লাইনে সারকথা: Refused Bequest হলো এমন একটা subclass যেটা inheritance-এর সম্পর্ক রেখে দেয় কিন্তু পাওয়া জিনিসের বেশিরভাগই প্রত্যাখ্যান করে — type hierarchy মিথ্যা বলে, আর যে caller সেই মিথ্যা বিশ্বাস করে সে ক্ষতিগ্রস্ত হয়।
এই ব্যথার পেছনে একটা formal principle আছে: Liskov Substitution Principle (LSP) — SOLID-এর "L", computer scientist Barbara Liskov-এর নামে। সহজ কথায়: subclass object-কে যেখানে parent দরকার সেখানে কোনো অবাক করা ব্যাপার ছাড়াই ব্যবহার করা যেতে হবে। Inherited method-এ throw করা child হলো textbook LSP violation। Refused Bequest হলো যে smell দেখা যায়; LSP break হলো তার নিচের ক্ষত।
কলেজ কর্নার: LSP-র মানে শুধু "child throw করবে না" এর চেয়ে বেশি। Barbara Liskov-এর formulation হলো behavioural subtyping নিয়ে, এটা তোমাকে একটা precise checklist দেয়। Subclass override precondition শক্ত করতে পারবে না — মানে parent যা চেয়েছে তার চেয়ে বেশি demand করতে পারবে না। Parent-এর orderMilk(litres) যেকোনো positive number নিলে child দশের গুণিতক দাবি করতে পারবে না। Postcondition দুর্বল করতে পারবে না — parent যা দেওয়ার প্রতিশ্রুতি করেছে তার চেয়ে কম দিতে পারবে না। Parent-এর invariant রক্ষা করতে হবে। আর পুরনো method-এ নতুন exception type দিয়ে caller-দের অবাক করা যাবে না। লক্ষ্য করো যে compiler signature চেক করে, behaviour নয় — তোমার override নিখুঁতভাবে compile হতে পারে এবং তবুও উপরের প্রতিটা নিয়ম ভাঙতে পারে। এজন্যই LSP violation ধরা পড়ে test আর code review-তে, type checker-এ নয়। একটা practical কলেজ-স্তরের কৌশল: parent-এর পুরো unit test suite প্রতিটা subclass-এ চালাও। যেকোনো লাল test মানেই একটা refused bequest — stack trace সহ।
এগিয়ে যাওয়ার আগে একটা গুরুত্বপূর্ণ পার্থক্য বুঝতে হবে। প্রত্যাখ্যানের দুটো স্তর আছে:
- Implementation প্রত্যাখ্যান — child পুরো contract inherit করে কিন্তু কিছু inherited জিনিস নিজের (তবুও valid) উপায়ে করে। প্রায়ই গ্রহণযোগ্য।
- Interface প্রত্যাখ্যান — child নিজেই public contract প্রত্যাখ্যান করে, caller-রা যে method ব্যবহার করতে পারে সেখানে throw করে বা ভুল আচরণ করে। এটাই ক্ষতিকর রূপ, কারণ এটা substitution ভেঙে দেয়।
পুরো smell এক মানচিত্রে:
কীভাবে চেনা যায়
চেকলিস্ট দেখো:
- একটা subclass method override করে "not supported" throw করে, বা
nullবা-1এর মতো dummy value ফেরত দেয়। - Inherited public method গুলো codebase-এ কোথাও subclass-এ call হয় না।
- Inherited field গুলো এই subclass-এ সবসময় default মানেই পড়ে থাকে।
- Caller-রা নির্দিষ্ট method call করার আগে
if (x instanceof ChildType)লেখে — তারা hierarchy বিশ্বাস না করতে শিখে গেছে। - Subclass প্রায় প্রতিটা parent method override করে; খুব কম real behaviour শেয়ার হয়।
- Parent type-এর বিপরীতে লেখা unit test গুলো এই subclass-এ fail করে (বা skip করা হয়)।
- Class-এর comment বা commit history বলে "শুধু connection logic reuse করতে X extend করা হয়েছে।"
লক্ষণের ছক:
| তুমি যা দেখো | এর আসল মানে |
|---|---|
Override-এ throw new NotSupportedException() | Child publicly একটা contract প্রত্যাখ্যান করছে যেটায় সে publicly সই করেছে |
| Inherited method গুলো child-এ কখনো call হয় না | Bequest হলো dead weight, class-এর surface শুধু বড় হচ্ছে |
Method call করার আগে instanceof চেক | Caller-রা আর type বিশ্বাস করে না; polymorphism এখানে মৃত |
| Child প্রায় সব কিছু override করে | extends এক-দুটো জিনিসের জন্য, real is-a নয় |
| Parent-এর test child-এ fail করে | সরাসরি, পরিমাপযোগ্য Liskov violation |
| "Is-a" বাক্য জোরে বললে ভুল শোনায় | "একটা জিম হলো একটা মিষ্টির দোকান"? বাক্যটা অদ্ভুত হলে কোডটাও তাই |
শেষ সারিটাই সবচেয়ে সহজ classroom test: inheritance-টা বাংলায় একটা বাক্য হিসেবে বলো। "একটা ReadOnlyList হলো একটা MutableList" — অদ্ভুত, read-only জিনিস mutable নয়। "একটা Penguin হলো একটা FlyingBird" — অদ্ভুত। বাক্যটা শুনে কষ্ট লাগলে কোডও সেরকম।
quantitative version-ও আছে। Bequest-এর audit করো: parent যা দিয়েছে তার কতটুকু child সত্যিই মেনে চলছে? এখানে রুবেলের audit:
নব্বই শতাংশ inheritance প্রত্যাখ্যাত, blocked, বা ফুলদানি হলে extends keyword মাত্র দশ শতাংশ সত্য বহন করছে — বাকি নব্বই শতাংশ মিথ্যা। একটা সুস্থ subclass-এর সাথে তুলনা করো — KajuKatliCounter extends SweetShop যেটা একটা বিশেষত্ব যোগ করে — সেখানে pie প্রায় পুরোটাই "inherited as used" হবে।
কেন এটা সমস্যা
ক্ষতি ১: Substitution হয়ে যায় মাইনফিল্ড। Subclass-এর পুরো উদ্দেশ্যই হলো parent যেখানে দরকার সেখানে child দেওয়া যাবে। Refused bequest-এ কিছু call কাজ করে আর কিছু runtime-এ বিস্ফোরণ ঘটায়। প্রতিটা polymorphic call site এখন সম্ভাব্য মাইন, আর type system বলে না কোনটা।
ক্ষতি ২: Hierarchy প্রতিটা পাঠককে বিভ্রান্ত করে। Inheritance হলো codebase-এর সবচেয়ে জোরালো design statement। নতুন developer দেখে ধরে নেয়: "এটা একটা MutableList, তাই আমি এতে add করতে পারি।" statement মিথ্যা হলে মানুষ আত্মবিশ্বাসের সাথে bug লেখে।
ক্ষতি ৩: Type check ফিরে আসে। মাইনগুলো থেকে বাঁচতে caller-রা instanceof check যোগ করে — এখন তুমি Switch Statements smell তৈরি করেছো। Subclass-এর quirk সম্পর্কে জ্ঞান পুরো codebase-এ ছড়িয়ে পড়ে, ঠিক যেটা polymorphism আটকানোর কথা ছিল।
ক্ষতি ৪: Dead surface area। Unused inherited member গুলো child-কে যত capable তার চেয়ে বেশি দেখায়। Auto-complete এমন method offer করে যেগুলো throw করে। Documentation generator এমন feature list করে যেগুলো exist করে না। প্রতিটা পাঠককে একটু বাড়তি confusion tax দিতে হয়।
নিচের loop টা দেখো — crash-and-guard cycle কখনো শেষ হয় না, কারণ মূল মিথ্যা (extends) এখনও দাঁড়িয়ে আছে।
এখন ঈদের বিপর্যয়টা message sequence হিসেবে দেখো। Festival ordering system বছরের পর বছর আগে লেখা হয়েছিল, সৎভাবে, SweetShop-এর বিরুদ্ধে। এর কোনো ধারণাই নেই যে তার shop list-এ একটা জিম লুকিয়ে আছে:
Crash-টা জিমের stack trace-এ নয়, order system-এর stack trace-এ পড়ে। এটাই এই smell-এর নিষ্ঠুর বৈশিষ্ট্য: মিথ্যাবাদী সরে যায়, আর সৎ caller দোষ পায়। সেদিনটা developer-এর জন্য কেমন লাগে যে support duty-তে আছে:
মাঝের section-টা লক্ষ্য করো: quick instanceof patch মনে হয় progress, কিন্তু আসলে না — কারণ প্রতিটা guard হলো আরেকটা জায়গা যেটা পরের fake sweet shop আসলে বদলাতে হবে। শুধু শেষ section-টাই — hierarchy ঠিক করা — মেজাজ (আর design) ফিরিয়ে আনে।
বাস্তব কোডের উদাহরণ
চলো মিষ্টির দোকানটা code করি। মূল পারিবারিক ব্যবসা:
// The parent: a full-fledged sweet shop
class SweetShop {
protected recipes = new Map<string, string>(); // 240 family recipes
protected milkSupplier = "Sharma Dairy, 5 a.m. daily";
constructor(public readonly signboard: string) {
this.recipes.set("kaju katli", "cashew, sugar, ghee...");
this.recipes.set("motichoor laddoo", "besan, saffron, ghee...");
// ...238 more
}
makeSweet(name: string): string {
const recipe = this.recipes.get(name);
if (!recipe) throw new Error(`No recipe for ${name}`);
return `Fresh ${name} made using: ${recipe}`;
}
orderMilk(litres: number): string {
return `Ordered ${litres}L from ${this.milkSupplier}`;
}
sellAtCounter(item: string, price: number): string {
return `Sold ${item} for ₹${price}`;
}
}এখন রুবেল "inherit" করে — কারণ সে বিল্ডিং, কাউন্টার আর সাইনবোর্ডের সুনাম চায়:
// BAD CODE: a gym pretending to be a sweet shop
class GuptaGym extends SweetShop {
constructor() {
super("Gupta Mithai Bhandar"); // keeps grandfather's signboard!
}
// Refused bequest #1: 240 recipes, all refused
override makeSweet(name: string): never {
throw new Error("Sorry, we don't make sweets anymore.");
}
// Refused bequest #2: supplier contact, refused
override orderMilk(litres: number): never {
throw new Error("We only stock protein shakes.");
}
// The ONLY inherited thing actually used:
// sellAtCounter() — for gym memberships.
startWorkout(member: string): string {
return `${member} started training!`;
}
}আর এখানেই আসল ব্যথা — নিরীহ কোড, parent-এর বিরুদ্ধে সঠিকভাবে লেখা, child পেলে বিস্ফোরণ ঘটে:
// This function is 100% correct for any real SweetShop...
function prepareDiwaliOrder(shop: SweetShop): string[] {
shop.orderMilk(50); // BOOM with GuptaGym
return [
shop.makeSweet("kaju katli"), // BOOM with GuptaGym
shop.makeSweet("motichoor laddoo"),
];
}
const shops: SweetShop[] = [new SweetShop("Agarwal Sweets"), new GuptaGym()];
// Somebody, somewhere, will eventually write:
shops.forEach(prepareDiwaliOrder); // runtime crash, festival ruinedলক্ষ্য করো এটা crash করলে কে দোষ পায়। GuptaGym নয়, যার মিথ্যা কারণ — বরং বেচারা prepareDiwaliOrder, যেখানে stack trace দেখায়। Refused bequest নিরীহ কোডকে দোষী দেখায়। আর সাধারণ "fix" সব কিছু আরও খারাপ করে:
// The smell spreading: callers peep through the window
function prepareDiwaliOrderDefensive(shop: SweetShop): string[] {
if (shop instanceof GuptaGym) return []; // special-casing the liar
shop.orderMilk(50);
return [shop.makeSweet("kaju katli")];
}এরকম প্রতিটা guard হলো আরেকটা জায়গা যেটা পরের fake sweet shop আসলে বদলাতে হবে। দুটো design পাশাপাশি দেখো:
প্রথম bar হলো এক সংখ্যায় smell: এমন একটা class যার public surface বেশিরভাগই মাইন। অন্য দুটো bar হলো লক্ষ্য — ছোট হয়তো, কিন্তু প্রতিটা method বাস্তব।
ধাপে ধাপে পরিষ্কার করা
ধাপ ১: is-a প্রশ্নটা সৎভাবে করো। "একটা জিম হলো একটা মিষ্টির দোকান"? না। রুবেল আসলে চায় দোকানের অংশ: কাউন্টার (billing) আর premises। সমাধান হলো পুরোটা inherit করা বন্ধ করে অংশগুলো ধরে রাখা — এই refactoring-কে বলে Replace Inheritance with Delegation।
ধাপ ২: সত্যিকারের shared অংশটা extract করো। একটা মিষ্টির দোকান আর একটা জিমের মধ্যে সত্যিই কী মিল আছে? একটা retail counter। শুধু সেটাই একটা ছোট্ট class-এ নাও:
// Step 2: the real shared thing, extracted
class SalesCounter {
private total = 0;
sell(item: string, price: number): string {
this.total += price;
return `Sold ${item} for ₹${price}`;
}
dailyTotal(): number {
return this.total;
}
}ধাপ ৩: দুটো ব্যবসাই counter রাখে; কেউ অপরটা নয়। Inheritance হয়ে যায় composition:
// Step 3: is-a becomes has-a — nothing left to refuse
class SweetShop {
private counter = new SalesCounter(); // has-a
private recipes = new Map<string, string>();
makeSweet(name: string): string { /* ...as before... */ return ""; }
orderMilk(litres: number): string { /* ...as before... */ return ""; }
sellAtCounter(item: string, price: number): string {
return this.counter.sell(item, price); // delegate
}
}
class Gym {
private counter = new SalesCounter(); // has-a, same helper!
sellMembership(plan: string, price: number): string {
return this.counter.sell(`${plan} membership`, price);
}
startWorkout(member: string): string {
return `${member} started training!`;
}
// No makeSweet. No orderMilk. Nothing to throw. Nothing to refuse.
}ধাপ ৪: Substitution এখনও দরকার হলে, উভয়ই যে contract রাখতে পারবে সেটা share করো। ধরো শহরের "দোকান মালিক সমিতি"-র software সব ব্যবসা একইভাবে handle করতে চায়। সবাই যা রাখতে পারবে শুধু সেই প্রতিশ্রুতি ধারণ করে একটা interface দাও:
// Step 4: a truthful common contract
interface Business {
dailySales(): number;
}
class SweetShop implements Business {
dailySales() { return this.counter.dailyTotal(); }
// ...
}
class Gym implements Business {
dailySales() { return this.counter.dailyTotal(); }
// ...
}
// Works for EVERY business — no instanceof, no landmines
function collectAssociationFee(b: Business): number {
return b.dailySales() * 0.001;
}এখানে class diagram হিসেবে সম্পূর্ণ design — লক্ষ্য করো arrow-এর দিক "extends" থেকে "has" আর "implements"-এ বদলে গেছে:
যেখানে parent আংশিক ঠিক — বেশ কয়েকটা child কিছু behaviour share করে কিন্তু প্রত্যেকে ভিন্ন অংশ প্রত্যাখ্যান করে — সেখানে দুটো sibling refactoring সাহায্য করে: Extract Superclass (শুধু common member গুলো নতুন, ছোট parent-এ তুলে নাও) আর Push Down Method / Push Down Field (শুধু একটা child ব্যবহার করা member গুলো সেই child-এ নামিয়ে দাও)। নিয়ম হলো: parent-কে এত ছোট করো যাতে কোনো child কিছুই প্রত্যাখ্যান না করে।
একটা hierarchy, দোকানের মতো, তার জীবনে চেনা স্বাস্থ্য অবস্থার মধ্য দিয়ে যায়। দক্ষতাটা হলো festival crash-এর আগেই বুঝতে পারা তোমারটা কোন অবস্থায় আছে:
বাম পথ (parent ছোট করা) সেই hierarchy-র জন্য উপযুক্ত যেগুলো বেশিরভাগই ঠিক আছে। ডান পথ (delegation) সেই hierarchy-র জন্য উপযুক্ত, রুবেলেরটার মতো, যেগুলো শুরু থেকেই সত্যিকারের is-a ছিল না।
বিখ্যাত classroom উদাহরণটা বাস্তব: java.util.Stack extends Vector, তাই প্রতিটা Java Stack-এ insertElementAt আর এরকম method inherit হয় — যেগুলো stack-এর মাঝখানে ঢুকিয়ে last-in-first-out নিয়ম ভেঙে দেয়। Java team-এর নিজেদের documentation এখন Deque ব্যবহার করতে বলে। Standard library-ও এই ভুল করে — is-a বাক্যগুলো দুবার যাচাই করো।
C#-এ একই smell
একটু ভাবো — পাখির hierarchy তৈরি করলে কী হয়? একটা ছোট C# উদাহরণ:
// BAD: not every bird honours the flying contract
class Bird
{
public virtual string Fly() => "Flying high!";
public virtual string Eat() => "Pecking at seeds.";
}
class Penguin : Bird
{
public override string Fly()
=> throw new NotSupportedException("Penguins cannot fly!");
// Eat() is fine — but the contract is already broken.
}
// Innocent code, runtime explosion:
void Migrate(List<Bird> flock)
{
foreach (var bird in flock) Console.WriteLine(bird.Fly()); // BOOM at the penguin
}সমাধান হলো: parent-এর প্রতিশ্রুতি ছোট করো সব পাখি যা রাখতে পারবে তার মধ্যে, আর flying-কে আলাদা, optional contract-এ নিয়ে যাও:
// GOOD: small truthful base + optional capability interface
abstract class Bird
{
public virtual string Eat() => "Pecking at seeds.";
}
interface IFlyingBird
{
string Fly();
}
class Sparrow : Bird, IFlyingBird
{
public string Fly() => "Flying high!";
}
class Penguin : Bird
{
public string Swim() => "Torpedo mode!"; // its own real talent
}
// Only things that CAN fly are asked to fly — checked at compile time
void Migrate(IEnumerable<IFlyingBird> flyers)
{
foreach (var f in flyers) Console.WriteLine(f.Fly());
}কিছু throw করে না, কিছু প্রত্যাখ্যান করে না। Compiler — runtime exception নয় — পেঙ্গুইনকে উড়তে বলা থেকে তোমাকে থামায়।
কলেজ কর্নার: এই bird fix দেখায় interface segregation (SOLID-এর "I") LSP-র সাথে হাত মিলিয়ে কাজ করছে। মূল Bird দুটো capability — খাওয়া আর উড়া — একটা contract-এ বেঁধে দিয়েছিল, প্রতিটা subclass-কে দুটোতেই সই করতে বাধ্য করে। Optional capability IFlyingBird-এ আলাদা করলে প্রতিটা class শুধু যে প্রতিশ্রুতি রাখতে পারে সেটায় সই করে — আর substitution আবার প্রমাণযোগ্য হয়। Hierarchy design-এর একটা general শিক্ষা: base type-এ থাকা উচিত সব subtype যা করতে পারে তার intersection, কোনো কোনো subtype যা করতে পারে তার union নয়। Union-গুলো যায় ছোট capability interface-এ যেগুলোতে subtype opt in করে। যখন দেখবে throwing override লিখতে যাচ্ছো, বুঝবে parent-এর কিছু একটা intersection-এ নয় union-এ আছে — আর সেটাই হলো তোমার cue, throw করার বদলে interface extract করো।
বাস্তব project-এ এই smell কোথায় লুকায়
- Standard library. Java-র
Stack extends Vector(index method গুলো LIFO ভাঙে) আর অনেকjava.utilcollection যেখানে read-only wrapper-এরremove()throwsUnsupportedOperationException— দুটোই platform-এ bake করা refused bequest। - "Fat" framework base class. Team একটা ভারী
BaseControllerবাBaseServiceextend করে hooks ভর্তি, তারপর অর্ধেক hooks কিছু না করতে override করে। প্রতিটা empty override একটা ছোট প্রত্যাখ্যান। - Test double. একটা
FakePaymentGateway extends RealPaymentGatewayযেটা network call এড়াতে সব কিছু override করে — শূন্য behaviour share করে আর শুধু trouble inherit করে। এটার একটা interface দরকার ছিল, parent নয়। - Square/Rectangle trap.
Square extends Rectangleগাণিতিকভাবে স্পষ্ট মনে হয় যতক্ষণ নাsetWidth-কে height-ও বদলাতে হয়, আর rectangle resize করা কোড square-এ ভেঙে পড়ে। Domain drift সেখানে প্রত্যাখ্যান তৈরি করে যেখানে প্রথম দিনে ছিল না — ঠিক যেমন মিষ্টির দোকান দুই প্রজন্ম ধরে সত্যিকারের is-a ছিল, রুবেলের আগ পর্যন্ত। - Read-only view mutable model extend করছে।
ReadOnlyOrder extends Orderthrowing setter সহ — প্রত্যাখ্যানটাই পুরো design। - Copy-reuse inheritance। "আমি
CsvImporterextend করলাম ওর file-reading code পেতে, কিন্তু আমার class XML import করে।" একটা কাজের private method, বিশটা refused public method।
প্রতিটা ক্ষেত্রে shared মূল কারণ: reuse-এর সুবিধার জন্য inheritance, is-a বাক্যটা সত্য বলে নয়।
কখন উপেক্ষা করা যায়
| পরিস্থিতি | রায় | কেন |
|---|---|---|
| Child একটা implementation detail প্রত্যাখ্যান করে কিন্তু পুরো public contract রাখে | ঠিক আছে | Substitution কাজ করে; সেই flexibility-ই override-এর জন্য |
| Override যা কিছু করে না, যেখানে "কিছু না করা" সেই child-এর জন্য সত্যিই সঠিক আচরণ | ঠিক আছে | একটা SilentLogger.log() যেটা message উপেক্ষা করে contract রাখে: caller কোনো return আশা করে না, অবাক হয় না |
| Framework তোমাকে heavyweight base extend করতে বাধ্য করে (Activity, ControllerBase) | সহ্য করো | Framework-এর বিরুদ্ধে লড়াই কয়েকটা unused inherited member-এর চেয়ে বেশি খরচ |
| একটা ছোট unused inherited helper method | রেখে দাও | Dead weight-এর একটা টুকরো hierarchy surgery-র মতো নয় |
| Child inherited public method-এ throw করে | Refactor করো | এটাই ক্ষতিকর রূপ — substitution ভেঙে গেছে |
Caller-রা ইতিমধ্যে child-এর চারপাশে instanceof guard লিখছে | জরুরিভাবে Refactor করো | Smell পুরো codebase-এ type-switching-এ ছড়িয়ে পড়েছে |
| Is-a বাক্যটা জোরে বললে অদ্ভুত শোনায় | Refactor করো | Hierarchy মিথ্যা বলছে; প্রতিটা ভবিষ্যৎ পাঠক বিভ্রান্ত হবে |
দুটো প্রশ্ন যেকোনো সন্দেহজনক subclass-কে map-এ রাখে: এটা contract-এর কতটুকু প্রত্যাখ্যান করে, আর এটা কতটা widely parent type হিসেবে pass হয়?
GuptaGym বিপদের কোণে গভীরে বসে: প্রায় সব কিছু প্রত্যাখ্যান করে আর codebase-এ SweetShop[] list-এর মধ্যে ঘোরে। একটা class যেটা অনেক কিছু প্রত্যাখ্যান করে কিন্তু কখনো substituted হয় না (নিচে-ডানে) অপেক্ষা করতে পারে; যে প্রত্যাখ্যানকারী ঘোরে সে পারে না।
Quick honesty meter: implementation প্রত্যাখ্যান = সাধারণত ঠিক আছে; interface প্রত্যাখ্যান (public প্রতিশ্রুতি) = real smell। জিজ্ঞেস করো: "parent-এর জন্য লেখা কোড কি এই child দিয়ে crash করতে পারে বা ভুল আচরণ করতে পারে?" হ্যাঁ হলে ঠিক করো।
কোন refactoring গুলো সমাধান করে
| Refactoring | কখন ব্যবহার করবে |
|---|---|
| Replace Inheritance with Delegation | মূল সমাধান: child শুধু parent-এর অংশ চায় — field হিসেবে রাখো, শুধু যা honour করো তাই expose করো |
| Extract Superclass | বেশ কয়েকটা class কিছু behaviour share করে — শুধু common অংশটা নতুন ছোট parent-এ তোলো যেটা কেউ প্রত্যাখ্যান করে না |
| Extract Interface | Shared জিনিসটা code নয়, contract — সত্যিকারের promise set define করো আর implement করো |
| Push Down Method | একটা parent method শুধু কিছু children-এর কাছে important — সেই children-এ নামিয়ে দাও |
| Push Down Field | Field-এর জন্যও একই কথা যেগুলো কিছু children ব্যবহার করে |
| Rename / re-document hierarchy | কখনো কখনো কোড ঠিক আর নাম মিথ্যা বলে — rename করে is-a বাক্যটা সত্যিকারের করো |
Quick revision box
+----------------------------------------------------------------+
| REFUSED BEQUEST — CHEAT SHEET |
+----------------------------------------------------------------+
| Story : Rohan inherits the sweet shop, refuses 230 of 240 |
| recipes, runs a gym under grandfather's signboard. |
| Smell : Subclass keeps `extends` but rejects the bequest: |
| throwing overrides, unused members, broken promises|
| Spot it : NotSupportedException overrides, instanceof guards,|
| parent tests fail on child, absurd is-a sentence. |
| Danger : Liskov violation -> innocent caller code crashes; |
| type checks spread; hierarchy misleads everyone. |
| Cure : Replace Inheritance with Delegation (is-a -> has-a)|
| Extract Superclass / Interface; Push Down members. |
| OK when : Refusing implementation (not interface); framework |
| bases; one harmless unused helper. |
| Mantra : Say the is-a sentence out loud. If you wince, fix. |
+----------------------------------------------------------------+অনুশীলনের প্রশ্ন
ধরো একটা school management system-এ এই hierarchy আছে। Compile হয়, "কাজ করে," আর মিথ্যা বলছে:
class Teacher {
constructor(public name: string) {}
teachClass(subject: string): string { return `${this.name} teaches ${subject}`; }
gradeExams(count: number): string { return `${this.name} graded ${count} papers`; }
attendStaffMeeting(): string { return `${this.name} attended the meeting`; }
collectSalary(): number { return 45000; }
}
// A guest speaker visits once a year for Career Day...
class GuestSpeaker extends Teacher {
override gradeExams(count: number): never {
throw new Error("Guests do not grade exams");
}
override attendStaffMeeting(): never {
throw new Error("Guests do not attend staff meetings");
}
override collectSalary(): number {
return 0; // guests are unpaid... is this honest or a refusal?
}
}তোমার কাজ:
- Diagnose করো: Is-a বাক্যটা জোরে বলো। প্রতিটা refused member list করো, আর প্রতিটা প্রত্যাখ্যান classify করো: interface refusal (throw / caller ভাঙে) নাকি implementation refusal (ভিন্ন কিন্তু valid আচরণ)।
collectSalary() → 0কি সৎ, নাকি payroll code যেটাTeacherআশা করছিল সেটা এখন guests-দের ভুলভাবে handle করছে? - ক্ষতিগ্রস্তকে খোঁজো:
endOfTerm(teachers: Teacher[])function লেখো যেটা সবার উপরgradeExamscall করে, আর দেখাও কীভাবে list-এ একটাGuestSpeakercrash করে — যদিওendOfTermbug-free। এটা তোমার miniature ঈদের অর্ডার। - চিত্র ২-এর মতো Audit করো:
GuestSpeaker-এর জন্য pie আঁকো — চারটা inherited method-এর কতগুলো honoured, dummy, বা throwing? Bequest-এর কত শতাংশ প্রত্যাখ্যাত? - Refactor করো: সমাধান apply করো। একটা ছোট সত্যিকারের contract extract করো (হয়তো
interface Educator { teachClass(subject: string): string }),GuestSpeaker-কে শুধু সেটা implement করা standalone class বানাও, আরTeacher-কে grading, meetings, আর salary রাখতে দাও। দুটো real code share করলে delegation ব্যবহার করো। - Verify করো: Refactor করার পরে
GuestSpeaker-কেendOfTerm-এর list-এ দেওয়ার চেষ্টা করো। Compiler এখন refuse করবে — আর compile-time "না" হলো ঠিক সেই protection যা runtimethrowকখনো দিতে পারেনি।
সচরাচর জিজ্ঞাসা
- Refused Bequest নামটার মানে কী?
- Bequest মানে হলো বাবা-মা মারা যাওয়ার সময় সন্তানকে যা দিয়ে যায়। কোডে, parent class তার method আর field গুলো subclass-কে দেয়। Refused Bequest হলো যখন একটা subclass inheritance-এর সম্পর্ক রেখে দেয় কিন্তু পাওয়া জিনিসের বেশিরভাগই ফিরিয়ে দেয় — method throw করে, field ব্যবহার হয় না, প্রতিশ্রুতি রাখা হয় না।
- Refused Bequest আর Liskov Substitution Principle-এর সম্পর্ক কী?
- LSP বলে যেখানে parent type দরকার সেখানে subclass object দিলেও ঠিকঠাক কাজ করতে হবে। একটা subclass যদি inherited method-এ NotSupported throw করে, সেই কথা ভেঙে দেয়: parent ভেবে লেখা কোড এই child পেলে crash করে। Refused Bequest হলো smell — LSP violation হলো সেই ক্ষত।
- subclass কি সবসময় method override করে কিছু না করলে সমস্যা হয়?
- না। কিছু না করাটাই যদি সত্যিই সেই subclass-এর সঠিক আচরণ হয়, তাহলে contract ঠিক আছে। smell তখনই বড় হয় যখন subclass parent-এর public contract ভেঙে দেয় — error throw করে বা caller-দের প্রত্যাশা মেটায় না।
- Refused Bequest-এর মূল সমাধান কী?
- Replace Inheritance with Delegation। Parent extend করার বদলে, class-টা parent (বা একটা shared helper) কে private field হিসেবে রাখে আর শুধু যে method গুলো সত্যিই support করে সেগুলোই expose করে। is-a হয়ে যায় has-a, আর প্রত্যাখ্যান করার কিছুই থাকে না।
- এই smell-এর কোনো বিখ্যাত বাস্তব উদাহরণ আছে?
- হ্যাঁ — java.util.Stack extends Vector করে, তাই Stack-এ index-based insert আর remove method inherit হয় যেগুলো last-in-first-out নিয়ম ভেঙে দেয়। Java team নিজেরাই এখন Deque ব্যবহার করতে বলে। Code reuse-এর জন্য inheritance নেওয়া হয়েছিল, true is-a relationship-এর জন্য নয়।
আরো দেখো
সম্পর্কিত পাঠ
Alternative Classes with Different Interfaces: দুই টিফিন সার্ভিস, দুই ভাষা
একটা টিফিন ডেলিভারির গল্প দিয়ে শেখো এই code smell: দুটো class একই কাজ করে কিন্তু method নাম আলাদা, তাই একটাকে অন্যটার জায়গায় বসানো যায় না। ধাপে ধাপে ঠিক করা দেখো।
Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা
Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।
Speculative Generality: যে সুইমিং পুলের জন্য পাইপ বসালে, পুলটাই হলো না
বাড়ি বানানোর গল্প দিয়ে Speculative Generality smell বোঝো। YAGNI কী, ভবিষ্যতের অনুমানে কোড লেখা কেন ক্ষতিকর, আর অব্যবহৃত abstraction কীভাবে সরাতে হয় — সব পরিষ্কার হয়ে যাবে।
Replace Inheritance with Delegation: কাউন্টার ভাড়া নাও, দোকান উত্তরাধিকারে নিও না
Replace Inheritance with Delegation রিফ্যাক্টরিং শেখো একটা মিষ্টির দোকানের গল্প দিয়ে — composition over inheritance-এর আসল মানে, fragile base class সমস্যা, আর TypeScript ও C#-এ ধাপে ধাপে রূপান্তর।