মূল বিষয়বস্তুতে যান
Clean Code Mastery

Collapse Hierarchy: যখন Parent আর Child Class একই হয়ে যায়

একটা মহল্লার কমিটির গল্পের মাধ্যমে Collapse Hierarchy refactoring শেখো — TypeScript আর C#-এ superclass আর subclass মার্জ করার ধাপে ধাপে পদ্ধতি, আর কখন বুঝবে একটা hierarchy আর কাজে আসছে না।

22 মিনিট আপডেট: June 11, 2026intermediate
refactoringinheritancehierarchylazy classclass designtypescriptcsharp

কমিটি আর সাব-কমিটির গল্প

ধরো ঢাকার মিরপুরে "সবুজ আবাস" নামে একটা আবাসিক এলাকা আছে, চল্লিশটা ফ্ল্যাট নিয়ে। সব এলাকার মতোই একটা ম্যানেজিং কমিটি আছে — সাত জন সদস্য, যারা রক্ষণাবেক্ষণের টাকা, দারোয়ানের বেতন, পানির ট্যাংকার, লিফট মেরামত সব সামলায়। জামাল সাহেব চেয়ারম্যান। সালাম সাহেব হিসাব রাখেন। আর সচিব নাসরিন আপা প্রতিটা মিটিংয়ের কার্যবিবরণী একটা বড় লাল খাতায় লেখেন।

বারো বছর আগে এলাকার ঈদ উৎসব অনেক বড় হয়ে গেল। প্যান্ডেল, আলোকসজ্জা, দুইশো মানুষের খাবার, বাচ্চাদের সাংস্কৃতিক অনুষ্ঠান। ম্যানেজিং কমিটি বললো, "এত বাড়তি কাজ আমাদের পক্ষে সামলানো কঠিন।" তাই তৈরি হলো একটা উৎসব সাব-কমিটি। নতুন খাতা (সবুজ রঙের), শুক্রবার বিকেলে আলাদা মিটিং, লিফটের কাছে নিজস্ব নোটিশ বোর্ড। সেই বছর ব্যাপারটার মানে ছিল — আলাদা সদস্য (বি-ব্লকের তরুণ রুবেল নেতৃত্ব দিতো), আলাদা কাজ, আর একটা আলাদা ছোট বাজেট।

কিন্তু বারো বছর পরে দেখো:

  • উৎসব ছোট হয়ে গেছে — সাত দিন থেকে দুই দিন।
  • সাজসজ্জার কাজ ঠিকাদারকে দেওয়া হয়েছে, আর ঠিকাদার মূল কমিটির সাথেই কথা বলে।
  • উৎসবের হিসাব "অডিটের সুবিধার জন্য" মূল ব্যাংক অ্যাকাউন্টে মিশিয়ে দেওয়া হয়েছে।
  • রুবেল ফ্ল্যাট বেচে চলে গেছে। তার জায়গায় এলেন জামাল সাহেব নিজে। বাকি দুই সদস্যও গেলেন — তাদের জায়গায় এলেন সালাম সাহেব আর নাসরিন আপা।

এখন উৎসব সাব-কমিটিতে একই সাত জন, একই রোববার মিটিং, একই আলোচনা। আসল পার্থক্য শুধু কাগজপত্রে। নাসরিন আপাকে একই কার্যবিবরণী দুইবার লিখতে হয় — লাল খাতায় একবার, সবুজ খাতায় একবার। প্রতিটা নোটিশ দুটো বোর্ডে যায়। অডিটর আসলে সালাম সাহেব একই সংখ্যা দুইবার ব্যাখ্যা করেন। গত বছর তরুণ তারিক ৩০৪ নম্বর ফ্ল্যাট কিনে জিজ্ঞেস করলো, "উৎসবের চাঁদার জন্য কার সাথে কথা বলবো?" — তিনজন প্রতিবেশী তিন রকম উত্তর দিলো। কারণ কেউ নিশ্চিত না কোন কমিটি কী দেখভাল করে।

গত মাসে বার্ষিক সাধারণ সভায় নাসরিন আপা উঠে দাঁড়িয়ে দুটো খাতা তুলে ধরে বললেন: "এই দুটো বইয়ে একই হাতের লেখায় একই কথা। দুটো কমিটি যেগুলো আসলে একটা — এটা কেন রক্ষণাবেক্ষণ করছি? সাব-কমিটিকে মূল কমিটিতে মিশিয়ে দিই। একটা খাতা, একটা নোটিশ বোর্ড। আমরা যা করি তা বদলাবে না — শুধু বাড়তি ঝামেলাটা যাবে।"

সবাই দুই মিনিটেই একমত হলো। জামাল সাহেব, যিনি সাব-কমিটি বানিয়েছিলেন, তিনি প্রথমে হাত তুললেন। সেই AGM-এর সিদ্ধান্তটাই আজকের refactoring। কোডে, যখন একটা subclass আর তার superclass ধীরে ধীরে একই জিনিস হয়ে যায় — একই member, একই behaviour, কোনো আসল পার্থক্য নেই — তখন আমরা সেগুলো একটা class-এ মার্জ করি। এটাই Collapse Hierarchy

চিত্র ১: দুটো কমিটি থেকে একটা কমিটিতে — double paperwork শেষে একটা সত্যে পৌঁছানো

Collapse Hierarchy কী?

Collapse Hierarchy হলো সেই class hierarchy-র জন্য একটা refactoring যেগুলো তাদের কারণ হারিয়ে ফেলেছে। একসময় subclass-টা সত্যিকারের কিছু যোগ করতো — বাড়তি field, override করা method, একটা আলাদা variant। কিন্তু সোসাইটির মতোই কোডও বদলায়। Method-গুলো parent-এ উঠে গেছে। Feature-গুলো মুছে গেছে। পার্থক্যগুলো একটু একটু করে সমান হয়ে গেছে। কেউ plan করেনি, কিন্তু একদিন subclass-টা ফাঁকা খোল — কোনো নতুন field নেই, কোনো override নেই, শুধু একটা ভিন্ন নাম।

সেই ফাঁকা খোলটা বিনামূল্যে না। Inheritance-এর প্রতিটা স্তর পাঠককে কিছু খরচ করায়:

  • আরও একটা type শিখতে হয়। নতুন teammate দুটো class-এর নাম দেখে দুটো concept ভাবে। আসলে একটাই। (তারিক ভেবেছিল দুটো কমিটি মানে দুটো দায়িত্ব। ভুল ছিল।)
  • আরও একটা file খুলতে হয়। "এই object কী করে?" বুঝতে হলে দুটো class trace করতে হয়, প্রায়ই দুটো আলাদা file-এ।
  • প্রতিটা পরিবর্তনে আরও একটা সিদ্ধান্ত। "এই নতুন method parent-এ যাবে নাকি child-এ?" — কোনো লাভ ছাড়াই বারবার এই প্রশ্ন। নাসরিন আপা প্রতিটা নোটিশ নিয়ে একই প্রশ্ন করতেন: কোন বোর্ডে, নাকি দুটোতেই?
  • পাঠক আর actual behaviour-এর মাঝে আরও একটা layer।

Refactoring-টা সহজ আর সৎ: যে class টিকবে সেটা বেছে নাও, বাকি member-গুলো সেখানে সরাও, সব reference survivor-এর দিকে নির্দেশ করো, অন্য class মুছো। সোসাইটি একটা কমিটি রাখে; কোড একটা class রাখে।

💡

এক লাইনে সারকথা: superclass আর subclass যখন আর দুটো আলাদা class হওয়ার মতো যথেষ্ট আলাদা থাকে না, তখন একটায় মার্জ করো — member-গুলো keeper-এ নিয়ে যাও, প্রতিটা reference পুনর্নির্দেশ করো, আর ফাঁকা খোল মুছো।

এই refactoring-এর পরিবার একটু জানা দরকার। Collapse Hierarchy হলো Extract Subclass-এর ঠিক উল্টো: একটা তৈরি করে child class যখন সত্যিকারের variant দেখা দেয়; অন্যটা child মুছে দেয় যখন variant মিলিয়ে যায়। আর এটা Inline Class-এর hierarchy-flavoured ভাই — সেটাও একই "ছোট class মার্জ করো" কাজ করে, কিন্তু parent-child-এর বদলে দুটো আলাদা collaborator-এর উপর। কখনো hierarchy collapse করার পরে যদি সত্যিকারের variant ফিরে আসে, খারাপ লাগার কিছু নেই — Extract Subclass একটা বিকেলেই সেটা পুনরায় বানিয়ে দেবে, আর এবার সেটা থাকবে কারণ দরকার আছে বলে।

চিত্র ২: Collapse Hierarchy-র idea map — এটা কী, কখন দরকার, আর কাছের আত্মীয়রা কারা

একটু গভীরে যাই: সিনিয়র engineer-রা কেন অপ্রয়োজনীয় hierarchy level অপছন্দ করেন তার একটা বিখ্যাত কারণ আছে — fragile base class problem। প্রতিটা subclass শুধু parent-এর public interface-এর সাথে না, private implementation-এর সাথেও যুক্ত: কোন method কোনটাকে call করে, কী ক্রমে, কী side effect সহ। Inheritance-এর প্রতিটা বাড়তি স্তর সেই সব surface বাড়িয়ে দেয় যেখান দিয়ে একটা class-এ নিরীহ পরিবর্তন অন্যটায় চুপচাপ bug ঢোকাতে পারে। ফাঁকা subclass তোমাকে শূন্য সুবিধা দেয় অথচ সেই সব ঝুঁকির দায় বহন করাতে থাকে। Hierarchy collapse করা শুধু পরিপাটি করা না — এটা এমন একটা coupling channel মুছে দেওয়া যেটা একদিন bug বহন করতে পারতো।

কখন এটা দরকার?

তোমার codebase-এ এই লক্ষণগুলো দেখো:

  • Subclass কিছু যোগ করছে না। Child class খুলে গণো: শূন্য নতুন field, শূন্য নতুন method, শূন্য override — অথবা শুধু এমন override যেগুলো super call করে আর ফলাফল হুবহু ফেরত দেয়। এটাই Lazy Class smell, inheritance-এর পোশাক পরা: এমন class যে তার খরচ উঠাতে পারছে না।
  • Hierarchy বানানো হয়েছিল এমন ভবিষ্যতের জন্য যা আসেনি। "আমাদের নিশ্চয়ই PremiumCustomer আর RegularCustomer কোনোদিন দরকার হবে" — পাঁচ বছর পরে দুটোই একইরকম। এটা Speculative Generality, আর collapse হলো এর সমাধান।
  • Parent আর child প্রায় একই কপি। কখনো convergence দুটো স্তরের মধ্যে Duplicate Code হিসেবে দেখা দেয়। Duplication মুছলে দেখা যায় child-এ আর কিছুই নেই।
  • প্রতিটা পরিবর্তন দুটো class-কে স্পর্শ করে। একটা concept দুটো স্তরে থাকলে একটা bug fix মানে দুটো edit আর দুটো review। নাসরিন আপার দুটো খাতার মতো friction।
  • Refactoring পার্থক্যটা ক্ষয় করে দিয়েছে। Pull Up Method আর Pull Up Field-এর একগুচ্ছ সঠিক move parent-এ সব একত্রিত করেছে। প্রতিটা ধাপ ঠিক ছিল; শেষে একটা class বেশি হয়ে গেছে।

এবার সতর্ক অংশ — কখন collapse করা উচিত নয়:

  • Subclass এখনও real behaviour override করছে, বা caller-রা polymorphically তার উপর নির্ভর করছে। কাজ করা variant lazy class নয়।
  • পাতলা হওয়াটা রোগ নয়, উপসর্গ। Child ফাঁকা যদি হয় কারণ behaviour ভুলভাবে parent-এ তোলা হয়েছে — সমাধান সেটা নিচে নামানো, মার্জ করা নয়।
  • বিপরীত smell দেখা যাচ্ছে। Subclass যদি parent-এর অংশ প্রত্যাখ্যান করে — inherit করে এমন method যেগুলো সে চায় না — সেটা Refused Bequest। এর চিকিৎসা Replace Inheritance with Delegation, মার্জ নয়। Collapse Hierarchy সেই class-গুলোর জন্য যেগুলো একই হয়ে গেছে; Refused Bequest সেগুলোর জন্য যেগুলো কখনো সত্যিকারভাবে সম্পর্কিত ছিল না।

দেখতে একরকম পরিস্থিতিগুলোর মধ্যে পার্থক্য মনে রাখার সহজ উপায়:

পরিস্থিতিকী দেখছোসঠিক পদক্ষেপ
Child কিছু যোগ করছে না, parent আর child একটাই conceptফাঁকা subclass, একই behaviourCollapse Hierarchy (এই পাঠ)
Child parent-এর member প্রত্যাখ্যান বা stub করছেthrow new Error("not supported") overrideReplace Inheritance with Delegation
দুটো collaborating class, একটা ছোটএকটা field আর একটা forwarding method-এর classInline Class
Child ফাঁকা কারণ behaviour ভুলভাবে উপরে নেওয়া হয়েছেParent-এ child-only logic আছেPush Down Method করো, hierarchy রাখো
পরের quarter-এ সত্যিকারের variant ফিরতে পারেProduct roadmap-এ সত্যিই আছেঅপেক্ষা করো, বা collapse করে পরে re-extract করো

"কিছু যোগ করছে না" সততার সাথে মাপবে কীভাবে? Member গণো। একটা সাধারণ collapse candidate-এর audit এরকম দেখায়:

চিত্র ৩: একটা সাধারণ collapse candidate-এর member-by-member audit — প্রায় সবই অপরিবর্তিত inherit করা

"Genuinely new behaviour" slice যদি শূন্য হয় — বা trivial override মুছলে শূন্য হয়ে যায় — hierarchy তার কাজ করা বন্ধ করে দিয়েছে।

এক নজরে আগে আর পরে

এখানে সমস্যার সবচেয়ে ছোট সৎ ছবি। একটা Employee class, আর একটা SalariedEmployee child যেটা একসময় hourly-versus-salaried logic বহন করতো — যতক্ষণ না hourly variant দুই বছর আগে product থেকে বাদ পড়লো:

// BEFORE: a two-level hierarchy holding exactly one concept
class Employee {
  constructor(
    protected name: string,
    protected monthlySalary: number,
  ) {}
 
  pay(): number {
    return this.monthlySalary;
  }
}
 
// Adds nothing: no fields, no methods, no overrides.
// It exists only because it used to mean something.
class SalariedEmployee extends Employee {}
 
// And callers must remember which name to use where...
const emp = new SalariedEmployee("Asha", 52000);

Collapse-এর পরে — একটা class, একটা নাম, একটা জায়গা:

// AFTER: the hierarchy is gone; the concept remains
class Employee {
  constructor(
    protected name: string,
    protected monthlySalary: number,
  ) {}
 
  pay(): number {
    return this.monthlySalary;
  }
}
 
// SalariedEmployee is deleted. Every reference now says Employee.
const emp = new Employee("Asha", 52000);

লক্ষ্য করো কী পরিবর্তন হয়নি: behaviour। pay() আগে আর পরে একই সংখ্যা ফেরত দেয়। Collapse Hierarchy সম্পূর্ণরূপে structural — এটা ceremony মুছে দেয়, function কখনো না। এই refactoring-এর পরে কোনো test-এর ফলাফল বদলে গেলে কোথাও ভুল হয়েছে।

চিত্র ৪: Collapse-এর আগে ফাঁকা খোল real class-এর নিচে ঝুলছে; পরে একটা class পুরো concept বহন করছে

ফাঁকা খোলটা runtime-এ আর পাঠকের মাথায় কী খরচ করায় সেটা দেখা মূল্যবান। প্রতিটা call এখনও দুটো class-এর মধ্য দিয়ে বুঝতে হয়, যদিও শুধু একটাই কিছু করে:

চিত্র ৫: Collapse-এর আগে প্রতিটা পাঠককে real behaviour-এ পৌঁছাতে ফাঁকা subclass-এর মধ্য দিয়ে মানসিকভাবে হাঁটতে হয়

সেই diagram-এর "middle column" হলো নাসরিন আপার সবুজ খাতা: যাত্রার একটা থামার জায়গা যেটা নতুন কিছু লেখে না।

নিরাপদ পথে ধাপে ধাপে

নাসরিন আপার মার্জারের মতো — আগে প্রস্তাব পাস, তারপর খাতা সরাও, তারপর পুরানো নোটিশ বোর্ড সরাও। ছোট ছোট ধাপ, প্রতিটার পরে test সবুজ।

ধাপ ১: Survivor বেছে নাও। সাধারণত superclass টিকে, কারণ বেশিরভাগ কোড general নামটাই ব্যবহার করে। কিন্তু সিদ্ধান্তের আসল প্রশ্ন হলো: মার্জ করা concept-কে কোন নামটা সবচেয়ে সৎভাবে বর্ণনা করে? পুরো codebase যদি SalariedEmployee নিয়ে কথা বলে আর Employee খুব কমই reference হয়, child-এর নাম রাখো। Parent-এর অন্য child-গুলোও চেক করো: যদি Employee-এর একটা জীবিত Contractor subclass থাকে, parent অবশ্যই টিকতে হবে — parent-কে একটা child-এ মার্জ করলে Contractor হয়ে যাবে SalariedEmployee-এর child, যেটা একেবারে আজগুবি।

ধাপ ২: বাকি member-গুলো survivor-এ সরাও। Subclass মুছলে Pull Up Field আর Pull Up Method দিয়ে child-এ এখনো যা আছে সেটা parent-এ তোলো। Superclass মুছলে member নিচে নামাও। একটা একটা করে, মাঝে মাঝে compile আর test করো।

// Intermediate state: the child still exists, but it is now COMPLETELY empty.
// Everything has been pulled up. Tests are green. Nothing is deleted yet.
class Employee {
  constructor(protected name: string, protected monthlySalary: number) {}
  pay(): number { return this.monthlySalary; }
  bonusFor(festival: string): number {        // pulled up from the child
    return festival === "Diwali" ? this.monthlySalary * 0.1 : 0;
  }
}
 
class SalariedEmployee extends Employee {}    // empty — ready for deletion

ধাপ ৩: প্রতিটা reference পুনর্নির্দেশ করো। যে class মুছে যাবে তার নাম যেসব জায়গায় আছে খুঁজে বের করো — new SalariedEmployee(...), type annotation, instanceof check, import statement, test file — আর সেগুলো survivor-এ বদলাও। IDE-র "find all usages" এখানে তোমার বন্ধু; শুধু plain text search-এর উপর ভরসা করো না, কারণ comment আর string-এ reference লুকিয়ে থাকতে পারে।

ধাপ ৪: ফাঁকা class মুছো। কোনো reference না থাকলে মুছে দেওয়া কোনো ব্যাপারই না। Compiler নিশ্চিত করবে।

ধাপ ৫: Compile করো আর পুরো test suite চালাও। শুধু class-এর কাছের unit test নয় — পুরো suite। Hierarchy construction, type check, আর serialization-এ এমন জায়গা স্পর্শ করে যেগুলো তুমি মনে নাও রাখতে পারো।

ধাপ ৬: Public API ভদ্রভাবে সামলাও। মুছে দেওয়া type যদি library থেকে export করা ছিল, চূড়ান্তভাবে মুছার আগে একটা release cycle-এর জন্য deprecated alias রাখো (/** @deprecated Use Employee */ export const SalariedEmployee = Employee; বা obsolete চিহ্নিত একটা পাতলা subclass)।

পুরো যাত্রাটা একটা state machine হিসেবে:

চিত্র ৬: Collapse-এর নিরাপদ অবস্থাগুলো — দুটো class থেকে সরাসরি deletion-এ লাফ দিও না
⚠️

সবচেয়ে সাধারণ ভুল হলো এমন subclass collapse করা যেটা শুধু প্রায় ফাঁকা। একটা ছোট override — একটা toString, একটা validation tweak, constructor-এ একটা default value — মিলিয়ে গেলে চুপচাপ behaviour বদলে যায়। ধাপ ২-এর আগে child-কে parent-এর সাথে member-by-member তুলনা করো, আর যেকোনো override-এর চারপাশে একটা দ্রুত characterization test লেখো। হয় override-টা মরা (আগে মুছো, prove করো test সবুজ থাকে) নইলে জীবিত (আর hierarchy-টা হয়তো বেঁচে থাকার যোগ্য)।

একটা বড় বাস্তব উদাহরণ

এই গল্পটা প্রায় প্রতিটা codebase-এ হয় যেটা কয়েক বছর বেঁচে থাকে। ধরো একটা notification module একসময় email আর SMS সমর্থন করতো:

// THE PAST: a hierarchy with a real reason to exist
abstract class Notifier {
  constructor(protected recipient: string) {}
  abstract send(message: string): void;
}
 
class EmailNotifier extends Notifier {
  send(message: string): void { /* SMTP magic */ }
}
 
class SmsNotifier extends Notifier {
  send(message: string): void { /* telecom gateway magic */ }
}

তারপর SMS gateway-র চুক্তি শেষ হলো। SmsNotifier মুছে দেওয়া হলো। পরের এক বছরে helpful teammate-রা shared logic উপরে তুলতে লাগলো: retry loop parent-এ গেল, logging গেল, আর একসময় কেউ "duplication কমাতে" send-কে parent-এ concrete করলো। প্রতিটা move সংগত ছিল। ফলাফল এখন এটা:

// THE PRESENT: a hierarchy with no reason left
class Notifier {
  constructor(protected recipient: string) {}
 
  send(message: string): void {
    this.logAttempt(message);
    this.deliverViaSmtp(message);   // the "abstract" part is long gone
  }
 
  protected logAttempt(message: string): void { /* ... */ }
  protected deliverViaSmtp(message: string): void { /* SMTP magic */ }
}
 
// What does this add? Nothing. It is the festival sub-committee.
class EmailNotifier extends Notifier {}

এই module খোলা প্রতিটা নতুন developer একই দুটো প্রশ্ন করে: "আর কোন notifier আছে?" (নেই) আর "Notifier নাকি EmailNotifier construct করবো?" (কোনো ব্যাপার না — এটাই সমস্যা)। দুটো নাম, একটা মানে।

Collapse দশ মিনিটের কাজ। Survivor choice এখানে মজার: Notifier সাধারণ নাম, কিন্তু class-টা এখন পুরোপুরি SMTP-specific। সৎ merged নাম EmailNotifier — নাম সত্য বলা উচিত। তাই এবার child টিকে থাকে, parent-এর member নিচে নামানো হয়, আর Notifier মুছে দেওয়া হয়:

// AFTER: one class, and its name tells the truth
class EmailNotifier {
  constructor(private recipient: string) {}
 
  send(message: string): void {
    this.logAttempt(message);
    this.deliverViaSmtp(message);
  }
 
  private logAttempt(message: string): void { /* ... */ }
  private deliverViaSmtp(message: string): void { /* SMTP magic */ }
}

একটা ছোট bonus লক্ষ্য করো: subclass না থাকায় protected member হয়ে গেছে private। Hierarchy collapse করা প্রায়ই visibility tight করতে দেয়, class-এর surface area আরও কমে যায়। SMS কি কখনো ফিরে আসবে? Extract Subclass — বা আরও ভালো, একটা interface আর দুটো implementation — অপেক্ষায় থাকবে।

চিত্র ৭: সিদ্ধান্তের পথ — তখনই collapse করো যখন child কিছু যোগ করছে না আর কোনো sibling parent-কে দরকার করে না

C#-এ একই refactoring

Mechanics C#-এও হুবহু একই। এখানে একটা reporting module mid-collapse-এ:

// BEFORE: PdfReport once differed; today it is an empty costume
public class Report
{
    protected readonly string Title;
    public Report(string title) => Title = title;
 
    public virtual byte[] Render()
    {
        // years ago this was abstract; now it renders PDF directly
        return PdfEngine.Render(Title);
    }
}
 
public class PdfReport : Report          // no members at all
{
    public PdfReport(string title) : base(title) { }
}
// AFTER: one class; constructor chaining ceremony gone with it
public sealed class Report
{
    private readonly string _title;
    public Report(string title) => _title = title;
 
    public byte[] Render() => PdfEngine.Render(_title);
}

C#-specific তিনটা বিষয় লক্ষ্য রাখার মতো:

  • এখন class-টা seal করা যাবে। কোনো child না থাকায় sealed design decision document করে আর runtime-কে call devirtualize করতে দেয়। Render-এ virtual ছিল pure cost — সরিয়ে দাও।
  • Constructor chaining মিলিয়ে যায়। Child-এর প্রতিটা : base(title) ছিল ceremony যেটা merge মুছে দেয়। অনেক constructor parameter-এর hierarchy-তে এটা একাই refactoring-এর মূল্য ন্যায্য করে।
  • Public API আর serialization সামলাও। PdfReport NuGet-published type ছিল হলে মুছার আগে একটা release cycle [Obsolete("Use Report")] রাখো। আর serialized payload বা EF Core discriminator column যদি type name PdfReport store করে, পুরানো data যেন load হয় তার জন্য mapping যোগ করো।

আর একটু Python flavor, কারণ audit step language-independent। Python-এ "empty" subclass আক্ষরিক অর্থেই এক লাইন — miss করা আরও সহজ:

class Report:
    def __init__(self, title: str) -> None:
        self.title = title
 
    def render(self) -> bytes:
        return pdf_engine.render(self.title)
 
 
class PdfReport(Report):    # the whole class. Nothing. Collapse it.
    pass

এক লাইনের pass subclass বারবার code review পেরিয়ে যায় কারণ এটা নিরীহ দেখায়। এটা সবুজ খাতা: নিরীহ, আর pure রক্ষণাবেক্ষণ।

Hierarchy কি তার কাজ করছে?

দুটো ছবি এই প্রশ্নের উত্তর যেকোনো code review argument-এর চেয়ে ভালো দেয়।

প্রথমে history plot করো। Subclass-এর git log টেনে প্রতি বছর সে কতটা সত্যিকারের নতুন member contribute করেছে গণো। মরা hierarchy এরকম curve দেখায়:

চিত্র ৮: প্রতি বছর subclass-এর নতুন member — variant চুপচাপ তৃতীয় বছরের দিকে মারা গেছে

দ্বিতীয়ত, class-টাকে keep-or-collapse map-এ রাখো। দুটো axis: child সত্যিই কতটা আলাদা, আর কতজন অন্য child parent-এর উপর নির্ভর করে:

চিত্র ৯: Keep-or-collapse map — নিচে-বাঁয়ে যেখানে ফাঁকা খোলগুলো থাকে

উৎসব সাব-কমিটি শক্তভাবে collapse corner-এ। আর Circle, Square, Triangle প্রত্যেকে area() override করে এমন Shape base class সুস্থ corner-এ — সেটা কখনো collapse করো না।

একটু গভীরে: এখানে Liskov Substitution Principle (LSP)-এর সাথে একটা সংযোগ আছে। LSP বলে subclass object অবশ্যই তার parent type যেখানে expected সেখানে কোনো চমক ছাড়াই ব্যবহারযোগ্য হতে হবে। ফাঁকা subclass LSP trivially pass করে — এটা behaviourally তার parent-ই। এটাই এর বিপক্ষে প্রমাণ। LSP একটা constraint হওয়ার কথা যেটা real variant-কে satisfy করতে কাজ করতে হয়। যখন LSP pass করা child-কে কিছুই খরচ করায় না কারণ child কিছুই করে না, type system একটা পার্থক্য বহন করছে যেটা behaviour বহন করছে না। Collapse type system-কে আবার সত্য বলতে দেয়।

IDE সহায়তা

কোনো IDE-তে "Collapse Hierarchy" নামে একটা button নেই, কারণ এই move ছোট refactoring-গুলোর সমন্বয় — কিন্তু উপাদানগুলো সব automated:

  • IntelliJ IDEA / Rider: Refactor → Pull Members Up আর Push Members Down checkbox precision সহ স্তরের মধ্যে field আর method সরায়। Safe Delete (Alt+Delete) usage থাকলে class মুছতে অস্বীকার করে আর প্রতিটা blocker list করে। IntelliJ এছাড়াও Inline Super Class দেয়, যেটা একটা guided dialog-এ parent-কে child-এ মার্জ করে — "child survives" variant-এর জন্য।
  • ReSharper (Visual Studio): C#-এর জন্য Pull Members Up / Push Members Down dialog, আর Safe Delete যেটা deletion-এর আগে প্রতিটা reference খুঁজে preview করে।
  • VS Code (TypeScript): dedicated hierarchy refactoring নেই, কিন্তু Find All References (Shift+F12) আর Rename Symbol (F2) ধাপ ৩ নির্ভরযোগ্যভাবে করে। একটা practical trick: F2 দিয়ে doomed class-টার নাম survivor-এর নামে rename করো — language service প্রতিটা reference পুনর্লিখন করবে — তারপর এখন-duplicate declaration মুছো।

যে tool-ই হোক, safety rule একই: compiler আর test suite হলো আসল referee। প্রতিটা member সরানোর পরে দুটোই চালাও।

সুবিধা আর ঝুঁকি

সুবিধাঝুঁকি / খরচ
একটা concept একটা class-এ — পড়তে অর্ধেক file, শূন্য "কোন স্তরে?" সিদ্ধান্তLive override আছে এমন child collapse করা চুপচাপ behaviour বদলে দেয় — আগে member-by-member diff করো
Indirection-এর একটা স্তর সরে; navigation আর debugging ছোট হয়ভুল survivor choice sibling subclass-এর জন্য Liskov principle ভাঙতে পারে
ভবিষ্যত edit একটা class-এ — কম shotgun surgeryPublic type মুছলে external consumer ক্ষতিগ্রস্ত হয় — মুছার আগে deprecate করো
Visibility tight করা যায় (protected থেকে private), class sealed করা যায়Child যদি ফাঁকা হয় কারণ behaviour ভুলভাবে উপরে তোলা হয়েছে, সঠিক fix হলো push-down, merge নয়
Lazy Class আর Speculative Generality-র মূল সারিয়ে দেয়পরে real variant ফিরলে subclass re-extract করতে হবে
Fragile-base-class breakage-এর জন্য একটা কম coupling channelSerialized type name আর DB discriminator মুছে দেওয়া class reference করতে পারে — পুরানো data map করো

কোন smell-গুলো এটা সারায়?

SmellCollapse Hierarchy কীভাবে সাহায্য করে
Lazy Classফাঁকা subclass যেটা কিছু উপার্জন করে না সেটা সম্পূর্ণ মার্জ হয়ে যায়
Speculative Generality"কোনোদিন variant দরকার হতে পারে" hierarchy আজকের বাস্তব আকারে ফিরে আসে
Duplicate CodeParent-child near-duplication একটা সংজ্ঞায় সংকুচিত হয়
Shotgun Surgeryএকটা concept-এর পরিবর্তন দুটো hierarchy স্তরে edit করা থেকে রক্ষা পায়
Middle-layer indirectionপাঠকরা ফাঁকা class পেরিয়ে না গিয়ে actual behaviour-এ পৌঁছায়

দ্রুত মনে করার কার্ড

+------------------------------------------------------------------+
|        COLLAPSE HIERARCHY - REVISION CARD                        |
+------------------------------------------------------------------+
| Problem  : superclass and subclass have CONVERGED -              |
|            the child adds no fields, methods, or overrides.      |
|            Two names, one concept. (The festival sub-committee   |
|            with the same members as the main committee.)         |
|                                                                  |
| Solution : 1. pick the SURVIVOR (best-truth name; check          |
|               sibling subclasses first!)                         |
|            2. pull up / push down remaining members              |
|            3. redirect EVERY reference to the survivor           |
|            4. delete the empty class                             |
|            5. full test suite; deprecate if API was public       |
|                                                                  |
| Inverse  : Extract Subclass (rebuild a child if a real           |
|            variant returns)                                      |
| Cousin   : Inline Class (same merge, for collaborators)          |
| NOT for  : children with live overrides (keep them) or           |
|            Refused Bequest (use delegation instead)              |
+------------------------------------------------------------------+

অনুশীলন

তোমার পালা। একটা ছোট banking codebase-এ এই pair আছে, যেটা বাকি আছে সেই যুগ থেকে যখন ব্যাংকের দুটো account product ছিল:

class Account {
  constructor(
    protected holder: string,
    protected balance: number,
    protected interestRate: number,
  ) {}
 
  deposit(amount: number): void { this.balance += amount; }
  withdraw(amount: number): void {
    if (amount > this.balance) throw new Error("Insufficient funds");
    this.balance -= amount;
  }
  yearlyInterest(): number { return this.balance * this.interestRate; }
}
 
class SavingsAccount extends Account {
  // The current-account product was discontinued in 2021.
  // Interest logic was pulled up to Account around the same time.
  yearlyInterest(): number {
    return super.yearlyInterest();   // forwards and adds nothing
  }
}

এটা নিয়ে কাজ করো:

  1. Child-এর প্রতিটা member audit করো। yearlyInterest override super call করে আর ফলাফল হুবহু ফেরত দেয় — এটা মরা ceremony নাকি behaviour বদলায়? আগে সেটা মুছো আর prove করো test সবুজ থাকে।
  2. Survivor ঠিক করো। কাল্পনিক codebase মনে মনে খোঁজো: কেরানি, statement, আর test সবাই "savings account" বলছে, আর Account-এর অন্য কোনো subclass নেই। কোন নামটা এখন সত্য বলে? এক বাক্যে তোমার পছন্দ justify করো।
  3. নিরাপদ ক্রমে collapse করো: member সরাও, প্রতিটা new, প্রতিটা type annotation, প্রতিটা instanceof পুনর্নির্দেশ করো, তারপর ফাঁকা class মুছো।
  4. Collapse-এর unlock করা জিনিস tight করো: কোনো protected member কি private হতে পারে? C#-এ class কি sealed হতে পারতো?
  5. এই collapse-এর জন্য চিত্র ৬-এর নিজের version আঁকো — ধাপ ৩-এর পরে কোন state-এ থাকবে, আর কোন event তোমাকে সামনে নিয়ে যাবে?
  6. Bonus চিন্তা: ধরো আগামী বছর ব্যাংক একটা "Fixed Deposit" product launch করে যেখানে lock-in period আছে আর আগাম তোলায় penalty আছে। তুমি কি subclass re-extract করবে, নাকি WithdrawalPolicy object দিয়ে composition-এর দিকে যাবে? কোনটা এবং কেন তা এক বাক্যে লেখো।

ধাপ ৬-এর উত্তরে যদি বলো lock-in rule হলো এমন behaviour যেটা ভিন্ন হয় আর swap বা combine করা যেতে পারে — তাই composition-ই প্রথম দেখার বিষয় — তুমি ইতোমধ্যে এই series-এর পরের দুটো পাঠের মতো ভাবছো। নাসরিন আপা অনুমোদন করতেন: একটা খাতা, একটা সত্য, কোনো বাড়তি কাগজ নেই।

সচরাচর জিজ্ঞাসা

Collapse করার পর কোন class টিকবে — superclass নাকি subclass?
সাধারণত superclass টিকে, কারণ বেশিরভাগ কোড general নামটাই ব্যবহার করে। কিন্তু আসল নিয়মটা আরও সহজ — যে নামটা মার্জ করা concept-কে সবচেয়ে সৎভাবে বোঝায় সেটা রাখো। দলের কথাবার্তায় যদি subclass-এর নামটাই বেশি উঠে আসে, সেটা রাখো আর parent-এর member-গুলো নিচে নামিয়ে আনো।
Collapse Hierarchy আর Inline Class-এর মধ্যে পার্থক্য কী?
দুটোই দুটো class-কে একটায় মার্জ করে। Collapse Hierarchy inheritance-এ যুক্ত parent আর child-এর উপর কাজ করে — hierarchy-র একটা স্তর সরিয়ে দেয়। Inline Class দুটো আলাদা collaborating class-এর উপর কাজ করে যেখানে একটা এত ছোট হয়ে গেছে যে আর আলাদা থাকার মানে নেই। মনোভাব একই, কিন্তু class-গুলোর মধ্যে সম্পর্ক আলাদা।
Subclass যদি এখনও এক-দুটো method override করে?
তাহলে সে এখনও কাজ করছে — একটু থামো। ভাবো সেই override-গুলো কি সত্যিকারের কোনো variant বোঝায় যার উপর caller-রা polymorphically নির্ভর করে? যদি হ্যাঁ, hierarchy তার কাজ করছে — collapse করো না। Override-গুলো যদি তুচ্ছ বা মরা হয়, আগে সেগুলো সরাও, test চালাও, তারপর নিশ্চিন্তে collapse করো।
Hierarchy collapse করলে কি Liskov Substitution Principle ভেঙে যেতে পারে?
হ্যাঁ, ভুলভাবে করলে পারে। General superclass-কে একটা specific subclass-এ মার্জ করলে অন্য সব subclass হঠাৎ সেই specific class থেকে inherit করবে — যেমন Transport-কে Car-এ মার্জ করলে Plane হয়ে যাবে Car-এর child। কোন class টিকবে ঠিক করার আগে সবসময় superclass-এর অন্য সব child দেখে নাও।
Subclass আমাদের public API-এর অংশ। তারপরও কি collapse করা যাবে?
যাবে, কিন্তু সাবধানে। Public type মুছে দেওয়া যারা এটা import করে তাদের সবার জন্য breaking change। সঠিক পথ হলো সব behaviour survivor-এ নিয়ে যাও, পুরানো type-টা একটা release cycle-এর জন্য deprecated alias হিসেবে রাখো, সবাইকে জানিয়ে দাও, তারপরই মুছো।

আরো দেখো

সম্পর্কিত পাঠ

Lazy Class: যে চাকরির কাজ শুধু একটা বাটন চাপা

Lazy Class code smell শিখো একটা মজার গল্পের মাধ্যমে। কোন class-গুলো টিকে থাকার যোগ্যতা রাখে না সেটা বুঝতে পারবে, আর Inline Class দিয়ে সেগুলো ঠিক করতে পারবে।

আরও পড়ুন

Speculative Generality: যে সুইমিং পুলের জন্য পাইপ বসালে, পুলটাই হলো না

বাড়ি বানানোর গল্প দিয়ে Speculative Generality smell বোঝো। YAGNI কী, ভবিষ্যতের অনুমানে কোড লেখা কেন ক্ষতিকর, আর অব্যবহৃত abstraction কীভাবে সরাতে হয় — সব পরিষ্কার হয়ে যাবে।

আরও পড়ুন

Inline Class: যে Class কিছুই করে না, তাকে মিলিয়ে দাও

Inline Class refactoring শেখো একটা school committee-র গল্পের মাধ্যমে। যে class কিছুই করে না তাকে তার user-এর সাথে মিলিয়ে দাও আর অকারণ layer মুছে ফেলো।

আরও পড়ুন

Pull Up Method: পুরো স্কুলের জন্য একটাই নির্দেশিকা

Pull Up Method refactoring শেখো স্কুলের ছুটির আবেদনের গল্পের মাধ্যমে — subclass-এ duplicate হয়ে যাওয়া method-গুলো superclass-এ তুলে আনো, TypeScript আর C#-এ safe steps সহ, IDE dialog আর কখন Form Template Method বেছে নেবে সেটাসহ।

আরও পড়ুন