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

Extract Superclass: দুটো যমজ class-এর জন্য একটাই সাধারণ নিয়মের বই

Extract Superclass refactoring শেখো সায়েন্স ল্যাব আর কম্পিউটার ল্যাবের গল্পের মাধ্যমে — TypeScript ও C#-এ pull-up move, superclass বনাম interface তুলনা টেবিল, এবং কীভাবে এটা Duplicate Code দূর করে।

22 মিনিট আপডেট: June 11, 2026beginner
refactoringextract superclassinheritanceduplicate codepull up methodtypescriptcsharp

দুটো ল্যাব, দুটো খাতা, একটাই মাথাব্যথা

ধরো তোমার স্কুলে দুটো বিশেষ ঘর আছে। একটা সায়েন্স ল্যাব — রহেলা ম্যাডাম চালান। আরেকটা কম্পিউটার ল্যাব — জামাল স্যার চালান। দুজন দুই করিডোরে থাকেন। একদিকে টেস্ট টিউব আর বার্নার, অন্যদিকে keyboard আর একটা গুনগুন করা server। স্টাফ রুমে চায়ের টেবিল ছাড়া তারা তেমন একটা দেখাই করেন না।

কিন্তু একটু ভাবো — দুটো ল্যাব কীভাবে চলে। রহেলা ম্যাডাম তিনটা জিনিস রাখেন: রুম বুকিং ডায়েরি (কোন ক্লাস কোন পিরিয়ডে ল্যাব নেবে), চাবির খাতা (কে চাবি নিল, কখন ফেরত দিল), আর পরিষ্কারের রোটা (কোন দিন ল্যাব মোছা হবে)। এখন কম্পিউটার ল্যাবে যাও। জামাল স্যারও রাখেন... রুম বুকিং ডায়েরি। চাবির খাতা। পরিষ্কারের রোটা।

একদম একই তিনটা system। আলাদা হাতের লেখায়, আলাদা আলমারিতে। কেউ কারো নকল করেননি — দুজনেই শুধু একই কাজের মুখোমুখি হয়েছিলেন, আর দুজনেই একই সমাধান বের করেছিলেন।

গত মাসে এটা বড় সমস্যা হলো। প্রধান শিক্ষক সোমবারের সমাবেশে নতুন নিয়ম দিলেন: "চাবি এক ঘণ্টার মধ্যে ফেরত না দিলে লাল কালিতে নোট করো।" রহেলা ম্যাডাম সেদিনই আপডেট করলেন। জামাল স্যার সেই সোমবার ছুটিতে ছিলেন — কখনো শুনতেই পাননি। দুই সপ্তাহ পরে হঠাৎ inspection। কম্পিউটার ল্যাবের খাতা পুরনো নিয়মে। স্কুল written remark পেল। একই স্কুল, একই নিয়ম, দুটো কপি — আর কপিগুলো আলাদা হয়ে গেছে

সহকারী প্রধান শিক্ষিকা নাসরিন ম্যাডাম সহজ সমাধান করলেন। তিনি একটাই "ল্যাব রুলবুক" লিখলেন — বুকিং নিয়ম, চাবির নিয়ম, পরিষ্কারের নিয়ম। একবার প্রিন্ট, দুটো ল্যাবই মানে। প্রতিটা ল্যাব শুধু তার নিজের বিশেষ পাতা রাখে: রহেলা ম্যাডামের ল্যাবে রাসায়নিক নিরাপত্তা, জামাল স্যারের ল্যাবে software licence check। শেয়ার করা নিয়ম এখন একটাই জায়গায়। একবার পরিবর্তন করো — দুটো ল্যাবই পরিবর্তিত হয়, এমনকি কেউ ছুটিতে থাকলেও।

চিত্র ১: লাল কালির নিয়ম — শেয়ার করা রুলবুকের আগে ও পরে একটা নিয়ম কীভাবে পৌঁছাল

এটাই হলো Extract Superclass। দুটো (বা তার বেশি) class আলাদাভাবে বড় হয়ে একই field আর একই method শেয়ার করে ফেলে। আমরা তাদের উপরে একটা common parent class তৈরি করি আর শেয়ার করা implementation উপরে নিয়ে যাই

Extract Superclass আসলে কী?

Class গুলো প্রায়ই আমাদের দুটো ল্যাবের মতো হয়: আলাদা মানুষ, আলাদা সময়ে তৈরি, একে অপরের কথা জানা নেই — কিন্তু overlapping সমস্যা solve করছে। প্রতিটা class কাজ করে, কিন্তু একসাথে তারা Duplicate Code লুকিয়ে রাখে। ব্যাপারটা হলো, এই smell-টা সবচেয়ে ব্যয়বহুল — কারণ প্রতিটা শেয়ার করা নিয়ম প্রতিটা কপিতে ঠিক করতে হয়। একটা মিস করলেই bug।

Extract Superclass হলো bottom-up মেরামত। কীভাবে করবে?

  1. একটা নতুন (সাধারণত abstract) class তৈরি করো — সেটাই হবে সাধারণ রুলবুক।
  2. বিদ্যমান class গুলোকে সেটা extend করাও।
  3. শেয়ার করা field আর হুবহু একই method গুলো parent-এ pull up করো (Pull Up Field আর Pull Up Method move)।
  4. যেসব method দেখতে একরকম কিন্তু একটু আলাদা — skeleton উপরে তোলো, আলাদা step টা abstract রাখো child-দের পূরণ করার জন্য (Form Template Method)।
  5. যা সত্যিই অনন্য সেটা প্রতিটা child-এ থাকুক।

মূল কথাটা হলো implementation। নতুন superclass বাস্তব code ধরে রাখে — actual field, actual method body, এমনকি constructor logic। Child-রা কার্যকরী যন্ত্রপাতি inherit করে, শুধু প্রতিশ্রুতির তালিকা না। এটাই Extract Superclass-কে duplication-এর সঠিক প্রতিকার করে তোলে। আর এটাই এটাকে তার হালকা কাজিন Extract Interface থেকে আলাদা করে — সে কোনো code শেয়ার করে না।

Fowler বলেন এটা হলো abstraction নিচ থেকে আবিষ্কার করা। তুমি প্রথমদিনেই Lab design করো না। বছর পরে লক্ষ্য করো ScienceLab আর ComputerLab সবসময়ই একটা ধারণার দুটো সংস্করণ ছিল — আর তুমি type system-কে অবশেষে সেটা রেকর্ড করতে দাও।

রুলবুকের আগে প্রধান শিক্ষককে ব্যক্তিগতভাবে প্রতিটা ল্যাবে পৌঁছাতে হতো। একটা মিস মানেই drift। পরে, রুলবুক একক command-এর লাইন:

চিত্র ২: একটা নিয়ম, একটা জায়গা — extraction-এর পরে প্রধান শিক্ষক রুলবুক আপডেট করেন আর প্রতিটা ল্যাব সেটা inherit করে
💡

এক লাইনে: Extract Superclass sibling class গুলোর ডুপ্লিকেট field আর method body একটা নতুন parent-এ জড়ো করে, যাতে শেয়ার করা নিয়ম ঠিক একটাই জায়গায় থাকে — আর compiler জানে class গুলো একই ধরনের জিনিস।

কিছু উপরে তোলার আগে এই ছোট decision path-এ হেঁটে যাও। এটা দুটো classic ভুল থামায় — মিথ্যা পরিবার তৈরি করা, আর যেখানে বাস্তব code ডুপ্লিকেট সেখানে interface ব্যবহার করা:

চিত্র ৩: extraction সিদ্ধান্ত — সত্যিকারের পরিবারে শেয়ার করা code-এর জন্য superclass, শেয়ার করা role-এর জন্য interface, নয়তো helper class

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

এই লক্ষণগুলো খোঁজো:

  • সম্পর্কহীন class-এ যমজ code। দুটো class একই field (bookings, keyHolder, cleaningDay) আর প্রায় একই method declare করে। কোনো parent ছাড়া sibling-দের মধ্যে classic Duplicate Code
  • নিয়ম যেগুলো একসাথে পরিবর্তন হতে হবে। যখন একটা business rule ("দেরিতে চাবি লাল কালিতে") দুটো class-এ edit করতে বাধ্য করে — class গুলো আসলে একটাই concept।
  • সমান্তরাল বৃদ্ধি। ScienceLab-এ যোগ করা প্রতিটা feature শীঘ্রই ComputerLab-এও যোগ করতে হয়। তুমি একই class দুইবার maintain করছ।
  • দুটো class-ই মোটা হয়ে যাচ্ছে। প্রতিটা শেয়ার করা machinery-র নিজস্ব কপি বহন করছে আর তার বিশেষ logic — ধীরে ধীরে Large Class-এর দিকে যাচ্ছে।

নাসরিন ম্যাডাম রুলবুক প্রিন্ট করার আগে দুটো কাগজের খাতা মেপেছিলেন। Software-এও এই গণনাটা করার মতো — প্রতিটা class-এর কত ভাগ আসলে অন্যটার কপি?

চিত্র ৪: প্রতিটা ল্যাব class-এর কতটুকু আসলে শেয়ার করা machinery ছিল — ডুপ্লিকেট majority উপরে চলে যায়

শেয়ার করা অংশটা যখন majority, parent হলো nice-to-have না — এটাই আসল class, দুটো পোশাকে লুকিয়ে আছে।

আর সাবধানতাগুলো:

  • Inheritance slot বুদ্ধিমত্তার সাথে খরচ করো। C#, Java, আর TypeScript-এ একটা class শুধু একটাই base class extend করে। Class গুলো যদি শুধু একটা role শেয়ার করে আর কোনো বাস্তব code না — interface কিছুই খরচ করে না, নিচের comparison টেবিল দেখো।
  • মিথ্যা "is-a" জোর করো না। ScienceLab একটা Lab হয় — এটা সত্য। কিন্তু দুটো class কাকতালীয়ভাবে একটা utility method শেয়ার করলে তাদের একটা কল্পিত parent-এর নিচে আটকে দেওয়া ঠিক না। শেয়ার করা helper-এর জন্য composition (Extract Class) পছন্দ করো।
  • শুধু যা সত্যিই common তাই তোলো। এমন কোনো method উপরে তুললে যেটা শুধু একটাই child ব্যবহার করে — তুমি Refused Bequest smell তৈরি করলে। পরের সপ্তাহে Push Down Method apply করে undo করতে হবে।

দুটো বড় sharing tool-এর মধ্যে পছন্দটা এই quadrant-এ বসে। এটা তোমার desk-এর উপরে পিন করে রাখো:

চিত্র ৫: superclass নাকি interface? Same-family class-এ বাস্তব শেয়ার করা code স্পষ্টভাবে Extract Superclass-এর দিকে নির্দেশ করে

ল্যাবের case টা top-right-এ গভীরভাবে বসে আছে: বাস্তব ডুপ্লিকেট code, আর class গুলো সত্যিই একই ধরনের জিনিস। Bottom-left — শুধু একটা role শেয়ার করা সম্পর্কহীন class — সেটা Extract Interface এলাকা। Bottom-right কোণটা ফাঁদ: class গুলো পরিবার না, কিন্তু বাস্তব code শেয়ার করছে। তাদের জন্য মিথ্যা parent বানিও না — শেয়ার করা code একটা helper class-এ নিয়ে যাও যেটা দুটোই contain করতে পারে (composition)।

কলেজ কর্নার — slot economics: এত সাবধানতা কেন? কারণ C#, Java, আর TypeScript-এ extends clause একটা বিরল resource — প্রতি class-এ ঠিক একটাই, চিরকালের জন্য। Interface গুলো unlimited (class ComputerLab : Lab, IBookable, IAuditable C#-এ ঠিকঠাক), তাই প্রতিশ্রুতির খরচ নেই। কিন্তু implementation inherit করা একটা monopoly। Lab-এ slot খরচ করা মানে ComputerLab আর কখনো, ধরো, NetworkedRoom extend করতে পারবে না। ভাষাগুলো ইচ্ছাকৃতভাবে single inheritance বেছে নিয়েছে — multiple implementation inheritance বিখ্যাত diamond problem তৈরি করে, যেখানে একটা class একই field-এর দুটো competing কপি inherit করে। C++ এটা allow করে আর complexity-তে মূল্য দেয়। Java, C#, আর TypeScript এটা বাতিল করেছে আর slot scarcity-তে মূল্য দেয়। তোমার কাজ শুধু নিশ্চিত করা যে একটাই slot বাস্তব, দীর্ঘস্থায়ী শেয়ার করা machinery কেনে।

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

TypeScript-এ দুটো ল্যাব, duplication সহ:

// BEFORE: twin code, no shared parent
class ScienceLab {
  private bookings = new Map<string, string>(); // period -> class
  private keyHolder: string | null = null;
 
  bookSlot(period: string, className: string): void {
    if (this.bookings.has(period)) throw new Error("Slot already booked");
    this.bookings.set(period, className);
  }
 
  issueKey(teacher: string): void { this.keyHolder = teacher; }
  returnKey(): void { this.keyHolder = null; }
 
  runSafetyDrill(): string { return "Chemical spill drill done"; }   // unique
}
 
class ComputerLab {
  private bookings = new Map<string, string>(); // period -> class  (copy!)
  private keyHolder: string | null = null;      //                  (copy!)
 
  bookSlot(period: string, className: string): void {               // (copy!)
    if (this.bookings.has(period)) throw new Error("Slot already booked");
    this.bookings.set(period, className);
  }
 
  issueKey(teacher: string): void { this.keyHolder = teacher; }     // (copy!)
  returnKey(): void { this.keyHolder = null; }                      // (copy!)
 
  auditLicences(): string { return "All software licences valid"; } // unique
}

Extract Superclass-এর পরে — একটা রুলবুক, দুটো পাতলা ল্যাব:

// AFTER: shared implementation lives once, in Lab
abstract class Lab {
  private bookings = new Map<string, string>();
  private keyHolder: string | null = null;
 
  bookSlot(period: string, className: string): void {
    if (this.bookings.has(period)) throw new Error("Slot already booked");
    this.bookings.set(period, className);
  }
 
  issueKey(teacher: string): void { this.keyHolder = teacher; }
  returnKey(): void { this.keyHolder = null; }
}
 
class ScienceLab extends Lab {
  runSafetyDrill(): string { return "Chemical spill drill done"; }
}
 
class ComputerLab extends Lab {
  auditLicences(): string { return "All software licences valid"; }
}
চিত্র ৬: Extract Superclass — booking আর key management নতুন Lab parent-এ উপরে চলে যায়; প্রতিটা ল্যাব শুধু তার বিশেষ পাতা রাখে

প্রধান শিক্ষকের লাল কালির নিয়ম এখন Lab.returnKey()-এর ভেতরে এক লাইন, এক জায়গার পরিবর্তন। দুটো ল্যাবই তাৎক্ষণিকভাবে মানে, চিরকালের জন্য। প্রতিটা ভবিষ্যৎ শেয়ার করা নিয়মের maintenance cost দেখো:

চিত্র ৭: একটা শেয়ার করা নিয়ম পরিবর্তন হলে কতটা edit দরকার — একটা chart-এ refactoring-এর পুরো উদ্দেশ্য

দুইয়ের বিপরীতে একটা — ছোট সাশ্রয় মনে হচ্ছে। কিন্তু আসল কথাটা হলো failure mode: দুটো জায়গা নিয়ে, আসল খরচ দ্বিতীয় edit না — এটা ভুলে যাওয়া দ্বিতীয় edit, inspection-এর মন্তব্য, bug report। একটা জায়গা নিয়ে, ভুলে যাওয়া অসম্ভব।

ধাপে ধাপে, নিরাপদ উপায়ে

একবারে একটা member সরাও। Compiler আর test হলো তোমার safety rope। যাত্রায় পরিষ্কার, compile-green station আছে:

চিত্র ৮: ডুপ্লিকেট যমজ থেকে পরিষ্কার hierarchy পর্যন্ত — আগে data উপরে তোলো, তারপর body, তারপর template
  1. খালি parent তৈরি করো আর children connect করো। একটা খালি abstract class Lab {} আর দুটো class-এই extends Lab। আর কিছু পরিবর্তন না। সবকিছু এখনো compile হয়।
// INTERMEDIATE STEP: parent exists but is still empty
abstract class Lab {}
class ScienceLab extends Lab { /* all original code still here */ }
class ComputerLab extends Lab { /* all original code still here */ }
  1. আগে fields উপরে তোলো। Method গুলো তুলতে হলে আগে তাদের data দরকার। bookings আর keyHolder Lab-এ সরাও (private হিসেবে, অথবা protected যদি child-দের সরাসরি access দরকার হয়)। দুটো child থেকে কপিগুলো মুছে দাও। Compile করো।

  2. হুবহু একই method উপরে তোলো। bookSlot, issueKey, returnKey character-for-character একই — একটা method উপরে সরাও, দুটো child কপি মুছে দাও, test চালাও। প্রতিটা method-এ পুনরাবৃত্তি করো। যদি দুটো কপিতে ছোট আকস্মিক পার্থক্য থাকে (একটা log করে, একটা করে না), থামো। আগে সঠিক একক আচরণ ঠিক করো — উপরে তোলাও drift সারানোর সুযোগ।

  3. মিল কিন্তু আলাদা method গুলো template করো। ধরো দুটো ল্যাবেই একটা closeForDay() আছে যেটা বন্ধ করে, কিন্তু প্রতিটা আগে একটা বিশেষ check করে। Skeleton উপরে তোলো, আলাদা step abstract রাখো:

abstract class Lab {
  closeForDay(): string {
    const note = this.specialClosingCheck();  // child fills this step
    this.returnKey();
    return `Lab closed. ${note}`;
  }
  protected abstract specialClosingCheck(): string;
}
  1. Constructor-এর সাধারণ অংশ উপরে তোলো। দুটো child তাদের constructor-এ একই field set করলে সেই assignment parent constructor-এ নিয়ে যাও। প্রতিটা child থেকে super(...) call করো।

  2. অতিরিক্ত তোলার জন্য পর্যালোচনা করো। Parent-এর মধ্য দিয়ে হাঁটো। প্রতিটা member কি প্রতিটা child-এর জন্য অর্থবহ? কিছু যদি শুধু একটা ল্যাব ব্যবহার করে আর উপরে উঠে এসেছে — এখনই ফেরত নামিয়ে দাও, কেউ এটার উপর নির্ভর করার আগে।

⚠️

Fields উপরে তোলো সেগুলো ব্যবহার করা method-এর আগে, আর প্রতিটা একক move-এর পরে compile করো। আগে method তুললে সে child field reference করবে যেটা parent দেখতে পাচ্ছে না — একটা ছোট fixable step-এর বদলে error-এর দেওয়ালের মুখোমুখি হবে। ধীর হওয়া মানে মসৃণ। মসৃণ মানে দ্রুত।

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

ধরো একটা reporting module-এ দুটো exporter আলাদাভাবে বড় হয়েছে — PDF আর CSV। দুটোই timestamp সহ file name তৈরি করে, output folder নিশ্চিত করে, আর export log করে। শুধু body-writing আলাদা:

// AFTER extraction: shared machinery up, format-specific work down
abstract class ReportExporter {
  constructor(protected outputFolder: string) {}
 
  export(reportName: string, rows: string[][]): string {
    const fileName = this.buildFileName(reportName);
    this.ensureFolderExists();
    const content = this.renderBody(rows);        // the abstract step
    this.writeFile(fileName, content);
    console.log(`Exported ${fileName} (${rows.length} rows)`);
    return fileName;
  }
 
  private buildFileName(reportName: string): string {
    const stamp = new Date().toISOString().slice(0, 10);
    return `${this.outputFolder}/${reportName}-${stamp}.${this.fileExtension()}`;
  }
 
  private ensureFolderExists(): void { /* mkdir -p logic */ }
  private writeFile(name: string, content: string): void { /* fs write */ }
 
  protected abstract renderBody(rows: string[][]): string;
  protected abstract fileExtension(): string;
}
 
class CsvExporter extends ReportExporter {
  protected renderBody(rows: string[][]): string {
    return rows.map(r => r.join(",")).join("\n");
  }
  protected fileExtension(): string { return "csv"; }
}
 
class PdfExporter extends ReportExporter {
  protected renderBody(rows: string[][]): string {
    return `%PDF... ${rows.length} rows rendered ...`;
  }
  protected fileExtension(): string { return "pdf"; }
}

Refactoring-এর আগে, file-naming, folder-checking, আর logging code দুইবার ছিল আর ইতিমধ্যে drift হয়েছিল। একটা exporter toISOString ব্যবহার করত, অন্যটা hand-rolled date string — তাই একই report-এর PDF আর CSV ফাইলগুলো আলাদাভাবে sort হত! Refactoring-এর পরে, পুরো export pipeline একবার লেখা। প্রতিটা exporter শুধু তার দুটো সৎ পার্থক্য দেয়। আগামীকাল ExcelExporter যোগ করা মানে ঠিক দুটো ছোট method লেখা।

চিত্র ৯: template method export() parent-এ একবার থাকে; প্রতিটা child শুধু renderBody() আর fileExtension() দেয়

কলেজ কর্নার — Template Method আর fragile base: উপরের export() skeleton হলো Gang of Four catalog-এর Template Method pattern। আর Extract Superclass হলো এটা wild-এ সাধারণত কীভাবে দেখা যায় — designed নয়, discovered। একটা সতর্কতা যা textbook কম বলে: একটা superclass child-দের implementation-এর সাথে যুক্ত করে, শুধু interface নয়। যদি Lab.bookSlot() পরে internally অন্য overridable method call করা শুরু করে, একটা child-এর নিরীহ override bookSlot-এর আচরণ এমনভাবে পরিবর্তন করতে পারে যা child কখনো চায়নি — এটাই fragile base class problem। প্রতিরক্ষা: parent internal গুলো private রাখো, overridable surface ছোট আর স্পষ্টভাবে নামকরণ করো (যেমন specialClosingCheck), আর C#-এ method গুলো default-এ non-virtual চিহ্নিত করো (যা ভাষাটা ইতিমধ্যে করে — Java-র বিপরীতে, যেখানে প্রতিটা method final না হলে virtual)।

C#-এ একই refactoring

C#-এ ল্যাবের উদাহরণ। ভাষার details দেখো — abstract, protected, আর base(...) constructor chaining:

public abstract class Lab
{
    private readonly Dictionary<string, string> _bookings = new();
    private string? _keyHolder;
    protected string RoomNumber { get; }
 
    protected Lab(string roomNumber) => RoomNumber = roomNumber;
 
    public void BookSlot(string period, string className)
    {
        if (_bookings.ContainsKey(period))
            throw new InvalidOperationException("Slot already booked");
        _bookings[period] = className;
    }
 
    public void IssueKey(string teacher) => _keyHolder = teacher;
    public void ReturnKey() => _keyHolder = null;
 
    public abstract string DailyChecklist();      // shape shared, detail differs
}
 
public class ScienceLab : Lab
{
    public ScienceLab(string roomNumber) : base(roomNumber) { }
    public override string DailyChecklist() => "Check gas taps and acid cupboard";
}
 
public class ComputerLab : Lab
{
    public ComputerLab(string roomNumber) : base(roomNumber) { }
    public override string DailyChecklist() => "Check UPS and update antivirus";
}

মনে রাখো C#-এর নিয়মটা যেটা এই পুরো refactoring-কে আকৃতি দেয়: একটা class একটাই base class রাখতে পারে কিন্তু অনেক interface (class ComputerLab : Lab, IBookable, IAuditable)। Lab extract করা একমাত্র base-class slot খরচ করে — এখানে এটার মূল্য আছে, কারণ বাস্তব code উপরে উঠে গেছে। Java একইভাবে আচরণ করে (extends একটা, implements অনেক)। TypeScript-ও।

Python-এ একই ধারণা

Python multiple inheritance allow করে। কিন্তু এই refactoring-এর শৃঙ্খলাবদ্ধ সংস্করণ একইভাবে দেখায় — একটা abstract base শেয়ার করা machinery বহন করে, abc module দিয়ে চিহ্নিত যাতে কেউ সরাসরি Lab বানাতে না পারে:

from abc import ABC, abstractmethod
 
 
class Lab(ABC):
    def __init__(self, room_number: str) -> None:
        self.room_number = room_number
        self._bookings: dict[str, str] = {}
        self._key_holder: str | None = None
 
    def book_slot(self, period: str, class_name: str) -> None:
        if period in self._bookings:
            raise ValueError("Slot already booked")
        self._bookings[period] = class_name
 
    def issue_key(self, teacher: str) -> None:
        self._key_holder = teacher
 
    def return_key(self) -> None:
        self._key_holder = None
 
    @abstractmethod
    def daily_checklist(self) -> str: ...
 
 
class ScienceLab(Lab):
    def daily_checklist(self) -> str:
        return "Check gas taps and acid cupboard"
 
 
class ComputerLab(Lab):
    def daily_checklist(self) -> str:
        return "Check UPS and update antivirus"

@abstractmethod decorator Python-কে C# আর Java যা natively enforce করে তার একটা স্বাদ দেয়: Lab() সরাসরি instantiate করলে TypeError ওঠে। আর যে child daily_checklist ভুলে যায় সেটাও construct করা যায় না।

কলেজ কর্নার — abstract class বনাম interface, সুনির্দিষ্টভাবে: একটা abstract class (C# abstract class, Java abstract class, TS abstract class) field, constructor, পূর্ণ method body, আর abstract member ধারণ করতে পারে — আর single inheritance slot ব্যবহার করে। একটা interface (C# interface, Java interface, TS interface) ঐতিহ্যগতভাবে শুধু signature ধারণ করে — শূন্য state, শূন্য slot cost, প্রতি class-এ unlimited। Interview-এর জন্য জানার মতো আধুনিক জটিলতা: Java 8+ interface-এ default method body allow করে আর C# 8+ default interface member allow করে, কিন্তু কোনোটাই instance field allow করে না — তাই শেয়ার করা state এখনো abstract class জোর করে। দ্রুত test: "আমার কি সেখানে একটা field দরকার?" হ্যাঁ মানে abstract class। "শুধু behaviour প্রতিশ্রুতি?" মানে interface। "Body আছে কিন্তু field নেই, অনেক parent দরকার?" মানে default interface method, সংযতভাবে।

IDE support

এই refactoring-এর first-class automation আছে:

  • IntelliJ IDEA (Java/Kotlin): Refactor → Extract → Superclass... Extract Superclass dialog খোলে। নতুন parent-এর নাম আর package টাইপ করো, তারপর কোন member উপরে সরাবে সেটার জন্য checkbox tick করো। প্রতিটা method-এ extra checkbox আছে — body সরানোর বদলে abstract হিসেবে উপরে তোলা যায়। IDE extends clause পুনরায় সংযুক্ত করে আর যেখানে সম্ভব নতুন superclass ব্যবহার করার offer দেয়।
  • JetBrains Rider / ReSharper (C#): Refactor This → Extract Superclass একই member-selection dialog দেয়। Parent-এ member abstract করার আর child type থেকে parent type-এ usage আপডেট করার option আছে।
  • IntelliJ-family for TypeScript (WebStorm): Refactor → Extract Superclass TypeScript class-এও কাজ করে।
  • Visual Studio (C#): Extract base class... class name-এর Quick Actions (Ctrl+.) menu-তে দেখা যায় — member আর প্রতিটা abstract হবে কিনা বেছে নাও।

এই সব dialog-এর সাথে practical tip: fields আর হুবহু একই method ticked রেখে extract করো। মিল কিন্তু আলাদা method গুলো পরে careful manual template-method pass-এর জন্য রেখে দাও। Tools code সরাতে দারুণ। কোন পার্থক্যগুলো আকস্মিক drift আর কোনগুলো বাস্তব — সেটা শুধু তুমিই সিদ্ধান্ত নিতে পারবে।

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

আগে সেই headline সিদ্ধান্ত যেটা প্রতিটা developer-কে শিখতে হয় — আর exam ভালোবাসে জিজ্ঞেস করতে। Superclass code শেয়ার করে; interface একটা contract শেয়ার করে:

প্রশ্নExtract SuperclassExtract Interface
কী শেয়ার হয়?আসল code — field, method body, constructor (implementation)শুধু একটা প্রতিশ্রুতি — method signature, কোনো body নেই (contract)
Duplication দূর করে?হ্যাঁ — ডুপ্লিকেট body উপরে উঠে একবার থাকেনা — প্রতিটা class এখনো নিজস্ব body লেখে
প্রতি class-এ কতটা (C#/Java/TS)?একটাই base classঅনেক interface অবাধে
কোন সম্পর্ক assert করা হয়"is-a" — child-রা একই ধরনের জিনিস"can-do" — type গুলো শুধু একই role পালন করে
State (field) রাখতে পারে?হ্যাঁ — private machinery সহনা — default method সহও না
কখন সেরাSibling class গুলো কীভাবে কাজ করে তা ডুপ্লিকেট করেসম্পর্কহীন class শুধু কী offer করতে পারে তা শেয়ার করে; swapping বা test double দরকার
গল্পের সংস্করণএকটা Lab Rulebook যা দুটো ল্যাবই মানেএকটা sign-in form যা যেকোনো visitor পূরণ করতে পারে
খরচSingle inheritance slot খরচ করে; child-দের parent-এর code-এর সাথে যুক্ত করেকাঠামোগতভাবে প্রায় বিনামূল্যে; কিন্তু কোনো code শেয়ার করে না

এগুলো সুন্দরভাবে একত্রিত হয়: শেয়ার করা machinery-র জন্য superclass extract করো আর caller-রা যে contract-এর উপর নির্ভর করে তার জন্য interface — class ScienceLab extends Lab implements IBookableRoom। রুলবুক আর form হলো teammate, প্রতিদ্বন্দ্বী নয়।

আর এই refactoring-এর সাধারণ trade-off:

দিকসুবিধাঝুঁকি / খরচ
Duplicationশেয়ার করা নিয়ম একবার থাকে; drift অসম্ভব হয়আকস্মিক মিল ভুলভাবে একটা বাস্তব concept হিসেবে একীভূত হয়
Type system"এগুলো একই ধরনের" রেকর্ড করে; polymorphism enable করে (Lab[])একটা false is-a কঠিন undo করতে যখন caller এর উপর নির্ভর করে
ভবিষ্যৎ featureনতুন শেয়ার করা আচরণের একটা স্পষ্ট ঘর আছেParent swollen হয়ে god class হতে পারে যদি সবকিছু সেখানে যায়
নতুন variantনতুন child বিনামূল্যে সব machinery reuse করেChild-রা parent-এর implementation details-এর সাথে যুক্ত
Inheritance budgetএকটাই extends slot এখন খরচ হয়েছে

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

SmellExtract Superclass কীভাবে সাহায্য করে
Duplicate CodeSibling class জুড়ে ছড়িয়ে থাকা duplication-এর প্রাথমিক প্রতিকার — কপিগুলো একটা parent body-তে মিশে যায়
Large Classপ্রতিটা child শেয়ার করা machinery-র কপি ঝেড়ে ফেলে আর শুধু তার specialty রাখে
Alternative Classes with Different InterfacesMember উপরে তোলা sibling-দের এক সেট নাম আর signature-এ একমত হতে বাধ্য করে
Refused Bequestএড়ানোর ঝুঁকি, সারানোর প্রতিকার নয় — শুধু universal member উপরে তোলো, নয়তো এই smell তৈরি হবে

পুরো ধারণাটা একটা map-এ

চিত্র ১০: একটা map-এ তিনটা sharing style — প্রতিটা কী শেয়ার করে আর কী খরচ করে

দ্রুত revision box

+--------------------------------------------------------------+
|                    EXTRACT SUPERCLASS                        |
+--------------------------------------------------------------+
| Problem : Two sibling classes carry the SAME fields and      |
|           methods, written twice, drifting apart.            |
| Story   : Science lab & computer lab each kept their own     |
|           booking diary, key register, cleaning rota ->      |
|           one common "Lab Rulebook" both follow.             |
| Fix     : 1. Empty abstract parent; children extend it       |
|           2. Pull Up Field (data first!)                     |
|           3. Pull Up Method (identical bodies)               |
|           4. Form Template Method (similar bodies)           |
|           5. Push back anything only one child uses          |
| Shares  : IMPLEMENTATION (real code) - vs interface,         |
|           which shares only a CONTRACT (promise).            |
| Budget  : One base class per class (C#/Java/TS);             |
|           interfaces are unlimited.                          |
| Cures   : Duplicate Code, Large Class                        |
| Inverse : Collapse Hierarchy                                 |
+--------------------------------------------------------------+

অনুশীলন করো

একটা স্কুলের পরিবহন অফিসে এই দুটো class আছে:

class SchoolBus {
  constructor(public plateNumber: string, public driverName: string) {}
  private trips: string[] = [];
 
  logTrip(route: string): void { this.trips.push(`${new Date().toISOString()}: ${route}`); }
  tripCount(): number { return this.trips.length; }
  fitnessCertificateDue(): string { return "Check RTO fitness certificate yearly"; }
  studentCapacity(): number { return 40; }
}
 
class StaffVan {
  constructor(public plateNumber: string, public driverName: string) {}
  private trips: string[] = [];
 
  logTrip(route: string): void { this.trips.push(`${new Date().toISOString()}: ${route}`); }
  tripCount(): number { return this.trips.length; }
  fitnessCertificateDue(): string { return "Check RTO fitness certificate yearly"; }
  acRepairContact(): string { return "Cool Breeze Services, Dhaka"; }
}

তোমার কাজ:

  1. দুটো class জুড়ে প্রতিটা ডুপ্লিকেট member-এ আন্ডারলাইন করো (একটা field, একটা constructor pattern, আর তিনটা method খুঁজে পাবে)। চিত্র ৪-এর pie-এর তোমার নিজস্ব সংস্করণ আঁকো — প্রতিটা class-এর কত ভাগ একটা কপি?
  2. নিরাপদ উপায়ে একটা SchoolVehicle superclass extract করো: প্রথমে empty parent, তারপর field, তারপর হুবহু একই method — প্রতিটা move-এর পরে compile করো। চিত্র ৮-এর state diagram-এর বিপরীতে নিজেকে check করো।
  3. সিদ্ধান্ত নাও: SchoolVehicle কি abstract হওয়া উচিত? studentCapacity() কি উপরে সরানো উচিত? সাবধান — ভ্যানটা ছাত্র বহন করে না। এটা উপরে তোলা কোন smell তৈরি করবে?
  4. Bonus: স্কুলের booking software যেকোনো bookable resource — vehicle, ল্যাব, auditorium — book(slot) দিয়ে সমানভাবে treat করতে চায়। একটা van আর auditorium-এর মধ্যে বাস্তব code শেয়ার হয় না। কোন refactoring সেই common role নামকরণ করে, আর কেন এটা Extract Superclass নয়? উত্তর দেওয়ার আগে চিত্র ৫-এর quadrant-এ case টা রাখো।
  5. কলেজ-স্তরের bonus: ধরো আগামী বছর কেউ SchoolBus কে নতুন GpsTrackedVehicle base class থেকেও inherit করাতে চায় যেটা tracking state ধরে। Single-inheritance slot-এর কথা মাথায় রেখে ব্যাখ্যা করো কেন এটা এখন একটা design সমস্যা — আর দুটো বের হওয়ার উপায়ের নাম বলো (hint: একটা composition ব্যবহার করে, একটা interface আর শেয়ার করা helper ব্যবহার করে)।

যদি তোমার শেষ parent শুধু সেই member ধারণ করে যেগুলো দুটো vehicle সত্যিই ব্যবহার করে, আর প্রতিটা child ঠিক একটা specialty method রাখে — তোমার রুলবুক সঠিকভাবে প্রিন্ট হয়েছে। নাসরিন ম্যাডাম একটা লাল কালির মন্তব্য ছাড়াই তোমার inspection পাস করতেন।

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

Extract Superclass আর Extract Interface-এর মধ্যে পার্থক্য কী?
Extract Superclass আসল CODE শেয়ার করে — বাস্তব field আর method body যেগুলো child class inherit করে আর reuse করে। Extract Interface শুধু একটা প্রতিশ্রুতি শেয়ার করে — method signature আছে কিন্তু body নেই, প্রতিটা class নিজেই implement করে। দুটো class যদি একইভাবে কাজ করার code ডুপ্লিকেট করে, তাহলে superclass extract করো। যদি শুধু কী করতে পারে সেটা শেয়ার করে, তাহলে interface extract করো।
Single inheritance কে কেন 'বাজেট' বলা হয়েছে?
C#, Java, আর TypeScript-এ একটা class শুধু একটাই base class extend করতে পারে, কিন্তু অনেক interface implement করতে পারে। Superclass বেছে নিলে সেই একটাই slot চিরকালের জন্য খরচ হয়ে যায়। তাই superclass extract করার আগে জিজ্ঞেস করো — সম্পর্কটা সত্যিই কি 'is-a' আর শেয়ার করা code কি সেই slot-এর মূল্য দেয়?
Extract Superclass আর Extract Subclass-এর মধ্যে পার্থক্য কী?
দিক একদম উল্টো। Extract Superclass হলো bottom-up: দুটো বিদ্যমান sibling class তাদের সাধারণ অংশ একটা নতুন parent-এ উপরে দেয়। Extract Subclass হলো top-down: একটা বোঝাই class তার special-case অংশ একটা নতুন child-এ নিচে পাঠায়। উপরের দিকে সাধারণীকরণ, নিচের দিকে বিশেষায়ন।
যদি দুটো method দেখতে একরকম কিন্তু হুবহু এক না হয়?
Form Template Method ব্যবহার করো। Superclass-এ shared skeleton টা একটা concrete method হিসেবে রাখো — সে ছোট ছোট abstract step call করে। প্রতিটা subclass শুধু যে step-গুলো আলাদা সেগুলো override করে। হুবহু একই method উপরে উঠে যায়। মিলের method template হিসেবে উপরে যায়। সম্পূর্ণ আলাদা method নিচেই থাকে।
Extract করা superclass কি abstract হওয়া উচিত?
সাধারণত হ্যাঁ। নতুন parent প্রায়ই এমন একটা concept প্রতিনিধিত্ব করে — যেমন 'Lab' বা 'Exporter' — যেটা সরাসরি কেউ বানাবে না, শুধু concrete child-গুলোই বাস্তব object হিসেবে অর্থবহ। এটাকে abstract করলে সেটা স্পষ্ট হয়, আর abstract method declare করা যায় যেগুলো child-গুলো পূরণ করবে।

আরো দেখো

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

Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা

বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।

আরও পড়ুন

Large Class: যে স্কুলের ব্যাগে সব কিছু থাকে

Large Class code smell কী সেটা বুঝো — কেন god class বড় হয়, low cohesion কীভাবে চেনা যায়, আর Extract Class দিয়ে কীভাবে ছোট ছোট focused class-এ ভাগ করা যায়।

আরও পড়ুন

Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি

Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।

আরও পড়ুন

Extract Subclass: বিশেষ কেসটাকে তার নিজের class দাও

Extract Subclass শেখো একটা দর্জির দোকানের জরুরি অর্ডারের গল্পের মধ্যে দিয়ে — TypeScript আর C#-এ flag সরানোর পদ্ধতি, নিরাপদ ধাপে ধাপে কাজ, আর কখন subclassing ভুল সিদ্ধান্ত সেটাও বুঝবে।

আরও পড়ুন