Alternative Classes with Different Interfaces: দুই টিফিন সার্ভিস, দুই ভাষা
একটা টিফিন ডেলিভারির গল্প দিয়ে শেখো এই code smell: দুটো class একই কাজ করে কিন্তু method নাম আলাদা, তাই একটাকে অন্যটার জায়গায় বসানো যায় না। ধাপে ধাপে ঠিক করা দেখো।
দুই টিফিন সার্ভিস, আলাদা ভাষা
ধরো, ফাতেমা আপা ঢাকার একটা অফিসে ত্রিশ জন কর্মীর জন্য প্রতিদিন দুপুরের খাবার order করেন। তিনি দুটো টিফিন সার্ভিস ব্যবহার করেন, কারণ প্রতিটা শহরের আলাদা এলাকায় deliver করে।
প্রথমটা হলো রহিমের টিফিন, বিখ্যাত ডালের জন্য। order করতে মেসেজ পাঠাতে হয়: "deliverLunch — ১২ বক্স, ভেজ, দুপুর ১২:৩০ এর মধ্যে।" রহিম ভাই কাজ শেষ হলে "lunch delivered" বলে confirm করেন।
দ্বিতীয়টা হলো করিমের খাবার ঘর। একই ডিব্বা, একই ডাল-ভাত-রুটি-তরকারি, একই দাম। কিন্তু তাদের order এর ধরন সম্পূর্ণ আলাদা। বলতে হয়: "sendMeal — meal count 12, type V, slot afternoon।" তারা confirm করে "meal dispatched" দিয়ে।
একই কাজ। একই খাবার। আলাদা ভাষা।
এবার ফাতেমা আপার প্রতিদিনের ঝামেলাটা দেখো। সোমবার রহিম ভাইয়ের কাছে order, তাই রহিম-ভাষায় লেখেন। মঙ্গলবার করিমের কাছে, তাই সব কিছু করিম-ভাষায় আবার লিখতে হয়। তিনি একটা ছোট অনুবাদ নোটবুক রাখেন: lunch = meal, boxes = count, veg = type V, 12:30 = afternoon slot। রহিম ভাইয়ের ডেলিভারি ছেলে অসুস্থ হলে সোমবারের মেসেজটা সরাসরি করিমের কাছে পাঠানো যায় না — পুরো order আবার অনুবাদ করতে হয়, আর একটা শব্দ ভুল মানে ত্রিশ জন কর্মী ক্ষুধার্ত।
একদিন সত্যিই হলো। রহিম ভাইয়ের স্কুটার সকাল ১১টায় নষ্ট। ফাতেমা আপা তাড়াহুড়ো করে নোটবুক নিয়ে অনুবাদ করলেন আর লিখলেন "sendMeal — meal count 12, type V, slot morning।" Morning slot। টিফিন এলো সকাল ৯:৪৫ এ, দুপুরের সময় একদম ঠান্ডা। তার accountant জামাল ভাই বিস্কুট খেতে খেতে কাজ করলেন। একটা ভুল অনুবাদ — 12:30 কে morning লেখা, afternoon এর বদলে — আর পুরো সিস্টেম ফেল। নোটবুকটা শুধু সময় নষ্ট করেনি, একটা নতুন ধরনের bug তৈরি করেছে: অনুবাদের ভুল।
এরপর যখন সালামের স্বাদ নামে তৃতীয় একটা সার্ভিস খুলল, আরও কম দামে? ফাতেমা আপা আফসোস করলেন। খাবারের জন্য না — কারণ সালামের একটা তৃতীয় ভাষা আছে ("bookThali — thali 12, jain no, hour 12"), নোটবুকে এখন নতুন অধ্যায় লাগবে, আর প্রতিটা অধ্যায় মানে আরেকটা ঠান্ডা-টিফিনের দিনের সম্ভাবনা।
ব্যাপারটা হাস্যকর না? তিনটা সার্ভিসই একই কাজ করে। সবাই যদি একটাই standard message মানত — order(boxCount, foodType, deliveryTime) — তাহলে এক সেকেন্ডে এক সার্ভিস থেকে অন্যটায় যাওয়া যেত, কোনো নতুন শেখা ছাড়াই।
সেই অনুবাদের নোটবুকটাই হলো Alternative Classes with Different Interfaces code smell। মানে হলো: দুটো বা তার বেশি class মূলত একই কাজ করছে কিন্তু আলাদা method নাম আর আকারে — তাই caller গুলো এক class এর বদলে অন্যটা ব্যবহার করতে পারে না code না লিখে। ফাতেমা আপা, রহিম ভাই, করিম পরিবার, আর সেই ঠান্ডা-টিফিনের দিনটা মাথায় রাখো — এই পুরো আলোচনা তাদের গল্প।
এই smell আসলে কী?
এটা Object-Orientation Abuser smells এর শেষটা, আর এটা চুপচাপ থাকে। বাকিগুলো চিৎকার করে — বিশাল switch, override ছুঁড়ে দেওয়া, null ভরা field। এটা শুধু... থাকে না। এখানে abuse হলো সেই abstraction এর অনুপস্থিতি যেটা OOP তোমাকে তৈরি করতে বলছিল।
সহজ কথায়: দুটো বা তার বেশি class একই conceptual কাজ করে, কিন্তু তাদের public interface মেলে না। আলাদা method নাম (deliverLunch বনাম sendMeal), আলাদা parameter ক্রম, আলাদা আকার (একটায় এক method, অন্যটায় দুই method), আলাদা return type (void বনাম boolean)। কোনো shared contract নেই বলে caller গুলোকে ঠিক কোন class টা তারা ধরে আছে সেটা জানতে হয় আর তার নিজস্ব ভাষায় কথা বলতে হয়।
এই smell টা তার পাশের smell এর পাশে রাখলে বোঝা সহজ। Fowler এর Refactoring এ এদের আয়না-প্রতিচ্ছবি হিসেবে দেখানো হয়েছে:
- Refused Bequest: class গুলো এমন একটা contract share করছে যেটা তাদের উচিত না — একটা মিথ্যা "is-a।"
- Alternative Classes with Different Interfaces: class গুলোর এমন একটা contract নেই যেটা share করা উচিত ছিল — একটা missing "is-a।"
একটা hierarchy মিথ্যা বলে বিদ্যমান থেকে, অন্যটা মিথ্যা বলে না থেকে।
এটা কীভাবে হয়? প্রায় কখনই ইচ্ছা করে না:
- স্বাধীনভাবে বিকশিত হওয়া। দুজন developer — বা দুটো দল, বা জানুয়ারির তুমি আর জুনের তুমি — codebase এর আলাদা কোণে একই সমস্যা solve করে, প্রত্যেকে নিজের নাম বানিয়ে নেয়। কেউ জানেই না অন্যটা আছে।
- Third-party library। দুটো payment gateway, দুটো SMS vendor, দুটো storage SDK — প্রতিটা vendor নিজের API বানায়, আর সেই পার্থক্য তোমার code এর গভীরে ঢুকে যায়।
- Copy করে আলাদা হয়ে যাওয়া। কেউ একটা class নতুন context এর জন্য copy করে, method গুলো নতুন domain এর সাথে মানাতে rename করে, আর সেই alignment ভেঙে দেয় যেটা shared type এর সুযোগ দিত।
- কেউ abstraction টা নিজের মালিকানায় নেয়নি। "টিফিন deliver করে এমন কিছু" — এই concept টা কখনো নাম পায়নি, তাই প্রতিটা concrete implementation নিজের surface বানিয়ে নিয়েছে।
এক কথায়: এই smell মানে দুটো class এক কাজ দুই ভাষায় করছে — missing shared interface প্রতিটা caller কে অনুবাদক বানিয়ে দেয়।
পুরো smell একটা map এ:
কীভাবে চিনবে
চেকলিস্ট:
- দুটো class এর parallel দায়িত্ব আছে কিন্তু method নাম মেলে না:
sendবনামdispatch,open/closeবনামconnect/disconnect। - call site গুলো না লিখে একটা class কে অন্যটার জায়গায় swap করা যায় না — যদিও দুটোই কাজ করত।
- Caller code এ branch আছে কোন concrete class আছে তার উপর:
if (useRahim) { rahim.deliverLunch(...) } else { karim.sendMeal(...) }। - ছোট translation helper আছে যাদের একমাত্র কাজ একটা class এর vocabulary কে অন্যটার ভাষায় রূপান্তর করা।
- দুটো class এ প্রায় একই রকম logic আছে যেটা surface আলাদা হওয়ায় একত্রিত করা যাচ্ছে না।
- একই parameter গুলো আলাদা ক্রমে আসে, বা একটা class এক method এ যা রাখে অন্যটা দুটো method এ ভাগ করে রাখে।
- একটা class এ bug ঠিক হলে কাউকে মনে করে তার "twin" এও ঠিক করতে হয় — আর মাঝে মাঝে ভুলে যায়।
Symptom table:
| যে symptom দেখছ | আসলে মানে কী |
|---|---|
| দুটো class এ একই কাজ, আলাদা method নাম | এক concept, দুটো vocabulary — abstraction কখনো নাম পায়নি |
Caller এ if (isVendorA) a.foo() else b.bar() | হারানো polymorphism; প্রতিটা caller concrete type এ branch করছে |
| হাতে লেখা translator/wrapper glue | Caller গুলো translation tax দিচ্ছে যেটা class গুলো প্রতিরোধ করতে পারত |
| উভয় class এ প্রায় একই logic | Duplicate Code মিসম্যাচ surface এর আড়ালে লুকিয়ে |
| একটা twin এ bug ঠিক, অন্যটায় জীবিত | Class গুলো প্রতিটা release এ আরও আলাদা হয়ে যাচ্ছে |
| Vendor তৃতীয়টা যোগ করা মানে প্রতিটা call site edit | Missing interface দিয়ে Open/Closed Principle আটকে আছে |
একটা দ্রুত detection trick দেখো: দুটো class নাও আর তাদের method list পাশাপাশি কাগজে লেখো। প্রতিটা জোড়ার মধ্যে যেটা একই মানে বহন করে তার মধ্যে একটা লাইন টানো। যদি বেশিরভাগ লাইন টানা যায় — deliverLunch ↔ sendMeal, cancelOrder ↔ stopMeal — তুমি আসলে দুটো পোশাকে একটা interface দেখছ।
একটা time-audit version ও আছে। ফাতেমা আপা একবার ট্র্যাক করলেন তার লাঞ্চ-order এর এক ঘণ্টা কোথায় যাচ্ছে। ফলাফলটা যেকোনো সংজ্ঞার চেয়ে ভালোভাবে smell টা বোঝায়:
মাত্র একটা অংশ আসল কাজ। বাকি তিনটা অংশ — ঘণ্টার আশি ভাগ — শুধু এই কারণে আছে কারণ দুটো সার্ভিস কখনো এক vocabulary তে রাজি হয়নি। যখন তুমি একটা caller class profile করবে যেটা দুটো mismatched vendor সামলাচ্ছে, তুমি একই আকার পাবে: business logic এর একটা পাতলা core, পুরু translation এর আবরণে মোড়া।
কেন এটা সমস্যা
খরচ ১: হারানো polymorphism। OOP এর superpower হলো "order() call করো আর সঠিক class সাড়া দিক।" Mismatched interface এ সেই শক্তি বন্ধ। Caller গুলোকে কোন concrete class আছে সেটা জানতে হয় আর তার dialect এ কথা বলতে হয় — প্রতিটা caller কে প্রতিটা implementation এর সাথে আটকে রাখে।
খরচ ২: Branching caller গুলোকে সংক্রামিত করে। কোনো common type নেই বলে caller গুলোতে if (কোনটা?) conditional বাড়তে থাকে। আর এখন তোমার কাছে Switch Statements smell ও আছে, সেই code জুড়ে ছড়িয়ে যেটা একটাই polymorphic call হওয়া উচিত ছিল।
খরচ ৩: Duplication drift করে। Twin গুলোতে প্রায় সবসময় shared logic থাকে — retry নিয়ম, validation, formatting। এর কোনো common home না থাকায় logic duplicate হয়, আর duplicate logic diverge করে: AnnaTiffins এ ঠিক করা bug GharKaKhana এ মাসের পর মাস বেঁচে থাকে।
খরচ ৪: প্রতিটা নতুন বিকল্প জিনিস আরও খারাপ করে। তৃতীয় vendor এর কাছে implement করার কোনো contract নেই, তাই সে তৃতীয় vocabulary বানায়। প্রতিটা call site যেটা এটা support করতে চায় তৃতীয় branch পায়। খরচ গুণনীয়ভাবে বাড়ে: callers গুণন vendors। Shared interface থাকলে খরচ হতো একটাই নতুন class, flat।
এবার দেখো একটাই order সেই ঠান্ডা-টিফিনের দিন সিস্টেমের মধ্যে দিয়ে যাচ্ছে। Manager কে ফোনে কে ধরছে তার উপর নির্ভর করে আলাদা ভাষায় কথা বলতে হচ্ছে — আর translation ধাপটাই সেই জায়গা যেখানে bug ঢোকে:
Vendor রা কিছু ভুল করেনি। রহিম ভাই স্কুটারের ব্যাপারে সৎ ছিলেন। করিমরা ঠিক যা চাওয়া হয়েছিল তাই deliver করেছিল। Bug পুরোটাই translation ধাপে — একটা ধাপ যেটা শুধু interface আলাদা হওয়ায় আছে। একটু ভাবো, এই দিনটা mood graph এ:
আর plain সংখ্যায় দেখো — প্রতিবার নতুন vendor যোগ হলে কতগুলো জায়গা পরিবর্তন করতে হয়:
Contract ছাড়া, প্রতিটা caller method প্রতিটা vendor এর জন্য একটা করে branch পায়, bars উঠতে থাকে। Contract থাকলে উত্তর সবসময় এক: নতুন class লেখো। Bar আর কখনো বাড়ে না।
একটা বাস্তব code উদাহরণ
ফাতেমা আপার সমস্যা code এ দেখি। দুটো টিফিন সার্ভিস, দুজন আলাদা developer লিখেছে যারা কখনো কথা বলেনি:
// Written by developer A, in January
class AnnaTiffins {
deliverLunch(boxes: number, isVeg: boolean, byTime: string): void {
console.log(`Anna: ${boxes} ${isVeg ? "veg" : "non-veg"} boxes by ${byTime}`);
}
cancelLunch(boxes: number): void {
console.log(`Anna: cancelled ${boxes} boxes`);
}
}
// Written by developer B, in June — same job, different language
class GharKaKhana {
// different name, different parameter ORDER, different types!
sendMeal(slot: "morning" | "afternoon", mealType: "V" | "NV", count: number): boolean {
console.log(`GharKaKhana: ${count} type-${mealType} meals, ${slot} slot`);
return true;
}
stopMeal(count: number, refund: boolean): void {
console.log(`GharKaKhana: stopped ${count} meals, refund=${refund}`);
}
}আর এখানে ফাতেমা আপা — caller — একটা branching notebook সহ অনুবাদক হতে বাধ্য:
// BAD CODE: the caller pays the translation tax, every single day
class OfficeLunchManager {
constructor(
private anna: AnnaTiffins,
private ghar: GharKaKhana,
private useAnna: boolean, // concrete-type switch in disguise
) {}
orderForStaff(staffCount: number, vegCount: number): void {
if (this.useAnna) {
this.anna.deliverLunch(vegCount, true, "12:30");
this.anna.deliverLunch(staffCount - vegCount, false, "12:30");
} else {
// translate: 12:30 -> "afternoon", true -> "V", reorder the params...
this.ghar.sendMeal("afternoon", "V", vegCount);
this.ghar.sendMeal("afternoon", "NV", staffCount - vegCount);
}
}
cancelToday(count: number): void {
if (this.useAnna) this.anna.cancelLunch(count);
else this.ghar.stopMeal(count, true); // and don't forget the refund flag!
}
}দেখো, OfficeLunchManager এর প্রতিটা method অর্ধেক আসল logic, অর্ধেক dictionary। orderForStaff এর else branch দেখো — "12:30" কে "afternoon" এ সেই হাতে-অনুবাদ ঠিক সেই লাইন যেখানে ঠান্ডা-টিফিনের দিন বাস করে। সেখানে ভুলে "morning" লিখলে কোনো compiler, GharKaKhana এর কোনো test, কিছুই অভিযোগ করবে না। Bug টা translation এ, আর translation টা caller এ।
আর যখন SwadTiffins bookThali(thaliCount, jain, deliveryHour) নিয়ে launch করবে? এখানে প্রতিটা method তৃতীয় branch পাবে, constructor তৃতীয় field পাবে, আর useAnna: boolean একটা enum হয়ে যাবে। Caller প্রতিটা vendor এর সাথে একটু একটু করে পচে।
ধাপে ধাপে পরিষ্কার করা
Standard cure এর একটা স্পষ্ট ক্রম আছে, সরাসরি Fowler এর playbook থেকে: প্রথমে interface গুলো একই করো, তারপর shared contract বের করো, তারপর caller গুলোকে সেদিকে ঘোরাও।
ধাপ ১: Rename Method — এক vocabulary তে রাজি হও। সেরা নামগুলো বেছে নাও আর উভয় class এর method গুলো মেলাতে rename করো। আধুনিক editor এটা নিরাপদে মুহূর্তে করে:
// Step 1: same names everywhere (signatures not aligned yet)
class AnnaTiffins {
order(boxes: number, isVeg: boolean, byTime: string): void { /* ... */ }
cancel(boxes: number): void { /* ... */ }
}
class GharKaKhana {
order(slot: "morning" | "afternoon", mealType: "V" | "NV", count: number): boolean { /* ... */ }
cancel(count: number, refund: boolean): void { /* ... */ }
}ধাপ ২: Signature গুলো একীভূত করো। Parameter ক্রম, type, আর return value মিলিয়ে নাও। যেখানে একটা class এর বাড়তি detail দরকার (Ghar এর refund flag), হয় সেটা উভয়তে একটা sensible default সহ যোগ করো বা class এর ভেতরে config হিসেবে রাখো:
// Step 2: one shape. Anna's "12:30" and Ghar's "afternoon" both
// become a proper shared type. Each class translates INTERNALLY.
type FoodType = "veg" | "non-veg";
class AnnaTiffins {
order(count: number, food: FoodType, deliveryHour: number): boolean {
const byTime = `${deliveryHour}:30`; // its own dialect, hidden inside
console.log(`Anna: ${count} ${food} boxes by ${byTime}`);
return true;
}
cancel(count: number): boolean { /* ... */ return true; }
}
class GharKaKhana {
order(count: number, food: FoodType, deliveryHour: number): boolean {
const slot = deliveryHour < 11 ? "morning" : "afternoon"; // hidden inside
const mealType = food === "veg" ? "V" : "NV";
console.log(`GharKaKhana: ${count} type-${mealType} meals, ${slot} slot`);
return true;
}
cancel(count: number): boolean { /* refund handled internally */ return true; }
}এটাই পুরো refactoring এর মূল insight। অনুবাদ অদৃশ্য হয়নি — সরে গেছে। এটা একসময় প্রতিটা caller এ ছিল। এখন প্রতিটা class তার নিজের dialect ভেতরে একবারই translate করে। deliveryHour < 11 নিয়মটা যেটা একসময় ফাতেমা আপার error-prone নোটবুকে ছিল এখন GharKaKhana এর ভেতরে আছে, একবার লেখা, একবার test করা, তাড়াহুড়ো করা caller এর পক্ষে ভুল করা অসম্ভব।
ধাপ ৩: Shared contract বের করো। এখন surface গুলো একই, abstraction এর নাম দেওয়া সহজ:
// Step 3: the concept finally gets a name
interface TiffinService {
order(count: number, food: FoodType, deliveryHour: number): boolean;
cancel(count: number): boolean;
}
class AnnaTiffins implements TiffinService { /* as above */ }
class GharKaKhana implements TiffinService { /* as above */ }ধাপ ৪: Caller গুলোকে contract এর দিকে ঘোরাও। ফাতেমা আপা নোটবুক ফেলে দেন:
// Step 4: the caller speaks ONE language to ANY service
class OfficeLunchManager {
constructor(private service: TiffinService) {} // any vendor fits
orderForStaff(staffCount: number, vegCount: number): void {
this.service.order(vegCount, "veg", 12);
this.service.order(staffCount - vegCount, "non-veg", 12);
}
cancelToday(count: number): void {
this.service.cancel(count);
}
}
// Swapping vendors is now ONE line at setup time:
const manager = new OfficeLunchManager(new GharKaKhana());SwadTiffins launch করলে সে TiffinService implement করে, আর OfficeLunchManager একটা অক্ষরও পরিবর্তন হয় না। শেষ আকারটা এরকম:
যদি service গুলো আসল logic ও share করে (order validation, delivery-time checks), Extract Superclass দিয়ে একটা common base class এ তুলে নাও। প্রায়ই দেখবে দুটো class এতটাই একই যে একটা সহজেই মুছে ফেলা যায়।
College corner: এই refactoring হলো interface unification, আর এটা চুপচাপ SOLID এর "D" deliver করে — Dependency Inversion Principle। আগে: OfficeLunchManager (high-level policy) সরাসরি AnnaTiffins আর GharKaKhana (low-level details) এর উপর নির্ভর করত। পরে: manager আর vendor দুটোই TiffinService abstraction এর উপর নির্ভর করে, dependency arrow গুলো contract এর দিকে ভেতরে নির্দেশ করে। এই inversion ই vendor গুলোকে pluggable করে আর manager কে একটা fake দিয়ে testable করে। আরেকটা জানার মতো বিষয়: TypeScript আর Go structural typing ব্যবহার করে — যেকোনো class যার আকার TiffinService এর সাথে মেলে সেটা automatically satisfy করে। Java আর C# nominal typing ব্যবহার করে, যেখানে class কে explicitly implements declare করতে হয়। Structural typing unification সস্তা করে, কিন্তু contract এর নামকরণ এখনো গুরুত্বপূর্ণ: TiffinService নামের একটা interface concept টা document করে, test fake anchor করে, আর পরের developer কে সেই vocabulary দেয় যেটা ফাতেমা আপার নোটবুক কখনো standardize করেনি। Ecosystem-scale প্রমাণ: Java তে SLF4J আর .NET এ Microsoft.Extensions.Logging ঠিক এই refactoring পুরো industry এর logging dialect এ apply করা।
Repair নিজেই স্পষ্ট ধাপের মধ্যে দিয়ে যায়। ধাপগুলো জানা তোমাকে live codebase এ নিরাপদে করতে সাহায্য করে, প্রতিটা transition এ এক commit:
দুটো ending লক্ষ্য করো। কখনো কখনো তুমি "callers on contract" এ থামো — এক interface এর পেছনে দুটো সত্যিকারের আলাদা vendor। আর কখনো কখনো, unification এর পরে, তুমি আবিষ্কার করো twin গুলো সবসময় একই class ছিল। আর সবচেয়ে আনন্দের refactoring ঘটে: git rm একটা।
যদি class গুলো edit করতে না পারো — যেমন, দুটো third-party SDK? তাহলে boundary তে একই idea apply করো: প্রতিটা SDK এর জন্য একটা পাতলা Adapter class লেখো যেটা তোমার TiffinService interface implement করে আর ভেতরে translate করে। Vendor এর method rename করতে পারবে না, কিন্তু তাদের vocabulary তোমার দরজায় থামাতে পারবে। লক্ষ্য করো এটা Strategy idea এর সাথেও মেলে — এক contract এর পেছনে বিনিময়যোগ্য service ঠিক যা Strategy pattern formalize করে।
C# তে একই smell
একটা ছোট C# version — দুটো দলের লেখা দুটো file-storage helper:
// BAD: same job, two dialects
class LocalDiskStore
{
public void SaveFile(string name, byte[] data) { /* write to disk */ }
public byte[] LoadFile(string name) => Array.Empty<byte>();
}
class CloudBucketStore
{
public bool Upload(byte[] content, string key) { return true; } // reversed params!
public byte[] Download(string key) => Array.Empty<byte>();
}
// Every caller branches:
void Backup(string name, byte[] data, bool useCloud,
LocalDiskStore disk, CloudBucketStore cloud)
{
if (useCloud) cloud.Upload(data, name);
else disk.SaveFile(name, data);
}Rename + signature alignment + interface extraction এর পরে:
// GOOD: one contract, swappable implementations
interface IFileStore
{
void Save(string name, byte[] data);
byte[] Load(string name);
}
class LocalDiskStore : IFileStore
{
public void Save(string name, byte[] data) { /* write to disk */ }
public byte[] Load(string name) => Array.Empty<byte>();
}
class CloudBucketStore : IFileStore
{
public void Save(string name, byte[] data) { /* upload, params translated inside */ }
public byte[] Load(string name) => Array.Empty<byte>();
}
void Backup(string name, byte[] data, IFileStore store) => store.Save(name, data);Backup একটা branching translator থেকে একটাই সৎ লাইনে নেমে এসেছে — আর এটা এখন এমন storage backend support করে যেগুলো এখনো উদ্ভাবন হয়নি। খারাপ version এ reversed-parameter trap লক্ষ্য করো: Upload(data, name) বনাম SaveFile(name, data)। তাড়াহুড়ো করে vendor switch করার সময় ভুল ক্রমে argument pass করলে, type এর উপর নির্ভর করে, এটা compile ও হতে পারে। এটাই C# এর "morning slot এর বদলে afternoon।"
বাস্তব project এ এই smell কোথায় লুকায়
- Payment gateway। একটা SDK বলে
charge(amount, currency), পরেরটা বলেcreatePayment(currencyCode, value), তৃতীয়টা builder object চায়। Mature দলগুলো একটাPaymentProviderinterface define করে আর প্রতিটা vendor কে edge এ adapter-wrap করে। - Notification channel। Email বলে
send(to, subject, body), SMS বলেdispatch(phone, text), push বলেpublish(deviceToken, payload)। Conceptually সবই "একজন মানুষকে notify করো" — এদের একNotifierএর অধীনে refactor করা এই smell এর textbook exercise। - Logging library। দশকের পর দশকের
log.error(...)বনামlogger.Error(...)বনামconsole.error(...)এই কারণেই SLF4J (Java) আরMicrosoft.Extensions.Logging(.NET) এর মতো facade project আছে। - Storage backend। Local disk, S3-style bucket, database blob — একই save/load কাজ, তিনটো vocabulary, যতক্ষণ না কেউ একটা
FileStorecontract বের করে। - In-house twin। সবচেয়ে সাধারণ ক্ষেত্র:
CustomerCsvExporter.exportData()আরOrderCsvWriter.writeFile()— দুজন teammate, repo এর দুটো কোণ, একটাই কাজ। Code review আর shared naming convention হলো vaccine। - Test fake বনাম real service। হাতে লেখা একটা fake যার method গুলো real client এর method থেকে সরে গেছে — test গুলো তখন এমন dialect exercise করে যেটা production কখনো বলে না।
সাধারণ pattern: এই smell সেখানে জমা হয় যেখানে একই capability এর একাধিক provider আছে আর কেউ শুরুতে shared contract এর নাম দিতে উঠে দাঁড়ায়নি।
কখন উপেক্ষা করা ঠিক আছে
| পরিস্থিতি | সিদ্ধান্ত | কারণ |
|---|---|---|
মিল শুধু কাকতালীয় — উভয় class এর process() আছে কিন্তু সম্পর্কহীন domain এ | Unify করো না | এখানে shared interface হবে false abstraction, smell এর চেয়ে খারাপ |
| মাত্র একটা caller, দ্বিতীয় implementation এর বাস্তব সম্ভাবনা নেই | ছেড়ে দাও | একটা ছোট local if এমন একটা abstraction বানানোর চেয়ে সস্তা যেটা কিছু অর্জন করে না |
| দুটো class third-party library থেকে আসছে | Adapt করো, rename না | সেগুলো edit করতে পারবে না; প্রতিটাকে তোমার এক interface এর পেছনে Adapter দিয়ে wrap করো |
| Class গুলো ইতিমধ্যে ১০ টার মধ্যে ৯ টা method মেলে | কাজ শেষ করো | তুমি একটা rename থেকে একটা পরিষ্কার contract — এটা করো আবার drift করার আগে |
| একই কাজ, mismatched API, একাধিক caller branching | Refactor করো | এটা পুরো মাত্রায় smell; প্রতিটা নতুন vendor ব্যথা বহুগুণ করে |
| Twin গুলোর ভেতরের logic প্রায় একই | Refactor + Extract Superclass | তুমি সম্ভবত একটা class সম্পূর্ণ মুছে ফেলবে — সর্বোত্তম ফলাফল |
দুটো প্রশ্ন যেকোনো lookalike class জোড়াকে map এ রাখে: তারা কতটা সত্যিকারের বিনিময়যোগ্য, আর কতজন caller তাদের সামলাচ্ছে?
টিফিন সার্ভিস গুলো unify-now কোণে আছে: সত্যিকার অর্থে বিনিময়যোগ্য, প্রতিটা ordering screen আর cancellation screen প্রতিদিন tax দিচ্ছে। সম্পর্কহীন process() lookalike গুলো বামে থাকে — তাদের জোর করে একসাথে করলে এমন false abstraction তৈরি হবে যেটা smell এর চেয়ে undo করা কঠিন।
Unify করার আগে সৎ test: "একটা caller কি সত্যিই একই উদ্দেশ্যে যেকোনো class গ্রহণ করবে?" যদি হ্যাঁ, তারা সত্যিকারের alternative — unify করো। যদি না, তারা শুধু দেখতে একই রকম — আলাদা রাখো। ভুল abstraction missing abstraction এর চেয়ে বেশি খরচ করে।
কোন refactoring গুলো সমাধান করে
| Refactoring | কখন ব্যবহার করবে |
|---|---|
| Rename Method | প্রথম ধাপ, সবসময়: দুটো class কে একই শব্দ দিয়ে একই operation describe করাও |
| Change Function Declaration (add/reorder parameters) | Signature মেলাও — একই parameter, একই ক্রম, একই return আকার |
| Move Method | একটা দায়িত্ব ভুল class এ আছে, surface গুলো মেলানো আটকাচ্ছে |
| Extract Interface | Surface মেলে; contract এর নাম দাও আর caller গুলোকে এর উপর নির্ভর করাও |
| Extract Superclass | Twin গুলো আকারের পাশাপাশি logic ও share করে — সেটা common parent এ তুলে নাও |
| Adapter (pattern) | Class গুলো edit করতে পারবে না (third-party); প্রতিটাকে তোমার unified interface এর পেছনে wrap করো |
| একটা class মুছে ফেলো | সবচেয়ে আনন্দের সমাপ্তি: unification এর পরে twin গুলো একই — একটা রাখো |
দ্রুত revision box
+----------------------------------------------------------------+
| ALTERNATIVE CLASSES WITH DIFFERENT INTERFACES — CHEAT SHEET |
+----------------------------------------------------------------+
| Story : Two tiffin services, same dabbas, but one speaks |
| "deliverLunch" and the other "sendMeal" — Sunita |
| Aunty keeps a translation notebook. |
| Smell : Two classes, one job, two vocabularies — callers |
| cannot swap them and must branch + translate. |
| Spot it : send vs dispatch, reversed params, if(vendorA) |
| ladders, glue translators, drifting twin logic. |
| Danger : Lost polymorphism, duplicated logic that diverges, |
| every new vendor multiplies caller edits. |
| Cure : Rename Method -> unify signatures -> Extract |
| Interface/Superclass -> callers use the contract. |
| Third-party code? Adapter at the boundary. |
| Mirror : Refused Bequest = false shared contract; |
| this smell = missing shared contract. |
| Mantra : One job deserves one vocabulary. |
+----------------------------------------------------------------+অনুশীলন করো
ধরো একটা school এর website দল দুটো আলাদা semester এ দুটো PDF-report helper লিখেছে। এদের unify করো:
class MarksheetPrinter {
generatePdf(studentName: string, marks: number[], term: string): Uint8Array {
/* builds a marksheet PDF */ return new Uint8Array();
}
emailToParent(pdf: Uint8Array, parentEmail: string): void { /* ... */ }
}
class CertificateMaker {
// same job family: produce a PDF for a student, send it home
buildDocument(activity: string, name: string): Uint8Array { // params reversed!
/* builds a certificate PDF */ return new Uint8Array();
}
dispatchHome(email: string, doc: Uint8Array): boolean { /* ... */ return true; }
}
// And the suffering caller:
function sendTermDocuments(student: { name: string; parentEmail: string },
marks: number[],
printer: MarksheetPrinter,
maker: CertificateMaker) {
const sheet = printer.generatePdf(student.name, marks, "Term 1");
printer.emailToParent(sheet, student.parentEmail);
const cert = maker.buildDocument("Science Fair", student.name);
maker.dispatchHome(student.parentEmail, cert);
}তোমার কাজ:
- Dialect গুলো map করো: দুটো method list পাশাপাশি লেখো আর matching লাইন টানো।
MarksheetPrinterএর কোন methodCertificateMakerএর কোনটার সাথে মেলে? প্রতিটা mismatch note করো — নাম, parameter ক্রম, return type। - Rename আর align করো: এক vocabulary বেছে নাও (suggestion:
build(studentName, details): Uint8ArrayআরsendHome(pdf, email): void)। উভয় class এ Rename Method apply করো আর parameter reorder করো যতক্ষণ surface একই না হয়।dispatchHomeএরbooleanreturn কোথায় যাবে সিদ্ধান্ত নাও। - Contract বের করো:
interface StudentDocumentServicedefine করো আর উভয় class কে সেটা implement করাও।sendTermDocumentsএমনভাবে rewrite করো যাতে এটাservices: StudentDocumentService[]receive করে আর কোন concrete class কোনটা সে সম্পর্কে শূন্য জ্ঞান রাখে। - জয় প্রমাণ করো: School এখন ID card চায় — তৃতীয় PDF document।
IdCardMaker implements StudentDocumentServiceযোগ করো আর গণনা করো কতটা existing line edit করতে হলো। (Target: শূন্য — চিত্র ৬ এর সাথে তুলনা করো।) - Bonus: ধরো
CertificateMakerএকটা বাইরের vendor এর npm package থেকে এসেছে আর তুমি এটা edit করতে পারবে না। একটা পাঁচ লাইনেরCertificateAdapter implements StudentDocumentServiceলেখো যেটা এটাকে wrap করে — আর লক্ষ্য করো তুমি ঠিক সেই জায়গায় Adapter pattern ব্যবহার করলে যেখানে এটা থাকা উচিত: boundary তে। - গল্প check: মূল
sendTermDocumentsএ সেই এক লাইন খোঁজো যেখানে "ঠান্ডা-টিফিনের দিন" bug লুকাতে পারত — এমন একটা জায়গা যেখানে কোনো compiler অভিযোগ ছাড়াই parameter swap বা মান ভুল অনুবাদ হতে পারত। তোমার refactored version এর কী এমন আছে যেটা সেই ভুল অসম্ভব করে তোলে?
সচরাচর জিজ্ঞাসা
- Alternative Classes with Different Interfaces smell এক কথায় কী?
- দুটো class মূলত একই কাজ করে, কিন্তু method নাম আর signature আলাদা। তাই caller গুলো একটাকে অন্যটার জায়গায় বসাতে পারে না — একটাই কাজের জন্য দুটো আলাদা ভাষা শিখতে হয়।
- এই smell আর Refused Bequest এর মধ্যে পার্থক্য কী?
- এরা একে অপরের উল্টো। Refused Bequest মানে class গুলো এমন একটা contract share করছে যেটা তাদের করা উচিত না। এই smell মানে class গুলোর কাছে এমন একটা contract নেই যেটা থাকা উচিত ছিল। একটায় মিথ্যা common interface আছে, অন্যটায় সত্যিকারের interface-ই নেই।
- এই smell এর সমাধান কী?
- প্রথমে Rename Method করো আর signature মিলিয়ে নাও যতক্ষণ না দুটো class এর API হুবহু মিলে যায়। তারপর shared interface বা superclass বের করো, caller গুলোকে সেটার উপর নির্ভর করাও। অনেক সময় একটা class একদমই অপ্রয়োজনীয় হয়ে যায় — মুছে ফেলা যায়।
- যদি mismatched class গুলো third-party library থেকে আসে যেটা edit করা যাচ্ছে না?
- Adapter pattern ব্যবহার করো। প্রতিটি library কে একটা পাতলা wrapper দিয়ে ঢেকে দাও যেটা তোমার একটাই unified interface expose করে। তাদের method rename করতে পারবে না, কিন্তু তাদের ভাষাটা তোমার boundary তে আটকে রাখতে পারবে।
- জোর করে common interface বানানো কি কখনো ভুল হতে পারে?
- হ্যাঁ। যদি মিলটা শুধু কাকতালীয় হয় — দুটো class এর হয়তো দুটোতেই process method আছে কিন্তু সম্পূর্ণ আলাদা domain এ — তাহলে shared interface হবে একটা মিথ্যা abstraction। শুধু সেই class গুলো unify করো যেগুলো সত্যিকারের alternative।
আরো দেখো
- Refactoring Guru: Alternative Classes with Different Interfaces article
- Refactoring (2nd Edition) by Martin Fowler book
- Samman Coaching: Alternative Classes with Different Interfaces article
- Adapter pattern in java-design-patterns (iluwatar) code
- SourceMaking: Alternative Classes with Different Interfaces article
সম্পর্কিত পাঠ
Refused Bequest: যে ছেলে মিষ্টির দোকানের রেসিপি নিতে চায়নি
Refused Bequest কোড স্মেল শেখো একটা পারিবারিক মিষ্টির দোকানের গল্পের মাধ্যমে — TypeScript ও C#-এ Liskov লঙ্ঘন আর delegation দিয়ে সমাধান ধাপে ধাপে।
Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা
বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।
Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা
Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।
Divergent Change: এক বেচারা কেরানি, অনেক বস
Divergent Change code smell শেখো একটা school-এর কেরানির গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript ও C# example, Shotgun Surgery-র সাথে তুলনা, আর practice exercise।