Extract Subclass: বিশেষ কেসটাকে তার নিজের class দাও
Extract Subclass শেখো একটা দর্জির দোকানের জরুরি অর্ডারের গল্পের মধ্যে দিয়ে — TypeScript আর C#-এ flag সরানোর পদ্ধতি, নিরাপদ ধাপে ধাপে কাজ, আর কখন subclassing ভুল সিদ্ধান্ত সেটাও বুঝবে।
🧵 দর্জির একটা মোটা অর্ডার বই
ধরো, ওস্তাদ জামাল লক্ষ্মীবাজার মার্কেটে একটা বিখ্যাত দর্জির দোকান চালায়। ত্রিশ বছর ধরে সে প্রতিটা অর্ডার লেখে একটা মোটা বাদামি অর্ডার বইয়ে। বেশিরভাগ অর্ডার সাধারণ: মাপ নাও, সাত দিনে সেলাই করো, তালিকার দামে বিল করো।
কিন্তু কিছু অর্ডার জরুরি। রোববার বিয়ে। শুক্রবার স্কুলের অনুষ্ঠান। জরুরি অর্ডারে নিয়ম আলাদা: কাস্টমার ৩০% বাড়তি দেয়, অর্ডার সেলাইয়ের লাইনে আগে যায়, আর ওস্তাদের সহকারী রুবেলকে ডেলিভারির একদিন আগে ফোন করতে হয়।
আজকাল দোকানটা এভাবে চলছে। বড় বইয়ের প্রতিটা পাতায় একটা ছোট বক্স: জরুরি? হ্যাঁ/না। আর দোকানের সবাই সেটা চেক করে। ইমরান, বিলিং-এর ভাতিজা, বিল লেখার আগে চেক করে — জরুরি হলে ৩০% যোগ করো। সেলাই টিম পরের কাজ তুলে নেওয়ার আগে চেক করে — জরুরি হলে আগে নাও। রুবেল প্রতি সন্ধ্যায় প্রতিটা পাতা চেক করে — জরুরি হলে ফোন করো। পাতায় আরও extra কলাম আছে — "অনুষ্ঠানের তারিখ" আর "ফোন রিমাইন্ডার দেওয়া হয়েছে?" — যেগুলো সাধারণ অর্ডারে খালিই থাকে, আর বেশিরভাগ পাতাই সাধারণ।
এক বৃহস্পতিবার ইমরান বক্সটা চেক করতে ভুলে গেল। রোববারের বিয়ের জরুরি শেরওয়ানি সাধারণ দামে বিল হলো, সাধারণ সময়ে সেলাই হলো, রুবেলও ফোন করল না। শনিবার রাতে কনের বাবার রাগী ভাই দোকানে এসে হাজির। ওস্তাদ জামাল রাত ৩টা পর্যন্ত জেগে নিজে শেরওয়ানি শেষ করল, ক্ষমা চাইতে ছাড় দিল। একটা হ্যাঁ/না বক্স মিস করে ত্রিশ বছরের সুনাম প্রায় শেষ।
ওস্তাদের মেয়ে ফাতেমা, যে commerce পড়ে, সব দেখে একটা সহজ সমাধান বলল: জরুরি অর্ডারের জন্য আলাদা একটা লাল অর্ডার বই রাখো। লাল বইয়ের প্রতিটা পাতা এমনিতেই জরুরি নিয়ম মানে — ৩০% চার্জ দামের ফর্মুলায় ছাপা, লাইনের নিয়ম বইয়েরই নিয়ম, রিমাইন্ডার কলাম প্রতিটা পাতায় আছে আর কখনো কারণ ছাড়া খালি থাকে না। কেউ আর হ্যাঁ/না বক্স চেক করে না। অর্ডারটা কোন বইয়ে আছে সেটাই উত্তর।
কোডে এই সমাধানকে বলে Extract Subclass। যখন একটা class গোপনে দুটো কাজ করছে — একটা সাধারণ কেস আর একটা বিশেষ কেস, একটা flag দিয়ে আলাদা — তখন বিশেষ কেসটাকে তার নিজের subclass দিয়ে দাও।
📕 Extract Subclass মানে কী?
একটু ভাবো — একটা class-এর কাজ কী? একটা ধারণা পরিষ্কারভাবে বর্ণনা করা। কিন্তু class প্রায়ই দ্বিতীয় একটা পরিচয় গড়ে তোলে চুপচাপ। তিনটা লক্ষণ দিয়ে এই সমস্যা চেনা যায়:
| লক্ষণ | দেখতে কেমন | ওস্তাদ জামালের দোকানে |
|---|---|---|
| Type flag | isUrgent: boolean বা kind: "normal" or "urgent"-এর মতো field যেটা instance-গুলোকে ভাগ করে | প্রতিটা পাতায় জরুরি? হ্যাঁ/না বক্স |
| সর্বত্র conditional | অনেক method-এ একই if (this.isUrgent) শাখা | ইমরান, সেলাই টিম, আর রুবেল প্রত্যেকে বক্স আবার চেক করছে |
| মাঝেমাঝে খালি field | শুধু flag চালু থাকলে পূরণ হয়, বাকি সময় null | "অনুষ্ঠানের তারিখ" আর "রিমাইন্ডার দেওয়া হয়েছে?" কলাম বেশিরভাগ পাতায় খালি |
তৃতীয় লক্ষণটার নিজস্ব নাম আছে — Temporary Field smell। এটা চুপচাপ ক্ষতি করে। খালি কলামে কেউ crash করে না; মানুষ ধীরে ধীরে কোনো কলামকেই বিশ্বাস করা বন্ধ করে দেয়।
Extract Subclass বলে: একটা নতুন class বানাও UrgentOrder extends Order। এতে শুধু-জরুরি field গুলো নামিয়ে দাও। যে method গুলো flag দেখে শাখা তৈরি করত, সেগুলোর স্বাভাবিক আচরণ parent-এ রাখো আর subclass-এ জরুরি আচরণ দিয়ে override করো। তারপর flag মুছে দাও।
Refactoring-এর পরে "এই অর্ডার কি জরুরি?" প্রশ্নের উত্তর দেয় type system, runtime চেক নয়। order.totalPrice() ডাকলে এমনিতেই সঠিক ফর্মুলা চলে, কারণ object নিজেই জানে সে কী। এটাই polymorphism — একবারের জন্য if-check করে দিচ্ছে, চিরতরে।
একটা ছোট নোট: Fowler-এর প্রথম edition-এ এটাকে Extract Subclass বলা হতো; দ্বিতীয় edition-এ এটা Replace Type Code with Subclasses-এর মধ্যে মিলিয়ে গেছে। Refactoring Guru ক্লাসিক Extract Subclass নামটা রেখেছে। একই ওষুধ, বোতলে দুটো লেবেল।
দোকানটার সিদ্ধান্তের জায়গাটা একটা flowchart-এ বসে:
মাঝের প্রশ্নটা — অর্ডার দেওয়ার সময় কি জরুরিত্ব ঠিক হয়ে যায়? — দেখতে সহজ কিন্তু খুব গুরুত্বপূর্ণ। সতর্কতার অংশে এটায় ফিরে আসব।
এক লাইনে: Extract Subclass একটা flag-পাহারাদেওয়া বিশেষ কেসকে তার নিজের subclass-এ নিয়ে যায়, যাতে ছড়িয়ে থাকা if-check গুলো overridden method হয় আর মাঝেমাঝে খালি field গুলো এমন class পায় যেখানে সেগুলো সবসময় অর্থবহ।
🔔 কখন এটার দরকার হয়?
Extract Subclass ব্যবহার করো যখন দেখবে:
- একই flag অনেক method-এ চেক হচ্ছে।
totalPrice(),queuePosition(),needsReminder()-এ বারবারif (isUrgent)দেখা মানে variant logic সারা class জুড়ে ছড়িয়ে আছে। নতুন কোনো জরুরি নিয়ম এলে প্রতিটা শাখা খুঁজে বের করতে হবে। - একটা class দুটো কাজ করছে। সাধারণ কেস আর বিশেষ কেস দুটোই model করা মানে Large Class-এর পথে হাঁটা — দ্বিগুণ field, দ্বিগুণ শাখা, পড়তে দ্বিগুণ কষ্ট।
- শাখাগুলোতে copy-paste।
ifআরelseবেশিরভাগ কোড সামান্য পার্থক্য নিয়ে repeat করলে তুমি আসলে এক class-এর ভেতরে লুকানো Duplicate Code টেনে নিয়ে বেড়াচ্ছ। - Field গুলো সাধারণত null। সব সাধারণ অর্ডারে শুধু-জরুরি data রাখা অপচয় আর "এটা এখানে null হতে পারে?" চিন্তার কারণ।
বিশেষ কেসটা আসলে কত বড়? ফাতেমা লাল বই প্রস্তাব করার আগে বাদামি বইটা গুনে দেখেছিল:
ত্রিশ শতাংশ অর্ডার জরুরি — তবুও প্রতিটা পাতায় জরুরি কলাম বহন করত, প্রতিটা কর্মী প্রতিটা অর্ডারে বক্স চেক করার মূল্য দিত। বিশেষ কেস বিরল না হলেও নিজের class পাওয়ার যোগ্য; দরকার শুধু সে আলাদা হোক।
আর জানো কখন ব্যবহার করা উচিত না:
- Runtime-এ ধরন বদলায়। বুধবার হঠাৎ জরুরি হয়ে যাওয়া অর্ডার তার class বদলাতে পারবে না — কোনো ভাষাই object-কে জীবনের মাঝখানে type swap করতে দেয় না। বরং State/Strategy pattern ব্যবহার করো (চিত্র ২-এর "swappable rule card")।
- Variation ছোট। একটা extra field, কোনো behaviour পার্থক্য নেই? একটা সাধারণ optional field বা parameter পুরো class-এর চেয়ে সস্তা।
- Child যেগুলো reject করতে বাধ্য হবে সেগুলো inherit করতে হচ্ছে। নতুন subclass যদি parent-এর অর্ধেক method-এ "not supported" ছুঁড়ে দিতে বাধ্য হয়, তুমি Refused Bequest smell তৈরি করছ। বিশেষ কেসটা হয়তো আদৌ parent-এর "একটা ধরন" না।
পুরো সিদ্ধান্তটা একটা quadrant-এ সাজানো যায়:
জরুরি অর্ডার উপরের বাম কোণে — কাস্টমার ঢোকার মুহূর্তে ধরন ঠিক, behaviour অনেক আলাদা — একদম subclass-এর জন্য। অর্ডারের status (received, stitching, ready, delivered) উপরের ডান কোণে: এটা object-এর জীবনে ক্রমাগত বদলায়, তাই state object হতে হবে, কখনো subclass নয়। Gift wrapping নিচের বাম কোণে — behaviour-বিহীন একটা boolean — এটাকে field হিসেবে রাখো আর এগিয়ে যাও।
কলেজ কর্নার: কেন একটা object runtime-এ তার class বদলাতে পারে না? কারণ C#, Java, আর বেশিরভাগ class-based ভাষায় allocation-এর সময়ই object-এর type তার memory layout আর method dispatch table (vtable) নির্ধারণ করে। UrgentOrder-এ "পরিণত হওয়া" মানে re-allocate করা আর প্রতিটা বিদ্যমান reference আবার point করা — runtime নিরাপদে এটা করতে পারে না। State pattern এটা এড়ায় object-এর identity স্থিতিশীল রেখে একটা ভেতরের reference swap করে: order.pricing = new UrgentPricing()। Identity থাকে, behaviour বদলায়। পরীক্ষার keyword: variation dynamic হলে inheritance-এর চেয়ে composition পছন্দ করো।
🔍 আগে আর পরে এক নজরে
TypeScript-এ ওস্তাদ জামালের অর্ডার বই, flag-সহ:
// BEFORE: one class, two personalities, flag-checks everywhere
class TailorOrder {
constructor(
public garment: string,
public listPrice: number,
public isUrgent: boolean, // the type flag
public functionDate?: Date, // empty for normal orders
) {}
totalPrice(): number {
if (this.isUrgent) {
return this.listPrice * 1.3; // 30% urgent charge
}
return this.listPrice;
}
queuePriority(): number {
return this.isUrgent ? 1 : 10; // urgent jumps the queue
}
needsReminderCall(): boolean {
return this.isUrgent; // assistant phones only urgent customers
}
}Extract Subclass-এর পরে — লাল বই আছে:
// AFTER: two honest classes, zero flag-checks
class TailorOrder {
constructor(public garment: string, public listPrice: number) {}
totalPrice(): number { return this.listPrice; }
queuePriority(): number { return 10; }
needsReminderCall(): boolean { return false; }
}
class UrgentOrder extends TailorOrder {
constructor(garment: string, listPrice: number, public functionDate: Date) {
super(garment, listPrice);
}
override totalPrice(): number { return this.listPrice * 1.3; }
override queuePriority(): number { return 1; }
override needsReminderCall(): boolean { return true; }
}লাভ গুনে দেখো: isUrgent flag গেছে, তিনটা if-check গেছে, আর functionDate এখন সবসময় শুধু সেই class-এ পূরণ থাকে যার এটা আছে। একটা সাধারণ অর্ডার জরুরি-শুধু কিছু বহনই করতে পারে না।
আর দেখো বিলিং কীভাবে বদলায়। আগে ইমরানকে প্রতিটা অর্ডার জিজ্ঞাসাবাদ করতে হতো; এখন object নিজেই সে যা তাই:
If-check-এর সংখ্যা একটা bar chart-এ একই গল্প বলে:
বড় দোকানের codebase-এ (billing, queueing, reminders, reports, SMS module...) নয়টা check শূন্যে নেমে এলো — আসলে একটায়, কিন্তু সেই একটা একটা নির্দিষ্ট জায়গায় থাকে, পরের অংশে দেখব।
🪜 ধাপে ধাপে, নিরাপদভাবে
ছোট পদক্ষেপ নাও। প্রতিটার পর compile করো আর test করো। Refactoring পাঁচটা compile-clean state-এর মধ্য দিয়ে যায়:
- খালি subclass তৈরি করো। শুধু shell, original class extend করে। কিছু ভাঙে না, এখনো কিছু বদলায়নি।
// INTERMEDIATE STEP: empty subclass, flag still alive
class UrgentOrder extends TailorOrder {
constructor(garment: string, listPrice: number, functionDate: Date) {
super(garment, listPrice, /* isUrgent */ true, functionDate);
}
}- Creation একটা factory দিয়ে route করো। যেখানে original class construct হচ্ছে প্রতিটা জায়গা খুঁজে বের করো। Direct construction-কে একটা factory function দিয়ে replace করো যেটা raw input দেখে সঠিক class বাছে। এটাই সেই একমাত্র জায়গা যেখানে একটা kind-check টিকে থাকার অনুমতি আছে।
function createOrder(garment: string, price: number, functionDate?: Date): TailorOrder {
return functionDate
? new UrgentOrder(garment, price, functionDate)
: new TailorOrder(garment, price, false);
}-
Variant-শুধু field নিচে নামাও।
functionDateশুধুমাত্রUrgentOrder-এর। এটা Push Down Field ব্যবহার করে সরাও — Push Down Method-এর data-সংক্রান্ত cousin। Parent constructor একটা parameter হারায়; subclass সেটা রাখে। -
একটা করে branching method override করো।
totalPrice()নাও।if-এর জরুরি অংশUrgentOrder-এ override-এ রাখো; স্বাভাবিক অংশ parent-এ থাকুক; conditional মুছে দাও। Test চালাও। তারপরqueuePriority()। তারপরneedsReminderCall()। -
Flag মুছো। কোনো method আর
isUrgentনা পড়লে, field আর তার constructor parameter সরাও। Compiler নিশ্চিত করবে কেউ miss করছে না। -
পুরো test suite চালাও। একই behaviour, পরিষ্কার আকার।
৪ নম্বরের আগে ২ নম্বর করো। Override শুরু করার আগেও যদি callers isUrgent: true দিয়ে base class construct করতে থাকে, সেই object গুলো হবে base-class instance যার urgent data আছে কিন্তু normal behaviour — একটা নীরব bug। Creation-কে প্রথমে সঠিক class বাছতে হবে; তবেই behaviour override-এ সরানো নিরাপদ।
🧮 একটা বড় বাস্তব উদাহরণ
ওস্তাদ জামালের দোকান ডিজিটাল হয়ে গেছে, আর order class-এ তৃতীয় একটা উদ্বেগ যোগ হয়েছে: bulk স্কুল-ইউনিফর্ম অর্ডার, নিজস্ব ছাড় আর ডেলিভারি নিয়ম নিয়ে। এখন একটা class-এ দুটো flag লড়াই করছে:
// BEFORE: two flags, four possible (and two impossible) combinations
class StitchingOrder {
constructor(
public garment: string,
public listPrice: number,
public quantity: number,
public isUrgent: boolean,
public isBulk: boolean,
public functionDate?: Date,
public schoolName?: string,
) {}
totalPrice(): number {
let price = this.listPrice * this.quantity;
if (this.isBulk) price = price * 0.85; // 15% bulk discount
if (this.isUrgent) price = price * 1.3; // 30% urgent charge
return price;
}
deliveryDays(): number {
if (this.isUrgent) return 2;
if (this.isBulk) return 21;
return 7;
}
}Business rule বলছে অর্ডার হয় সাধারণ, নয়তো জরুরি, নয়তো bulk — কখনো জরুরি আর bulk একসাথে নয়। কিন্তু flag গুলো এটা express করতে পারে না; isUrgent: true, isBulk: true খুশি মনে compile হয় আর অর্থহীন দাম দেয়। Subclass এটা ঠিক করে, কারণ একটা object-এর ঠিক একটাই class থাকে:
// AFTER: three kinds, each impossible to mix up
class StitchingOrder {
constructor(public garment: string, public listPrice: number, public quantity: number) {}
totalPrice(): number { return this.listPrice * this.quantity; }
deliveryDays(): number { return 7; }
}
class UrgentOrder extends StitchingOrder {
constructor(garment: string, listPrice: number, quantity: number, public functionDate: Date) {
super(garment, listPrice, quantity);
}
override totalPrice(): number { return super.totalPrice() * 1.3; }
override deliveryDays(): number { return 2; }
}
class BulkOrder extends StitchingOrder {
constructor(garment: string, listPrice: number, quantity: number, public schoolName: string) {
super(garment, listPrice, quantity);
}
override totalPrice(): number { return super.totalPrice() * 0.85; }
override deliveryDays(): number { return 21; }
}দেখো কীভাবে super.totalPrice() parent-এর base calculation পুনরায় ব্যবহার করছে — subclass গুলো শুধু তাদের পার্থক্য যোগ করছে। এটাই inheritance-এর সঠিক ব্যবহার: shared code উপরে, বিশেষ code নিচে।
কলেজ কর্নার: "অবৈধ state অপ্রতিনিধিত্বযোগ্য করো" ধারণাটা type theory-তে বড় ব্যাপার। দুটো boolean ২ × ২ = ৪টা representable state দেয় এমনকি মাত্র ৩টা legal হলেও; type system খুশি মনে মিথ্যাটা store করে। Subclass-এর একটা closed set (বা যে ভাষায় আছে, sealed class hierarchy — Kotlin ও আধুনিক Java-তে sealed, TypeScript-এ discriminated union, C#-এ abstract sealed pattern) ঠিক legal state গুলো construct করতে দেয়। Compiler তখন switch-এ exhaustively check করতে পারে আর নতুন kind যোগ হলে কিন্তু সামলানো না হলে সতর্ক করে — এই guarantee কোনো boolean flag কখনো দিতে পারবে না।
💼 C#-এ একই refactoring
ধরো একটা billing system যেখানে service item একটা parts class-এর ভেতরে flag দিয়ে লুকিয়ে আছে — আর পরিষ্কার করা version:
// BEFORE
public class BillItem
{
private readonly decimal _unitPrice;
private readonly int _quantity;
private readonly bool _isService; // type flag
private readonly decimal _hourlyRate; // meaningful only for services
public decimal Total()
=> _isService ? _hourlyRate * _quantity
: _unitPrice * _quantity;
}// AFTER
public class BillItem // the common "parts" case
{
private readonly decimal _unitPrice;
protected int Quantity { get; }
public BillItem(decimal unitPrice, int quantity)
=> (_unitPrice, Quantity) = (unitPrice, quantity);
public virtual decimal Total() => _unitPrice * Quantity;
}
public class ServiceItem : BillItem // the extracted special case
{
private readonly decimal _hourlyRate;
public ServiceItem(decimal hourlyRate, int hours) : base(0m, hours)
=> _hourlyRate = hourlyRate;
public override decimal Total() => _hourlyRate * Quantity;
}C#-এ গুরুত্বপূর্ণ বিষয়: parent method-কে virtual চিহ্নিত করতে হবে আর child-কে override, নইলে child version চুপচাপ override না হয়ে লুকিয়ে যায়। কোন class বানাবে তা সিদ্ধান্ত নেওয়া factory প্রায়ই একটা static method হয় — BillItem.From(dto) — যেখানে একমাত্র টিকে থাকা switch থাকে, আর আধুনিক C# pattern matching এটাকে সুন্দর পড়ার মতো করে: dto switch { { HourlyRate: > 0 } => new ServiceItem(...), _ => new BillItem(...) }।
🐍 Python-এ factory
Python-এ compiler নেই যে নিশ্চিত করবে flag শেষ হলো কিনা, তাই factory হয় সেই official সীমানা যেখানে raw input সঠিক class-এ পরিণত হয়:
# AFTER: the one surviving kind-check lives in the factory
from datetime import date
class TailorOrder:
def __init__(self, garment: str, list_price: int) -> None:
self.garment = garment
self.list_price = list_price
def total_price(self) -> float:
return self.list_price
def queue_priority(self) -> int:
return 10
class UrgentOrder(TailorOrder):
def __init__(self, garment: str, list_price: int, function_date: date) -> None:
super().__init__(garment, list_price)
self.function_date = function_date # always set, never None
def total_price(self) -> float:
return self.list_price * 1.3
def queue_priority(self) -> int:
return 1
def create_order(garment: str, price: int, function_date: date | None = None) -> TailorOrder:
if function_date is not None:
return UrgentOrder(garment, price, function_date)
return TailorOrder(garment, price)এর পরে, project-এ is_urgent খোঁজো — প্রতিটা hit যা পাবে সেটা একটা বাকি-থাকা box-check, যেটা লাল বই retire করার কথা ছিল।
🛠️ IDE সাপোর্ট
কোনো mainstream IDE-তে literal one-click "Extract Subclass" বোতাম নেই। কারণ পদক্ষেপটা আসলে ছোট ছোট supported পদক্ষেপের একটা recipe — আর IDE গুলো প্রতিটা পদক্ষেপ automate করে:
- IntelliJ IDEA / Rider: subclass তৈরি করো, তারপর Refactor → Push Members Down ব্যবহার করে variant field আর method গুলো parent থেকে সরাও। Push Members Down dialog প্রতিটা member-এর জন্য checkbox দেখায়, Pull Members Up-এর ঠিক mirror। Refactor → Replace Constructor with Factory Method আমাদের ২ নম্বর পদক্ষেপের creation-site routing automate করে।
- ReSharper / Rider (C#): Push Members Down আর Change Signature constructor রদবদল সামলায়; ReSharper সতর্ক করে যদি pushed member এখনো base type দিয়ে referenced থাকে।
- VS Code / WebStorm (TypeScript): Rename Symbol আর compiler-এর উপর নির্ভর করে। TypeScript-এর
overridekeyword (noImplicitOverrideচালু থাকলে) সেই classic mistake ধরে যেখানে override spell ভুল হয়ে নতুন method হয়ে যায়।
সর্বত্র সৎ workflow হলো: হাতে shell subclass, refactoring tool দিয়ে factory, refactoring tool দিয়ে members নিচে, হাতে flag মুছো compiler দেখতে দেখতে।
📊 সুবিধা আর ঝুঁকি
| দিক | সুবিধা | ঝুঁকি / খরচ |
|---|---|---|
| Conditional | Flag-check গুলো polymorphic override-এ মিলিয়ে যায় | Factory-তে একটা creation-time switch থাকে (স্বাস্থ্যকর) |
| Field | Variant-শুধু field গুলো তাদের class-এ সবসময় valid | Codebase-এ navigate করতে বেশি class |
| নতুন variant | একটা subclass যোগ করো; বিদ্যমান method-এ হাত দিতে হয় না | অতি-উৎসাহী extraction পাতলা, অর্থহীন subclass তৈরি করে |
| Type safety | অসম্ভব flag combination construct করা যায় না | প্রতি dimension-এ subclass দুটো varying dimension-এ scale করে না |
| Runtime পরিবর্তন | — | একটা object জীবনের মাঝখানে class বদলাতে পারে না; পরিবর্তনযোগ্য kind-এর State/Strategy দরকার |
| Readability | প্রতিটা class একটা গল্প বলে | পাঠককে এখন পুরো ছবির জন্য দুটো file দেখতে হবে |
পরে যদি দেখো একটা subclass আসল পার্থক্য কখনো বাড়াল না, inverse refactoring — Collapse Hierarchy — এটাকে parent-এ ভাঁজ করে ফেলে। Extract করা আর collapse করা দুটোই সস্তা; ভুল আকারে আটকে থাকাটাই দামি।
কলেজ কর্নার: নতুন subclass-কে Liskov Substitution Principle মানতে হবে — TailorOrder reference রাখা যেকোনো code-কে UrgentOrder দিলেও ঠিকমতো কাজ করতে হবে। আমাদের override গুলো এই test পাস করে কারণ সেগুলো parent-এর প্রতিশ্রুত অর্থের মধ্যে মান বদলায় (দাম, অগ্রাধিকার), চুক্তি নয়। যে override totalPrice()-কে unpaid customer-এর জন্য throw করায় সেটা LSP ভাঙবে আর তোমার সুন্দর লাল বইকে Refused Bequest factory বানাবে। পরীক্ষার rule of thumb: override আরও নির্দিষ্ট হতে পারে, কখনো আরও দাবিদার নয়।
🧪 এটা কোন smell সারায়?
| Smell | Extract Subclass কীভাবে সাহায্য করে |
|---|---|
| Temporary Field | Variant-শুধু field গুলো এমন class-এ যায় যেখানে সেগুলো সবসময় পূরণ থাকে, কখনো default-null নয় |
| Large Class | Overloaded class একটা focused parent আর ছোট focused child-এ বিভক্ত হয় |
| Switch Statements | একই flag-এ বারবার branching override-এ গলে যায়; একটা factory switch থাকে |
| Duplicate Code | প্রায়-একই if/else arm হয় একটা shared parent method আর একটা ছোট override |
| Refused Bequest | সারানো হয় না, প্রতিরোধ করা হয় — শুধু তখনই extract করো যখন child সত্যিই parent-এর "একটা ধরন", নইলে এই smell তৈরি হয় |
🧠 পুরো ধারণা এক মানচিত্রে
📦 দ্রুত revision বক্স
+--------------------------------------------------------------+
| EXTRACT SUBCLASS |
+--------------------------------------------------------------+
| Problem : One class, two jobs. A flag (isUrgent) splits |
| instances; methods if-check it; some fields |
| stay empty for the normal case. |
| Story : Tailor's one fat order book -> separate RED |
| book for urgent orders; red book's own rules |
| replace every YES/NO box check. |
| Fix : 1. Create empty subclass |
| 2. Factory picks the class at creation |
| 3. Push variant fields down |
| 4. Turn each if-branch into an override |
| 5. Delete the flag |
| Cures : Temporary Field, Switch Statements, Large Class |
| Beware : Kind changes at runtime? Use State/Strategy. |
| Tiny variation? A field is cheaper than a class. |
| Inverse : Collapse Hierarchy |
+--------------------------------------------------------------+✍️ অনুশীলন প্রশ্ন
ধরো, একটা cinema booking system-এ এই class আছে:
class Ticket {
constructor(
public movie: string,
public basePrice: number,
public isPremium: boolean, // recliner seats, food included
public seatRow?: string, // only premium tickets choose a row
) {}
finalPrice(): number {
if (this.isPremium) return this.basePrice * 2 + 250; // seat + meal
return this.basePrice;
}
entryGate(): string {
return this.isPremium ? "Gate P (lounge)" : "Gate A";
}
includesSnacks(): boolean {
return this.isPremium;
}
}কীভাবে করবে? এই কাজগুলো করো:
- এই class-এ রোগের তিনটা লক্ষণ তালিকা করো এই post-এর symptom table ব্যবহার করে (flag, branching method, sometimes-empty field)।
- নিরাপদ পদক্ষেপ follow করে একটা
PremiumTicketsubclass বের করো: আগে খালি subclass, তারপর একটাcreateTicket(...)factory, তারপরseatRowনিচে নামাও, তারপর তিনটা method override-এ রূপান্তর করো, তারপরisPremiumমুছো। চিত্র ৮-এর state diagram-এর সাথে মিলিয়ে দেখো — প্রতিটা state-এ কি compile করতে পারতে? - যাচাই করো: তোমার refactoring-এর পরে, কোনো non-premium ticket কি
seatRowবহন করতে পারে? কোনো method-এ কি এখনোisPremiumথাকতে পারে? - Bonus চিন্তার প্রশ্ন: cinema টা "সকালের show" ticket পরিকল্পনা করছে অর্ধেক দামে — কিন্তু একটা premium ticket-ও সকালের show হতে পারে। কেন একটা
MorningPremiumTicketsubclass কষ্টদায়ক হতে শুরু করে, আর কোন pattern দ্বিতীয় dimension আরও ভালো সামলায়? (Hint: ৩টা seat type × ৪টা show timing-এর জন্য কতটা class লাগবে গুনে দেখো।) - College-level bonus: চিত্র ৪-এর quadrant-এ "ticket-এর ধরন" আর "show timing" রাখো আর দুই বাক্যে justify করো কেন একটা subclass উপাদান আর অন্যটা নয়।
তোমার Ticket class-এ যদি কোনো flag না থাকে, কোনো optional field না থাকে, আর কোনো conditional না থাকে — তাহলে লাল অর্ডার বই খোলা হয়েছে। আর ওস্তাদ জামাল তোমার নাম দোকানের সম্মানের তালিকায় সেলাই করে দেবে।
সচরাচর জিজ্ঞাসা
- Extract Subclass আর Extract Superclass-এর পার্থক্য কী?
- এরা উল্টো দিকে কাজ করে। Extract Subclass শুরু হয় একটা class থেকে যেটা দুটো কাজ করছে — বিশেষ কাজটা নিচে একটা নতুন child class-এ নামিয়ে দেওয়া হয়। Extract Superclass শুরু হয় দুটো মিলতে-জুলতে class থেকে — তাদের মিলের অংশ উপরে তুলে একটা নতুন parent class বানানো হয়। একটা হলো উপর থেকে নিচে specialisation, অন্যটা নিচ থেকে উপরে generalisation।
- কখন Extract Subclass ব্যবহার করা উচিত না?
- যখন object-এর জীবনে 'ধরন' বদলাতে পারে। একটা object কখনো runtime-এ নিজের class বদলাতে পারে না — একটা সাধারণ অর্ডার হঠাৎ জরুরি হয়ে গেলে সে Order থেকে UrgentOrder-এ যেতে পারবে না। যে ধরন বদলায় সেখানে State বা Strategy object ব্যবহার করো, যেগুলো যেকোনো সময় swap করা যায়।
- Extraction-এর পর boolean flag-এর কী হয়?
- সেটা পুরোপুরি মুছে যায়। এটা যে তথ্য বহন করত — 'এটা কি জরুরি?' — সেটা এখন object-এর class নিজেই বলে দেয়। একটা UrgentOrder তার type-এই জরুরি। সাধারণত একটাই switch বা if টিকে থাকে একটা factory function-এ, যেটা raw input দেখে কোন class বানাবে ঠিক করে — এটা স্বাভাবিক আর স্বাস্থ্যকর।
- শুধু একটা extra field-এর জন্য subclass বানানো কি দরকার?
- বেশিরভাগ সময় না। যদি variant শুধু একটা ছোট value-তে আলাদা হয় আর কোনো behaviour না বদলায়, তাহলে একটা plain field বা ছোট strategy object পুরো নতুন class-এর চেয়ে সস্তা। Extract Subclass তখনই কাজে লাগে যখন variant-এর নিজের field আর নিজের behaviour দুটোই আছে — আলাদা calculation, আলাদা নিয়ম, আলাদা validation।
- একটা class থেকে কি একাধিক subclass বের করা যায়?
- হ্যাঁ, যদি type code-এ একাধিক মান থাকে — যেমন normal, urgent, আর bulk অর্ডার হতে পারে Order, UrgentOrder, আর BulkOrder। কিন্তু একসাথে দুটো স্বাধীন dimension-এ subclassing এড়িয়ে চলো, কারণ তখন combination সংখ্যা গুণ হয়ে বাড়ে। একটা dimension-এর জন্য subclass রাখো, অন্যটার জন্য composition ব্যবহার করো।
আরো দেখো
সম্পর্কিত পাঠ
Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি
Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।
Large Class: যে স্কুলের ব্যাগে সব কিছু থাকে
Large Class code smell কী সেটা বুঝো — কেন god class বড় হয়, low cohesion কীভাবে চেনা যায়, আর Extract Class দিয়ে কীভাবে ছোট ছোট focused class-এ ভাগ করা যায়।
Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা
বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।
Push Down Method: যে Method শুধু একটা Subclass ব্যবহার করে, সেটা সেখানে নামিয়ে দাও
Push Down Method শিখো একটা স্কুলের office গল্পের মাধ্যমে — superclass-এর সৎ contract, TypeScript আর C#-এ নিরাপদ ধাপে ধাপে move, আর কীভাবে এটা Refused Bequest smell ঠিক করে।