Parallel Inheritance Hierarchies: প্রতিটা জিনিসের একটা ছায়া থাকে
Parallel Inheritance Hierarchies code smell শেখো একটা মিষ্টির দোকানের গল্পের মাধ্যমে — mirrored class tree কেন সমস্যা, TypeScript আর C# example দিয়ে কীভাবে fix করবে, আর কখন এটা রেখে দেওয়া ঠিক আছে।
মিষ্টির দোকান আর ছায়ার কাজ
ধরো করিম মিষ্টান্ন ভাণ্ডার — পুরান ঢাকার একটা বিখ্যাত দোকান। রসগোল্লা, কালোজাম, মিষ্টি দই। দোকানটা চলে, কিন্তু দেখো প্রতিবার নতুন মিষ্টি মেনুতে যোগ করলে কী হয়।
ঈদের আগে করিম সাহেব ঠিক করলেন নতুন মিষ্টি আনবেন — ছানার পায়েস। সহজ মনে হচ্ছে, তাই না? মিষ্টি বানাও, শোকেসে রাখো, শেষ? মোটেই না। তার ছেলে রুবেল দোকানের নিয়মের খাতা বের করল:
- নতুন মিষ্টি → নতুন বক্স ডিজাইন দরকার। রসগোল্লা গোল বক্সে যায়, কালোজাম চ্যাপ্টা বক্সে — তাহলে ছানার পায়েসের জন্য আলাদা বক্স দরকার। printer-এ order দাও।
- নতুন মিষ্টি → নতুন price tag দরকার। প্রতিটা মিষ্টির নিজস্ব tag style আছে। আরেকটা design করো।
- নতুন মিষ্টি → register-এ নতুন column দরকার। বিক্রির খাতায় প্রতিটা মিষ্টির একটা column। আবার দাগ কাটো, column বাড়াও।
মানে "একটা মিষ্টি যোগ করা" মানে কখনোই এক কাজ না। সবসময় চারটা কাজ: মিষ্টি, বক্স, tag, আর column। রুবেল বলল, "আব্বু, প্রতিটা মিষ্টির তিনটা ছায়া আছে! আমরা কখনো এক জিনিস যোগ করি না — জিনিস যোগ করি, আর সাথে বক্স কপাটে ছায়া, tag board-এ ছায়া, আর খাতায় ছায়া।"
সেই ঈদের সপ্তাহটা ভাবো। মিষ্টি বানানোটা ছিল মজার অংশ — বাকি সপ্তাহটা কাটল ছায়া খুঁজতে:
আর ফাঁদটা হলো — ছায়াগুলো মনে রাখার উপর নির্ভর করে, কোনো rule-এর উপর না। গত বছর দোকানে মুগ ডালের হালুয়া এসেছিল, কিন্তু register-এ column যোগ করতে ভুলে গেছে। দুই সপ্তাহ হালুয়ার বিক্রি খাতার margin-এ লেখা হলো, আর মাসের হিসাব মিলল না। কেউ কোনো rule ভাঙেনি — শুধু এমন কোনো rule-ই ছিল না যেটা ছায়াগুলোকে step-এ রাখে।
সেদিন সন্ধ্যায় রুবেল একটা প্রশ্ন করল যেটারই উত্তর এই পুরো article: "আব্বু, বক্স কেন আলাদা system হতে হবে? রসগোল্লা নিজেই জানে সে গোল। মিষ্টি নিজের বক্সের জ্ঞান নিজের সাথে বহন করতে পারে না?"
Software-এও এটাই হয়। কখনো কখনো একটা codebase-এ দুটো class tree একে অপরকে mirror করে। একটা tree-তে subclass যোগ করো, আর অন্য tree-তেও matching subclass যোগ করতেই হবে — না হলে সব চুপচাপ ভেঙে পড়ে। এই smell-এর নাম Parallel Inheritance Hierarchies — প্রতিটা নতুন class তার ছায়া তৈরি করতে বাধ্য করে।
এই smell টা আসলে কী?
Parallel Inheritance Hierarchies মানে হলো দুটো class hierarchy একসাথে lock হয়ে আছে: একটা class-এর subclass বানালে, অন্য class-এরও matching subclass বানাতেই হবে।
Martin Fowler তার Refactoring বইয়ে এটাকে classic smell হিসেবে রেখেছেন। আর একটা জরুরি কথা বলেছেন: এটা আসলে Shotgun Surgery-র একটা special case। Shotgun Surgery মনে আছে? একটা change, অনেক জায়গায় edit। এখানে "একটা change" মানে নতুন variant যোগ করা (নতুন মিষ্টি, নতুন shape, নতুন event type), আর "scattered edits" গুলো একটু specific structure-এ পড়ে — দুটো mirrored class tree-তে, যেগুলো sync-এ থাকতে হবে।
ধরো এভাবে। তোমার একটা Sweet hierarchy আছে: Ladoo, Barfi, Jalebi। আর অন্যদিকে একটা SweetBox hierarchy: LadooBox, BarfiBox, JalebiBox। prefix গুলো rhyme করে। tree গুলোর shape একই। একই গতিতে বাড়ে। একটার diagram অন্যটার diagram, শুধু suffix বদলানো। KajuKatli যোগ করলে KajuKatliBox মনে রাখতে হবে — আর ওই "মনে রাখতে হবে" অংশটাই সব সমস্যার কারণ।
এই অবস্থায় code কীভাবে আসে? সাধারণত ভালো উদ্দেশ্য থেকেই। কেউ একটা responsibility — packing, rendering, validating, exporting — main hierarchy থেকে বের করে আলাদা tree-তে নিয়ে গেছে। এই instinct মাঝে মাঝে ঠিকই আছে। সমস্যা শুরু হয় যখন নতুন tree-টা পুরনো tree-কে one-for-one mirror করতে বাধ্য হয়, subclass by subclass। তখন দুটো tree আর independent design না — একটাই design, দুই জায়গায় ভাগ হয়ে, naming convention আর মানুষের স্মৃতি দিয়ে আটকে আছে।
মনে রাখার সহজ উপায়: Parallel hierarchies = প্রতিটা মিষ্টির একটা ছায়া বক্স দরকার। একটা নতুন class যোগ করলে সবসময় অন্যদিকে তার pair যোগ করতে হলে, দুটো tree chain হয়ে গেছে। cure সাধারণত হলো pair-এর behavior main class-এ নিয়ে যাওয়া আর shadow tree মুছে ফেলা।
পরিবারের মধ্যে এই smell-এর জায়গা: এটা Change Preventers group-এ পড়ে, Divergent Change আর Shotgun Surgery-এর পাশে। তিনটাই change-কে expensive করে — আর এই smell-টা extension কে expensive করে: "আরেক ধরনের জিনিস যোগ করো" — এই সবচেয়ে সাধারণ change-এর জন্য সবসময় দ্বিগুণ কাজ।
পুরো smell, একটা mind map-এ:
University corner: এই smell একটা সুন্দর উদাহরণ coupling without dependency-র। KajuKatli আর KajuKatliBox হয়তো কোনো import share করে না — static analyzer দুটোকে unrelated class দেখাবে — কিন্তু তারা tightly coupled, কারণ একটা change হলে অন্যটাও change করতেই হবে। এটাকে বলে implicit বা semantic coupling, আর এটা সবচেয়ে বিপজ্জনক কারণ কোনো tool এটার arrow draw করে না। Design course-এর জন্য lesson: একটা system-এর true coupling graph হলো "একসাথে change করতে হয়" graph — আর এটা সবসময় import graph-এর চেয়ে বড়।
এটা চেনার উপায়
এই smell চেনা সহজ, কারণ class-এর নামেই ধরা পড়ে। checklist:
- দুটো hierarchy যেখানে subclass-এর নাম rhyme করে:
CircleShapeআরCircleRenderer,SquareShapeআরSquareRenderer। - একটা tree-তে subclass যোগ করলে twin ছাড়া compile error আসে, runtime-এ "no handler found" দেখায়, বা feature চুপচাপ কাজ করে না।
- দুটো tree সবসময় একসাথে বাড়ে — tree A-তে class যোগ করা প্রতিটা PR-এ tree B-তেও class আসে।
- দুটো tree কে bridge করা code-এ
switch/ifladder বা registry আছে: "এটা Ladoo হলে LadooBox ব্যবহার করো; Barfi হলে BarfiBox..." - Team-এর একটা মুখের নিয়ম আছে: "যখনই
XEventযোগ করবে,XEventHandlerও যোগ করো" — কোথাও লেখা নেই, কেউ enforce করে না।
Quick reference table:
| Sign | কী দেখতে পাচ্ছো | মানে কী |
|---|---|---|
| Rhyming names | দুই tree-এ matching prefix | দুই tree আসলে এক concept, দুই জায়গায় ভাগ |
| Lockstep growth | একই commit-এ দুই tree-এ subclass আসে | variant যোগ করলে সবসময় দুটো class লাগে |
| Bridge ladder | switch/if দিয়ে tree A থেকে tree B-তে map | type দিয়ে dispatch করে tree দুটো আটকে রাখা হচ্ছে |
| Twin-or-break | twin ভুললে compile বা runtime ভাঙে | sync শুধু memory দিয়ে enforce হচ্ছে |
| Tribal rule | "মনে রেখো, matching X-ও যোগ করতে হবে" | দুই tree-এর contract invisible |
| Same diagram | একটা tree-র diagram = অন্যটার diagram, নাম বদলে | pure structural duplication |
একটা practical trick: সবচেয়ে নতুন subclass-টা নাও, git history-তে খোঁজো কোন commit-এ সেটা এসেছে। সেই commit-এ (বা পরের "oops, add missing handler" commit-এ) অন্য hierarchy-তেও class এসেছে কিনা দেখো। পেলেই প্রমাণ হাতে।
আরেকটা উপায় হলো শেষবার কেউ যখন নতুন variant যোগ করেছিল, সেটার "bill" গুনে দেখো। কাজ আসলে কোথায় গেল? healthy design-এ pie-এর এক slice — শুধু variant নিজে। parallel-hierarchy দোকানে ছায়াগুলো বেশিরভাগ pie খেয়ে ফেলে:
কেন এটা সমস্যা
১. প্রতিটা extension দ্বিগুণ (বা তিনগুণ) খরচ। Object-oriented code-এ নতুন variant যোগ করা সবচেয়ে সস্তা, সবচেয়ে আনন্দের কাজ হওয়ার কথা — একটা নতুন class, plug করে দাও। parallel tree-তে দুটো বা তিনটা coordinated edit, আলাদা folder-এ, exactly match করতে হবে। সবচেয়ে ঘন ঘন change-এর একটা permanent surcharge আছে।
২. Synchronization-এর কোনো guard নেই। Language-এ কোথাও enforce হয় না "প্রতিটা Sweet-এর একটা SweetBox থাকতে হবে"। এই rule convention-এ বাঁচে। Convention ভুলে যাওয়া হয় — বিশেষত নতুন team member, বিশেষত শুক্রবার বিকেলে। এভাবেই হালুয়ার column বাদ পড়ে গিয়েছিল।
৩. Tree গুলো drift করে। বছরের পর বছরে, একটা tree-তে subclass আসে যেটার pair অন্য tree-তে নেই, বা bridging map-এ কোনো entry miss হয়। মিলছে না এমন অবস্থায় subtle bug তৈরি হয়: নতুন event type চুপচাপ handle হয় না, নতুন report empty file হিসেবে export হয়।
৪. পড়ার load দ্বিগুণ। নতুন developer-কে দুটো hierarchy বুঝতে হবে আর তাদের মধ্যে invisible contract-ও বুঝতে হবে — শুধু একটা concept বোঝার জন্য। mental model দরকারের চেয়ে দ্বিগুণ।
৫. Shotgun Surgery institutionalised। একটা conceptual change — "hexagon support করো" — সবসময়ের জন্য একটা coordinated multi-class operation হয়ে যায়। এই smell শুধু একটা change ধীর করে না — প্রতিটা ভবিষ্যতের variant-এ tax বসায়, চিরতরে, যতক্ষণ না কেউ mirror collapse করে।
University corner: এটাকে change amplification (Ousterhout)-এর সাথে connect করো: "আমরা এখন ছানার পায়েস বেচি" — এই idea এক বাক্যে বলা যায়, কিন্তু implementation-এ তিনটা coordinated edit। এই amplification factor architecture-এই বাকে আছে, প্রতিটা ভবিষ্যতের variant-এ charge হবে। আর Open-Closed Principle-এর সাথে connect করো: OCP বলে variant যোগ করা মানে শুধু নতুন code যোগ, কোনো existing code modify নয়। Parallel hierarchies এই promise দুইবার ভাঙে — bridge ladder modify করতে হয়, আর দ্বিতীয় tree extend করতে মনে রাখতে হয়।
Real-life code example
করিম মিষ্টান্ন ভাণ্ডারের software লিখি, smell সহ। Sweet hierarchy আছে, parallel PackagingBox hierarchy আছে, আর একটা bridge function আছে যেটা এক tree থেকে অন্য tree-তে map করে।
// Tree 1: the sweets
abstract class Sweet {
constructor(public name: string, public pricePerKg: number) {}
}
class Ladoo extends Sweet {}
class Barfi extends Sweet {}
class Jalebi extends Sweet {}
// Tree 2: the shadow tree — one box class per sweet class
abstract class PackagingBox {
abstract label(sweet: Sweet, kg: number): string;
}
class LadooBox extends PackagingBox {
label(s: Sweet, kg: number) { return `ROUND BOX | ${s.name} | ${kg}kg | keep away from sun`; }
}
class BarfiBox extends PackagingBox {
label(s: Sweet, kg: number) { return `FLAT SILVER BOX | ${s.name} | ${kg}kg | refrigerate`; }
}
class JalebiBox extends PackagingBox {
label(s: Sweet, kg: number) { return `LEAK-PROOF BOX | ${s.name} | ${kg}kg | best fresh`; }
}
// The bridge: the glue that proves the trees are chained
function boxFor(sweet: Sweet): PackagingBox {
if (sweet instanceof Ladoo) return new LadooBox();
if (sweet instanceof Barfi) return new BarfiBox();
if (sweet instanceof Jalebi) return new JalebiBox();
throw new Error(`No box registered for ${sweet.name}`); // the halwa trap!
}structure টা class diagram-এ দেখলে mirror এড়ানো অসম্ভব — পাতাটা মাঝখান থেকে ভাঁজ করলে দুই পাশ name-for-name মিলে যায়:
এখন ঈদ এলো, আর রুবেল ছানার পায়েস যোগ করল:
class KajuKatli extends Sweet {}
// Montu compiles, ships, and goes home happy...Code compile হলো perfectly। TypeScript কোনো আপত্তি করল না। কিন্তু প্রথম customer যখন kaju katli কিনতে এলো, boxFor() throw করল: "No box registered for Kaju Katli." দেখো failure কীভাবে ধীরে ধীরে আসে — আর কখন কে জানতে পারে সেটা লক্ষ্য করো:
একটা মিষ্টি যোগ করতে রুবেলের আসলে তিনটা edit করা দরকার ছিল: KajuKatli class, KajuKatliBox class, আর boxFor-এ নতুন branch। সে করেছে একটা। বাকি দুটো ছিল ছায়া — আর ছায়া মানুষ ভুলে যায়। Compiler — যে বন্ধুটা warning দিতে পারত — চুপ করে রইল, কারণ type system-এ কোথাও লেখা নেই "প্রতিটা Sweet-এর একটা Box লাগবে"।
প্রতিটা ভবিষ্যতের মিষ্টির cost গুনে নাও: দুটো class আর একটা bridge branch, চিরতরে। আর প্রতিটা box subclass-এ আসলে কী আছে দেখো — শুধু একটা box style আর storage note। ছোট্ট data, ভারী class-এর costume পরে।
Step by step ঠিক করা
Standard cure, Fowler থেকে সরাসরি: একটা hierarchy অন্যটাকে refer করুক, তারপর Move Method আর Move Field দিয়ে সব primary tree-তে নিয়ে যাও। mirror subclass গুলো empty shell হয়ে গেলে পুরো shadow hierarchy মুছে দাও।
Step 1 — কোন tree primary সেটা ঠিক করো। সাধারণত real concept যেটা represent করে সেটাই primary (Sweet); shadow সেটাকে serve করার জন্যই আছে (Box)। shadow হলো যেটার subclass একা মানে রাখে না — LadooBox ছাড়া ladoo থাকলে কার কোনো কাজে আসবে?
Step 2 — Behavior টা primary tree-তে নিয়ে যাও। প্রতিটা box subclass-এ একটা sweet সম্পর্কে packaging knowledge আছে। সেই knowledge sweet by sweet আলাদা — এটাই polymorphism-এর কাজ। Move Method দিয়ে সেটা নিজের জায়গায় নিয়ে এসো:
abstract class Sweet {
constructor(public name: string, public pricePerKg: number) {}
abstract boxLabel(kg: number): string; // packaging now lives WITH the sweet
}
class Ladoo extends Sweet {
boxLabel(kg: number) { return `ROUND BOX | ${this.name} | ${kg}kg | keep away from sun`; }
}
class Barfi extends Sweet {
boxLabel(kg: number) { return `FLAT SILVER BOX | ${this.name} | ${kg}kg | refrigerate`; }
}
class Jalebi extends Sweet {
boxLabel(kg: number) { return `LEAK-PROOF BOX | ${this.name} | ${kg}kg | best fresh`; }
}Step 3 — Behavior-এর দরকারি data-ও নিয়ে যাও। LadooBox-এ যদি fields থাকে (box dimension, material), Move Field দিয়ে সেগুলো Ladoo-তে নিয়ে এসো।
Step 4 — Shadow tree আর bridge মুছে দাও। PackagingBox hierarchy এখন empty shell; boxFor() আর তার instanceof ladder কারো কাজে লাগছে না। দুটোই মুছে দাও। কম code, কম file, zero invisible contract।
Step 5 — Extension-এর নতুন cost উপভোগ করো। এখন ছানার পায়েস যোগ করার cost দেখো:
class KajuKatli extends Sweet {
boxLabel(kg: number) { return `DIAMOND BOX | ${this.name} | ${kg}kg | refrigerate`; }
}
// Done. One class. No shadow. No bridge edit. Nothing to forget.আর সবচেয়ে ভালো অংশ: এখন packaging ভুলে যাওয়া impossible। boxLabel হলো Sweet-এ abstract, তাই TypeScript এটা ছাড়া কোনো sweet compile করতে দেবে না। invisible convention এখন compiler-enforced contract। রুবেলের "oops" bug আর exist করতে পারে না — পুরো bug species-টাই extinct হয়ে গেছে।
Design-এর পুরো জীবন, clean থেকে mirrored থেকে collapsed, একটা state machine হিসেবে:
আর plain numbers-এ payoff — দোকানের সবচেয়ে common change-এর cost, আগে আর পরে:
যখন variation শুধু data, তখন আরেকটা পথ। ওই box subclass গুলো আবার দেখো — প্রতিটায় ছিল মাত্র দুটো string (box style, storage note)। mirror subclass শুধু data ধরলে, প্রতিটা subclass-এ method দরকার নেই; একটা সাধারণ field-ই কাজ করে। Python-এ এই collapse টা দেখতে বেশ সুন্দর:
# The whole shadow hierarchy becomes... constructor data.
from dataclasses import dataclass
@dataclass
class Sweet:
name: str
price_per_kg: int
box_style: str # was a LadooBox / BarfiBox class
storage_note: str # was a method override
def box_label(self, kg: float) -> str:
return f"{self.box_style} | {self.name} | {kg}kg | {self.storage_note}"
ladoo = Sweet("Ladoo", 520, "ROUND BOX", "keep away from sun")
kaju_katli = Sweet("Kaju Katli", 980, "DIAMOND BOX", "refrigerate")
# Adding a sweet is now ONE line. No class, no twin, no bridge.পুরো hierarchy constructor argument-এ পরিণত হয়ে গেল। সবসময় check করো তোমার shadow tree আসলে behavior (polymorphism রাখো) নাকি শুধু data (field ব্যবহার করো)।
মাঝে মাঝে shadow tree ভালো কারণেই থাকে — যেমন, Renderer hierarchy আলাদা রাখা হয়েছে যাতে UI code pure domain model-এ না ঢোকে। merge করলে UI framework টা model-এ টেনে আনতে হবে: সেটা আরও বড় সমস্যা! সেক্ষেত্রে tree দুটো রাখো, কিন্তু mirror টা সচেতনভাবে manage করো — registry বা Visitor pattern দিয়ে, scattered instanceof ladder দিয়ে নয়। chosen আর guarded parallelism হলো একটা trade-off; accidental parallelism হলো smell।
C#-এ একই smell
Compact C# version, একটা reporting system থেকে। প্রতিটা report type-এর shadow exporter type আছে:
// Before: two trees in lockstep + a bridge switch
public abstract class Report { }
public class SalesReport : Report { }
public class StockReport : Report { }
public abstract class ReportExporter { public abstract string Export(Report r); }
public class SalesReportExporter : ReportExporter
{
public override string Export(Report r) => "sales as CSV...";
}
public class StockReportExporter : ReportExporter
{
public override string Export(Report r) => "stock as CSV...";
}
public static ReportExporter ExporterFor(Report r) => r switch
{
SalesReport => new SalesReportExporter(),
StockReport => new StockReportExporter(),
_ => throw new InvalidOperationException("No exporter!") // the trap
};TaxReport যোগ করতে তিনটা synchronized edit লাগে। mirror collapse করতে export behavior-টা report-এই নিয়ে যাও:
// After: one tree; the shadow hierarchy and the switch are deleted
public abstract class Report { public abstract string Export(); }
public class SalesReport : Report { public override string Export() => "sales as CSV..."; }
public class StockReport : Report { public override string Export() => "stock as CSV..."; }
public class TaxReport : Report { public override string Export() => "tax as CSV..."; }
// One new report = one new class. The compiler enforces Export(). Nothing to forget.যদি exporting domain-এর বাইরেই থাকতে হয় (সেটা fair architectural choice), তাহলে separation রাখো কিন্তু link-টা explicit আর checked করো — startup-এ fail-fast করা একটা registry dictionary অনেক বেশি safe, রাত ৬টায় production-এ crash করা switch-এর চেয়ে।
Divergent Change বনাম Shotgun Surgery — আর এই smell কোথায়
Change Preventer শিখলে এই map মাথায় রাখতে হবে। দুটো parent smell mirror opposite, আর Parallel Inheritance Hierarchies-এর একটা precise seat আছে: এটা structure সহ Shotgun Surgery।
| প্রশ্ন | Divergent Change | Shotgun Surgery | Parallel Hierarchies |
|---|---|---|---|
| Pain-এর shape | এক class, অনেক reason | এক reason, অনেক class | এক নতুন variant, দুটো (বা বেশি) mirrored tree |
| Story version | এক দোকানদার, সব department ডাকে | এক address change, দশ office | প্রতিটা মিষ্টির shadow box দরকার |
| Cure direction | class split করো | scattered pieces gather করো | Gather — mirror tree main tree-তে ভাঁজ করো |
| Main refactoring | Extract Class, Move Method | Move Method, Move Field, Inline Class | Move Method, Move Field, তারপর shadow tree মুছো |
| See-saw-এর কোন পাশ | "too gathered" পাশ | "too scattered" পাশ | scattered পাশ, uniform-এ |
দুটো diagnostic প্রশ্ন এখানেও কাজ করে:
- একটা ধরনের change-এর জন্য (variant যোগ করা) কতটা class edit করতে হয়? দুটো বা তিনটা, সবসময় — তাই এটা Shotgun Surgery side-এ, instinct হলো gather করো।
- একটা class কি অনেক unrelated reason-এ hit হচ্ছে? না — তাই এটা Divergent Change না, split করা ভুল চিকিৎসা হবে।
Diagnosis map-এ মিষ্টির দোকান Shotgun Surgery corner-এ বসে — এক reason (নতুন মিষ্টি), কয়েকটা class touch হয় — কিন্তু free-form scatter-এর চেয়ে centre-এর কাছে, কারণ এই scatter disciplined আর predictable:
এই দ্বিতীয় point টা important। যদি ভুল করে parallel hierarchies-কে "এক জায়গায় বেশি জিনিস" ভাবো আর আরও split করো, তুমি তৃতীয় mirrored tree তৈরি করবে — প্রতিটা মিষ্টির দুটোর জায়গায় তিনটা ছায়া! নিয়মটা মনে রাখো: Divergent Change-এ split করো, Shotgun Surgery-তে gather করো — আর parallel hierarchies সবসময় gather পাশে পড়ে।
University corner: Scatter পাশ কেন এত familiar মনে হয়? কারণ mirror হলো cohesion failure dressed as coupling decision। "লাড্ডু কীভাবে pack হয়" — এই knowledge লাড্ডুর সাথেই cohesive, তারা একই module-এ থাকার কথা। কিন্তু রাখা হয়েছে আলাদা tree-তে, cross-tree coupling তৈরি করে যেটা কোনো import statement-এ record হয় না। Coupling metrics পড়ার সময় মনে রেখো: সবচেয়ে বড় offender dependency graph-এ কখনো দেখা যায় না। Ladoo আর LadooBox-এর lockstep co-change compiler-এর কাছে invisible — শুধু commit history-তে স্পষ্ট।
Real project-এ এই smell কোথায় লুকায়
Fowler-এর Refactoring, refactoring.guru, sourcemaking — সব catalogue বলে এই smell কিছু নির্দিষ্ট জায়গায় বারবার দেখা যায়:
- Event system।
OrderPlacedEvent-এর জন্যOrderPlacedEventHandlerদরকার;PaymentFailedEvent-এর জন্যPaymentFailedEventHandlerদরকার। handler tree faithfully event tree mirror করে। (মাঝে মাঝে acceptable — কিন্তু তৃতীয় tree না আসলেই হয়: validator, serializer...) - Model + renderer/view pair।
CircleShape/CircleRenderer,PdfBlock/PdfBlockPainter। drawing code geometry-র বাইরে রাখার healthy ইচ্ছা থেকে জন্ম — কিন্তু tree গুলো one-for-one lock হলেই smelly। - Entity + DTO + mapper triplet। প্রতিটা
Customerentity-র জন্যCustomerDtoআরCustomerMapper; প্রতিটা নতুন entity পুরো family জন্ম দেয়। কিছু layering ঠিক আছে; তিনটা rhyming tree একসাথে বাড়লে অন্তত code generator বা mapping library ব্যবহার করো। - Test class shadow। কিছু team-এ প্রতিটা production class-এর জন্য exactly একটা test class mandate করা হয়, name-এ mirrored। mirror বেশিরভাগ harmless, কিন্তু এটা মানুষকে behavior-এর বদলে structure test করতে push করতে পারে।
- Plugin আর serializer registry।
ThingXplusThingXSerializerplus একটা registry line। Framework গুলো মাঝে মাঝে reflection বা attribute দিয়ে এটা soften করে — আসলে framework তোমার হয়ে parallel hierarchy manage করছে।
Common thread: দ্বিতীয় tree সাধারণত একটা ভালো separation of concerns থেকে শুরু হয়েছিল। smell টা separation নিজে না — smell হলো one-for-one lockstep আর সেটা ধরে রাখা invisible contract।
কখন ignore করা যায়
| পরিস্থিতি | Ignore করা যাবে? | কারণ |
|---|---|---|
| Merge করলে model-এ unwanted dependency ঢুকবে (UI domain-এ) | হ্যাঁ — tree রাখো | Separation real architectural cleanliness কিনছে; registry বা Visitor দিয়ে mirror manage করো |
| দুটো tree ছোট আর বছরের পর বছর নতুন subclass আসেনি | হ্যাঁ | দুটো stable class-এর lockstep maintenance প্রায় কিছুই না |
| Framework বা code generator twin automatically maintain করে | বেশিরভাগ | Synchronization-এর guard আছে; human-memory danger নেই |
| কিছু subclass-এরই twin দরকার, আর missing twin startup-এই fail করে | প্রায়ই | Partial, checked mapping হলো managed design, accident না |
| প্রতি মাসে নতুন variant আসে আর twin ভুলে যাওয়া হয় | না — ঠিক করো | Smell actively "no handler found" bug তৈরি করছে |
| Mirror আগেই drift করে গেছে (orphan subclass, stale bridge map) | না — ঠিক করো | Drift মানে রোগ চলছে; এখনই mirror collapse বা guard করো |
সৎ summary: Change Preventers family-তে এই smell-এর সবচেয়ে বেশি legitimate exception আছে, কারণ দ্বিতীয় hierarchy-তে concern আলাদা করা মাঝে মাঝে excellent architecture। দুটো test দিয়ে judge করো — separation কি real value কিনছে? আর synchronization কি memory-র চেয়ে শক্তিশালী কিছু দিয়ে guard হচ্ছে? দুটোই হ্যাঁ হলে রাখো। দুটোই না হলে collapse করো।
কোন refactoring দিয়ে cure করবো
| Refactoring | এখানে কী করে | কখন reach করবে |
|---|---|---|
| Move Method | প্রতিটা twin-এর behavior corresponding primary subclass-এ নিয়ে যায় | Main collapsing tool — polymorphism mirror replace করে |
| Move Field | Twin-এর data behavior-এর সাথে নিয়ে যায় | Mirror subclass-এ শুধু method না, state-ও থাকলে |
| Inline Class | Empty হয়ে যাওয়া twin টা primary class-এ fold করে | Twin-এ আর কিছু নেই এমন cleanup step-এ |
| Replace Subclass with Fields | Data-only twin কে simple constructor data-তে পরিণত করে | Shadow tree শুধু value-তে আলাদা, behavior-এ না |
| Visitor pattern (design pattern, refactoring না) | Deliberate mirror safely manage করে | Tree দুটো architectural কারণে আলাদা থাকতে হলে |
এক নিঃশ্বাসে procedure: একটা hierarchy-র instance তার twin hold বা resolve করুক, Move Method আর Move Field দিয়ে differentiating method আর field move করো, twin subclass গুলো empty হয়ে যাক, তারপর পুরো shadow tree মুছে দাও। দুটো tree হয়ে যাবে একটা, আর variant যোগ করা হয়ে যাবে একটা joyful class।
Quick revision box
+--------------------------------------------------------------+
| PARALLEL INHERITANCE HIERARCHIES — QUICK REVISION |
+--------------------------------------------------------------+
| Story : Every new sweet forces a new box design, a new |
| price tag type, and a new register column |
| Smell : Add a subclass in tree A -> MUST add its twin |
| in tree B (names rhyme: XShape <-> XRenderer) |
| Family : Change Preventers — special case of |
| Shotgun Surgery (structured scatter) |
| Spot it : Rhyming prefixes; lockstep growth; bridge |
| switch ladders; "remember the twin" tribal rule |
| Costs : Double cost per variant; unguarded sync; drift; |
| double reading load; forgotten-twin bugs |
| Cure : GATHER -> Move Method + Move Field onto the |
| primary tree; delete the empty shadow tree |
| Keep it : Only when separation buys real architecture — |
| then guard it (registry / Visitor), don't trust |
| memory |
| Memory : One thing should never need a shadow -> MERGE |
+--------------------------------------------------------------+Practice exercise
চলো একটা দোকানকে তার ছায়া থেকে মুক্ত করি!
Exercise 1 — Mirror খুঁজে বের করো। একটা delivery app-এ এই class গুলো আছে: BikeDelivery, VanDelivery, DroneDelivery — আর অন্যদিকে: BikeFareCalculator, VanFareCalculator, DroneFareCalculator, আর এই bridge:
function calculatorFor(d: Delivery): FareCalculator {
if (d instanceof BikeDelivery) return new BikeFareCalculator();
if (d instanceof VanDelivery) return new VanFareCalculator();
if (d instanceof DroneDelivery) return new DroneFareCalculator();
throw new Error("No calculator found");
}List করো: (a) দুটো hierarchy, (b) invisible contract, (c) কেউ CycleDelivery যোগ করে twin ভুলে গেলে ঠিক কী ভাঙবে — আর কখন ভাঙবে।
Exercise 2 — Collapse করো। Refactor করো যাতে প্রতিটা delivery class-এর নিজের fare(distanceKm: number) method থাকে। calculator hierarchy আর bridge মুছে দাও। এখন CycleDelivery যোগ করতে কতটা edit লাগে? fare logic ভুলে যাওয়া থেকে কী আটকাচ্ছে?
Exercise 3 — Data নাকি behavior? ধরো প্রতিটা fare ছিল শুধু baseCharge + perKm * distance, আলাদা vehicle-এ আলাদা number। দেখাও পুরো calculator hierarchy কীভাবে দুটো constructor field হয়ে যেতে পারে subclass method-এর বদলে (উপরের Python Sweet dataclass-এর মতো)। তুমি কোন solution prefer করবে, আর কেন?
Exercise 4 — নিজের Figure 8 আঁকো। যেকোনো project নাও যেখানে rhyming class tree আছে (event আর handler-ও চলবে)। আজ একটা নতুন variant যোগ করতে কতটা edit লাগে, আর collapse-এর পরে কতটা লাগবে গুনে দেখো। before/after bar chart sketch করো। Bar যদি না কমে, তোমার mirror হয়তো deliberate boundary — "কখন ignore করা যায়" section-এর দুটো test apply করো।
Exercise 5 — Judgement call। একটা game-এ Monster subclass গুলো core engine-এ আর MonsterSprite subclass গুলো graphics module-এ আছে, one-for-one mirrored। Team lead merge করতে রাজি না: "graphics code engine-এ কখনো ঢুকবে না।" এই parallelism কি acceptable? রাখলে, এমন একটা mechanism বলো যেটা human memory-র চেয়ে ভালোভাবে team কে forgotten twin থেকে রক্ষা করবে।
(Hint: Exercise 1c — compile time-এ কিছু ভাঙবে না; error আসবে runtime-এ, প্রথম cycle delivery booking-এ। Exercise 5 — হ্যাঁ, এটা legitimate architectural separation; startup-time registry check দিয়ে protect করো যেটা কোনো Monster-এর sprite না থাকলে fast fail করবে, অথবা Visitor pattern দিয়ে mirror manage করো।)
তুমি এখন তিনটা Change Preventer-ই দেখেছ। এক লাইনে family revise করো: overloaded clerk-কে split করো (Divergent Change), scattered office গুলো gather করো (Shotgun Surgery), আর shadow tree merge করো (এটা)। Change সস্তা করো, আর বাকি সব improvement সম্ভব হয়ে যাবে।
সচরাচর জিজ্ঞাসা
- Parallel Inheritance Hierarchies smell টা এক লাইনে কী?
- দুটো class tree একসাথে lock হয়ে যায় — একটা tree-তে নতুন subclass যোগ করলে, অন্য tree-তেও সেটার 'pair' subclass যোগ করতেই হবে। একটা জিনিস যোগ করলে সবসময় তার ছায়াও যোগ করতে হয়।
- Parallel hierarchies দ্রুত চেনার উপায় কী?
- class-এর নামের prefix দেখো। CircleShape-এর জন্য CircleRenderer আছে, SquareShape-এর জন্য SquareRenderer আছে, TriangleShape-এর জন্য TriangleRenderer আছে — prefix গুলো দুই tree-এ rhyme করছে। এই mirrored naming-ই হলো classic sign।
- এই smell-এর সাথে Shotgun Surgery-র সম্পর্ক কী?
- এটা Shotgun Surgery-র একটা structural special case। একটা conceptual change — নতুন variant যোগ করা — দুটো mirrored class tree-তে edit করতে বাধ্য করে। cure-ও একই: mirror tree-টাকে main tree-তে ভাঁজ করে ফেলো।
- কোন refactoring দিয়ে parallel hierarchies ঠিক করবো?
- Move Method আর Move Field দিয়ে। একটা hierarchy অন্যটাকে refer করুক, তারপর behavior আর data primary type-এ নিয়ে যাও। mirror subclass গুলো empty হয়ে গেলে পুরো দ্বিতীয় tree মুছে দাও।
- কখন দুটো parallel hierarchy রেখে দেওয়া ঠিক আছে?
- হ্যাঁ, যখন merge করলে model-এ unwanted dependency ঢুকে পড়বে — যেমন UI rendering code pure domain class-এ চলে আসা। সেক্ষেত্রে parallelism একটা deliberate trade-off। Visitor pattern বা registry দিয়ে mirror টা manage করো, scattered instanceof ladder দিয়ে নয়।
আরো দেখো
সম্পর্কিত পাঠ
Shotgun Surgery: এক জায়গায় পরিবর্তন, দশ জায়গায় দৌড়াদৌড়ি
Shotgun Surgery code smell শিখবে রুবেলের বাসা বদলের গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript আর C# এর example, Divergent Change এর সাথে পার্থক্য, আর practice সহ।
Divergent Change: এক বেচারা কেরানি, অনেক বস
Divergent Change code smell শেখো একটা school-এর কেরানির গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript ও C# example, Shotgun Surgery-র সাথে তুলনা, আর practice exercise।
Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা
Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।
Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে
একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।