Middle Man: যে helper শুধু তোমার message পৌঁছে দেয়, নিজে কিছু করে না
Middle Man code smell টা বোঝো একটা school-এর সেই পিয়নের গল্প দিয়ে — যে শুধু চিরকুট বহন করে, নিজে কিছু যোগ করে না। যখন একটা class শুধু সব call forward করে, সেটা সরিয়ে দাও। কিন্তু Proxy, Facade, আর Adapter কেন জেনেশুনে middle man হয় — সেটাও জানো।
চিরকুট বাহক
ধরো তোমার school-এ একজন পিয়ন আছে, জামাল ভাই। সে principal-এর অফিসের বাইরে একটা কাঠের টুলে বসে থাকে। একদিন school কর্তৃপক্ষ একটা নিয়ম করলো: কেউ principal-এর সাথে সরাসরি কথা বলতে পারবে না। ছুটি চাই? একটা চিরকুট লিখো, জামাল ভাই-কে দাও। সে দশ পা হেঁটে চিরকুটটা principal-এর টেবিলে রাখে। principal "Approved" লিখে দেন, আর জামাল ভাই দশ পা হেঁটে ফিরে এসে তোমাকে দেয়।
এখানে একটা কথা সবাই ভুলে যায়: জামাল ভাই একসময় সত্যিকারের কাজ করতেন। বছর কয়েক আগে তিনি একটা register রাখতেন — কে principal-এর সাথে দেখা করলো, কখন, কী কারণে। অসম্পূর্ণ form নিয়ে আসা student-দের ফিরিয়ে দিতেন। দুপুর ২টায় একসাথে সব চিরকুট দিতেন যাতে principal সারাদিন disturb না হন। সেই টুল, সেই নিয়ম, সেই পদটা — সব কিছুর তখন মানে ছিল।
তারপর school visitor register কম্পিউটারে নিয়ে গেল। form checking চলে গেল class teacher-দের কাছে। ২টার batching বন্ধ হলো যখন principal একটা inbox tray পেলেন। একটা একটা করে সব দায়িত্ব সরে গেল। কিন্তু জামাল ভাই টুলে রইলেন, আর নিয়মটা notice board-এ রইলো। এখন সৎভাবে প্রশ্ন করো: জামাল ভাই আজকে কী যোগ করছেন?
তিনি check করেন না তোমার ছুটির form সম্পূর্ণ কিনা। দুষ্ট ছেলেদের আটকান না। কিছু translate করেন না, batch করেন না, note করেন না, filter করেন না। শুধু দুই দিকে দশ পা করে কাগজ বহন করেন। তিনি যদি ছুটিতে যান, সব request বন্ধ হয়ে যায় — গত ঈদের ছুটিতে এটা হয়েছিল, চল্লিশটা চিরকুট একটা খালি টুলের উপর জমে গিয়েছিল। principal যখন sports application-ও নিতে শুরু করলেন, জামাল ভাই-কেও সেটা বহন করতে হলো — একটা নতুন সুবিধার জন্য দুই জায়গায় পরিবর্তন। সবাই তার queue-তে অপেক্ষা করে... কিসের জন্য?
এবার তুলনা করো রুবেল ভাই-এর সাথে, আমাদের Proxy pattern গল্পের সেই watchman। রুবেল ভাই-ও তোমার আর একজন গুরুত্বপূর্ণ মানুষের মাঝখানে দাঁড়িয়ে থাকে — কিন্তু সে visitor list check করে, অপরিচিত মানুষকে ফেরায়, entry time note করে। সে protection যোগ করে। একটা receptionist যে appointment ঠিক করে, সে scheduling যোগ করে। একজন translator translation যোগ করে। এই মাঝখানের মানুষগুলো তাদের জায়গার দাম দেয়।
জামাল ভাই কিছু দেন না — আজকে। তিনি pure forwarding। কেন সরাসরি principal-এর কাছে যাও না? Code-এ তার মতো একটা class-এর একটা নাম আছে: Middle Man smell — এমন একটা class যার method গুলো শুধু অন্য object-এ call পাঠিয়ে দেয়।
এই smell টা আসলে কী?
Middle Man হলো এমন একটা class যার বেশিরভাগ method হলো এক-লাইনের forwarder:
doThing(x: number) { return this.delegate.doThing(x); }Martin Fowler তার Refactoring বইতে বলেন: একটা class-এর interface দেখো আর খোঁজো তার অর্ধেক method শুধু অন্য class-এ delegate করছে কিনা। Refactoring Guru-র catalog সরাসরি জিজ্ঞেস করে — যদি একটা class শুধু একটাই কাজ করে, অন্য class-এ কাজ পাঠানো, তাহলে এটা আছে কেন?
কিন্তু সাবধান, কারণ এই smell টা Couplers family-তে সবচেয়ে বেশি balance দরকার। Smell মানে এই না যে "একটা class delegate করে"। Delegation হলো object-oriented design-এর সবচেয়ে স্বাস্থ্যকর tool গুলোর একটা — Message Chains ঠিক করতে Hide Delegate apply করার সময় আমরা এটা খুশিমনে ব্যবহার করেছি। Smell হলো এমন একটা class যে delegate করে আর কিছু যোগ করে না: কোনো decision নেই, কোনো transformation নেই, কোনো protection নেই, কোনো simplification নেই। একটা ফাঁকা করিডোর যার মধ্য দিয়ে সব call হাঁটতে বাধ্য।
যেকোনো delegating class-এর জন্য এক-প্রশ্নের test: "এই class টা মুছে দিয়ে caller-দের সরাসরি real object-এর সাথে যুক্ত করলে কী হারাবো?" উত্তর যদি হয় "কিছু না" — তাহলে এটা Middle Man। উত্তর যদি হয় "caching হারাবো / permission check হারাবো / সহজ stable interface হারাবো / compatibility conversion হারাবো" — তাহলে class টা তার জায়গার দাম দিচ্ছে। Forwarding নিজেই সমস্যা না — forwarding দিয়ে কী ADD হচ্ছে সেটাই বিচার করো।
একটু deep জ্ঞান: David Wheeler, computing-এর প্রথম PhD-দের একজন, একটা বিখ্যাত কথা বলেছিলেন: "computer science-এর সব সমস্যা আরেকটা level of indirection দিয়ে সমাধান করা যায়।" এর কম-উদ্ধৃত পরের অংশটা, যেটা Kevlin Henney-সহ অন্যরা বলেন: "...সেই সমস্যাটা ছাড়া যেটা হলো অনেক বেশি level of indirection।" সহজ কথায়, প্রতিটা indirection layer system-এর fan-out depth বাড়ায় — কতগুলো module-এর মধ্য দিয়ে একটা request যায় — আর comprehension cost বাড়ায় (একজন reader-কে প্রতিটা layer-এর জন্য আরেকটা file খুলতে হয়) কিন্তু abstraction level না বাড়িয়ে। একটা useful metric: একটা class-এর delegation ratio (forwarding method ÷ মোট method) আর added-behaviour count (decisions, transformations, guards) দেখো। High ratio + zero added behaviour = Middle Man।
Middle Men কোথা থেকে আসে?
তিনটা সাধারণ জন্মের গল্প, আর তিনটাই innocent:
- বেশি করে fix করা Message Chain। Caller-দের
a.getB().getC().getD()লেখা থেকে বাঁচাতে কেউ Hide Delegate apply করলো — ভালো! তারপর আবার করলো, আর আবার, প্রতিটা hop-এ। Middle class গুলো forwarder-এ ভরে গেল যতক্ষণ না forwarding-ই সব কিছু হয়ে গেল। এক smell-এর cure, বেশি করলে, অন্য smell হয়ে গেল। Fowler সরাসরি বলেন: Remove Middle Man হলো Hide Delegate-এর উল্টো। - যে class তার কাজ শেষ হওয়ার পরেও টিকে আছে। Logging করতে, বা retry করতে, বা permission check করতে একটা wrapper যোগ করা হয়েছিল। পরে logging চলে গেল middleware-এ, check চলে গেল gateway-তে — কিন্তু wrapper রয়ে গেল, কোনো কারণ ছাড়াই call forward করতে, কারণ প্রতিটা client ইতিমধ্যে তার interface-এর উপর নির্ভর করছে। এটাই জামাল ভাই-এর exact জীবনী।
- Speculative indirection। "চলো একটা layer যোগ করি, কোনোদিন implementation swap করতে লাগতে পারে।" সেই কোনোদিন আর আসে না। Layer টা সেখানে বসে থাকে, ভাড়া নেয়, কিছু দেয় না। (এর cousin smell: Speculative Generality।)
দ্বিতীয় গল্পটা এত common যে এটার একটা state diagram দরকার। বেশিরভাগ Middle Man জন্ম নেয় না — তারা পেছনে ফেলে যাওয়া হয়:
EmptyShell-এর self-loop টা দেখো। এটাই বিপদজনক অবস্থা — class টা বছরের পর বছর সেখানে বসে থাকতে পারে, শুধু দেখতে important লাগে বলে।
কীভাবে চিনবে
Checklist:
- Class টা খোলো। বেশিরভাগ method কি
return _delegate.same(args);ধরনের এক-লাইনার? - এর public interface কি অন্য একটা class-কে প্রায় হুবহু mirror করে, যেন কারো drawing-এর উপর tracing paper রেখে এঁকেছে?
- একটা feature যোগ করতে কি দুই জায়গায় edit করতে হয় — delegate-এ real method আর এখানে forwarding method?
- Debugging করার সময় কি সব সময় এই class-এর মধ্য দিয়ে step through করো কিন্তু কখনো থামো না, কারণ এখানে কিছুই হয় না?
- Class টা কি একসময় real কাজ করতো (caching, checks, logging) যেটা তারপর সরে গেছে, খালি খোল রেখে?
- Class টা কি কোনো decision নেয় না? কোনো
ifনেই, কোনো transformation নেই, কোনো validation নেই, কোনো error handling নেই — শুধু relay?
Fowler-এর rough yardstick: যদি প্রায় অর্ধেক method হয় pure delegation, তাহলে Middle Man সন্দেহ করো। নিচের school clerk-এর জন্য honest audit এরকম দেখায়:
এই severity table ব্যবহার করো:
| Class-এ কী পাচ্ছো | Verdict |
|---|---|
| কয়েকটা forwarder + বেশ কিছু method যেগুলো real কাজ করে | Healthy — একটা working class-এর মধ্যে normal delegation |
| Forwarder যেগুলো check, default, conversion, বা log যোগ করে | Healthy — প্রতিটা forward তার hop-এর দাম দিচ্ছে |
| অর্ধেক method pure pass-through | Suspect — এক-প্রশ্নের test করো |
| প্রায় সব method pass-through, কোথাও কোনো decision নেই | Middle Man — সরাও বা real কাজ দাও |
| সব কিছু pass-through, আর class টাও tiny আর aimless | Middle Man + Lazy Class — নির্ভয়ে মুছে দাও |
| Forwarding with declared purpose: access control, caching, simplification, interface conversion | একটা pattern, smell না — Proxy / Facade / Adapter / Decorator (নিচে দেখো) |
একই judgment chart হিসেবে। দুটো axis সব কিছু decide করে: interface-এর কতটুকু forward হচ্ছে, আর প্রতিটা forward কী যোগ করছে:
একটু ভাবো — smell corner কি এই chart-এ bottom-right? আবার দেখো। x-axis হলো added value, y-axis হলো কতটুকু forward হচ্ছে। যে class সব কিছু forward করে আর checks যোগ করে (top-right) সেটা fine Proxy। যে class সব কিছু forward করে কিন্তু কিছু যোগ করে না (top-left) সেটা smell — এটাই Mermaid-এর numbering-এ quadrant 4। Lesson একটাই: forwarding axis একা কখনো একটা class-কে দোষী প্রমাণ করে না; শুধু value axis করে।
কেন এটা সমস্যা
১. কোনো value ছাড়া indirection। প্রতিটা call একটা extra hop দেয়, প্রতিটা reader একটা extra file খোলে — আর সেই hop কিছু কিনে না। Design-এ বলা হয়: indirection-এর প্রতিটা layer নিজেকে justify করতে হবে। এটা পারে না।
২. চিরকালের জন্য double maintenance। Delegate-এ একটা নতুন method যোগ হলে middle man-কেও সেটা mirror করতে হবে। একবার ভুলে গেলে caller-রা নতুন feature-এ পৌঁছাতে পারবে না। দুটো interface sync-এর বাইরে চলে যায়, আর সেগুলো মিলিয়ে রাখা কারো permanent chore হয়ে যায়। এক behavior, দুই edit — project-এর বাকি জীবন।
৩. Real design লুকিয়ে ফেলে। একজন নতুন programmer controller → service → repository পড়ে মনে করে তিনটা responsibility আছে। এক ঘণ্টা trace করার পর সে আবিষ্কার করে service টা আসলে ফাঁকা ছিল — real কথা ছিল controller আর repository-র মধ্যে। খালি layer টা architecture সম্পর্কে মিথ্যা বলেছে।
৪. False reassurance এটাকে permanent করে। Layer টা দেখতে serious structure-এর মতো। মানুষ load-bearing মনে হওয়া কিছু মুছতে দ্বিধা করে। ফলে dead weight জমে যায়, পরের module-এ "আমাদের standard pattern" হিসেবে copy হয়, আর multiply করে।
Cost নম্বর ২ graph করার মতো, কারণ path-এ প্রতিটা hollow layer stack হওয়ার সাথে সাথে এটা বাড়তে থাকে:
মাঝের box টা দেখো। আঙুল দিয়ে সেটা ঢেকে দাও, arrow গুলো সরাসরি যুক্ত করো — কিছু কি বদলায়? না। এই আঙুলের test-ই হলো refactoring যেটা আমরা এখন করতে যাচ্ছি।
Cargo-cult version থেকে সাবধান: "প্রতিটা controller-কে একটা service call করতেই হবে, প্রতিটা service-কে একটা repository call করতেই হবে" — এমনকি যখন service-এর কিছু করার নেই তখনও। এই ধরনের নিয়ম industrial scale-এ Middle Men manufacture করে। Layer গুলো responsibility-র জন্য, ceremony-র জন্য না। কোনো particular flow-এ যদি একটা layer-এর কোনো responsibility না থাকে, সেই flow সেটা skip করতে পারে।
Real code-এর উদাহরণ
এই হলো TypeScript-এ জামাল ভাই। একটা StudentOfficeClerk যে teacher-দের আর records system-এর মাঝখানে দাঁড়িয়ে আছে:
class StudentRecords {
find(id: number): Student | undefined { /* real lookup */ return undefined; }
save(s: Student): void { /* real persistence */ }
remove(id: number): void { /* real deletion */ }
attendanceOf(id: number): number { /* real query */ return 92; }
}
// The Middle Man — count the decisions made below: zero.
class StudentOfficeClerk {
constructor(private records: StudentRecords) {}
find(id: number) { return this.records.find(id); }
save(s: Student) { return this.records.save(s); }
remove(id: number) { return this.records.remove(id); }
attendanceOf(id: number) { return this.records.attendanceOf(id); }
}
// Every caller in the school goes through the clerk:
class ClassTeacher {
constructor(private clerk: StudentOfficeClerk) {}
markLongAbsent(id: number): string {
const attendance = this.clerk.attendanceOf(id);
return attendance < 75 ? "Send letter to parents" : "OK";
}
}StudentOfficeClerk লাইন by লাইন পড়ো আর একটাও if, একটা transformation, একটা check, একটা log খোঁজো। কিছু নেই। এর interface হলো StudentRecords-এর photocopy। একটা single call trace করো আর দেখো চিরকুট দশ পা হাঁটছে কোনো কারণ ছাড়াই:
Mirroring টা class diagram-এ আরো স্পষ্ট — clerk-এর interface হলো records system-এর উপরে tracing paper:
যেদিন StudentRecords-এ feesPendingOf(id) যোগ হবে, কাউকে মনে রাখতে হবে clerk-এ পঞ্চম photocopy line যোগ করতে — নাহলে teacher-রা fees সম্পর্কে জিজ্ঞেস করতে পারবে না।
আর class টা দেখতে important লাগে! এর একটা respectable নাম আছে, constructor আছে, dependency injection আছে। File list দেখে একজন reviewer মনে করবে এটা clerical validation করে। এই ভুল ধারণাটাই উপরের cost নম্বর ৩ আর ৪।
সম্পূর্ণতার জন্য, Python-এ একই hollow layer — সেখানে এটা লেখা আরো সহজ:
# Smelly: a "service" that photocopies the repository
class StudentService:
def __init__(self, records):
self._records = records
def find(self, sid): return self._records.find(sid)
def save(self, student): return self._records.save(student)
def remove(self, sid): return self._records.remove(sid)
def attendance_of(self, sid): return self._records.attendance_of(sid)চারটা লাইন, চারটা forward, শূন্যটা decision। Python-এ এই ধরনের class প্রায়ই "পরে logic যোগ করার জায়গা" এই excuse দিয়ে লুকিয়ে থাকে। পরে বলে একটা কথা আছে — সেটা সাধারণত আসে না। আর যখন আসে, সেটা সাধারণত একটা specific method-এ যায়, পুরো photocopied layer-এ না।
ধাপে ধাপে ঠিক করো
Cure হলো Remove Middle Man — caller-দের সরাসরি principal-এর সাথে কথা বলতে দাও। আস্তে আস্তে, ধাপে ধাপে, কারণ অনেক caller middle man-এর interface-এর উপর নির্ভর করছে।
ধাপ ১ — এক-প্রশ্নের test চালাও। মাথায় clerk টা মুছে দাও। কী হারালাম? কিছু না — কোনো check নেই, কোনো cache নেই, কোনো simplification নেই। Confirmed Middle Man। (যদি একটা method real কাজ করতো — ধরো save student validate করতো — তাহলে plan বদলে যায়: real কাজের জন্য class টা রাখো আর শুধু ফাঁকা forwarder গুলো Inline Method দিয়ে সরাও।)
ধাপ ২ — Delegate expose করো। Caller-দের real object-এ পৌঁছানোর রাস্তা দাও। প্রায়ই সবচেয়ে সহজ move হলো StudentOfficeClerk যেখানে inject হতো সেখানে সরাসরি StudentRecords inject করা:
class ClassTeacher {
constructor(private records: StudentRecords) {} // direct line now
markLongAbsent(id: number): string {
const attendance = this.records.attendanceOf(id);
return attendance < 75 ? "Send letter to parents" : "OK";
}
}ধাপ ৩ — Caller-দের একে একে migrate করো। Clerk টা এক ধাক্কায় মুছো না। একটা caller সরাও, compile করো, test করো, commit করো। পরেরটা সরাও। Clerk-এর photocopied interface মানে প্রতিটা migration হলো mechanical rename — কম risk, steady progress।
ধাপ ৪ — খালি খোল মুছে দাও। যখন শেষ caller সরে গেছে, StudentOfficeClerk-এর কোনো reference নেই। File টা মুছে দাও। এই মুহূর্তটা enjoy করো — code মুছে দেওয়া programming-এর বিশুদ্ধতম আনন্দের একটা।
ধাপ ৫ — Middle man যদি প্রায় তার delegate-ই হয়, inheritance ভাবো। যখন একটা class genuinely related একটা class-এর প্রায় পুরো interface forward করে, Replace Delegation with Inheritance কখনো কখনো cleaner: এটাকে subclass বানাও, আর হাতে লেখা সব forwarder অদৃশ্য হয়ে যাবে কারণ method গুলো simply inherited। শুধু তখনই ব্যবহার করো যখন "is-a" relationship সত্যিকারে সত্য — শুধু typing বাঁচাতে না।
ধাপ ৬ — পরের বারের জন্য balance মনে রাখো। মনে করো অনেক Middle Men কোথা থেকে আসে: Message Chains fight করতে Hide Delegate বেশি apply করা। দুটো refactoring হলো inverse; দুটো smell হলো opposite ditch। কয়েকটা forwarding method যেগুলো caller-দের changing structure থেকে রক্ষা করে = ভালো delegation। শুধু forward করা class = Middle Man। তুমি career-এ অনেকবার এই balance adjust করবে, দুই দিকেই, আর এটাই স্বাভাবিক।
Deep জ্ঞান: এই balance beam-এর architecture literature-এ একটা নাম আছে: সঠিক level of indirection বেছে নেওয়া হলো coupling (chains caller-কে structure-এর সাথে couple করে) আর cognitive distance (layers reader-কে আরো file traverse করায়)-এর মধ্যে trade। Large codebase-এ ব্যবহারিত একটা heuristic: একটা indirection layer অন্তত তিনটার একটা justification satisfy করতে হবে — এটা independently vary করে (তুমি genuinely implementation swap করো), এটা invariant guard করে (validation, permissions, transactions), বা এটা boundary stabilise করে (এর পেছনে যা আছে তার চেয়ে এর interface কম পরিবর্তন হয়)। তিনটার কোনোটাই না হলে, সংজ্ঞার দিক থেকে, এটা Middle Man — architecture diagram-এ নাম যতই official শোনাক না কেন।
C#-এ একই smell
সবচেয়ে বিখ্যাত C# middle man হলো hollow service layer:
// Smelly: a service that only photocopies the repository
public class UserService
{
private readonly UserRepository _repo;
public UserService(UserRepository repo) => _repo = repo;
public User Find(int id) => _repo.Find(id);
public void Save(User u) => _repo.Save(u);
public void Delete(int id) => _repo.Delete(id);
public bool Exists(int id) => _repo.Exists(id);
}Remove Middle Man করার পর, caller-রা real জিনিস ধরে:
public class RegistrationController
{
private readonly UserRepository _repo;
public RegistrationController(UserRepository repo) => _repo = repo;
public void Register(User user)
{
if (!_repo.Exists(user.Id))
_repo.Save(user);
}
}কিন্তু এর সাথে তুলনা করো এমন একটা service যে তার জায়গার দাম দেয় — একই shape, ভিন্ন soul:
// NOT a middle man: every method adds a decision or protection
public class UserService
{
private readonly UserRepository _repo;
private readonly IAuditLog _audit;
public User Find(int id) =>
_repo.Find(id) ?? throw new UserNotFoundException(id); // adds a guarantee
public void Save(User u)
{
u.Validate(); // adds validation
_repo.Save(u);
_audit.Record("user.saved", u.Id); // adds auditing
}
}Diagram-এ একই arrow, বিপরীত verdict। Smell কখনো arrow-তে ছিল না — সেটা ছিল arrow-র চারপাশের শূন্যতায়।
Real project-এ এই smell কোথায় লুকিয়ে থাকে
১. Anemic service layer। XService যে XRepository-কে photocopy করে, হাজার হাজার codebase-এ পাওয়া যায় কারণ "আমাদের সবসময় service layer থাকে"। যেখানে service genuinely orchestrate করে — transactions, validation, multiple repositories — সেটা real। যেখানে এটা CRUD one-to-one forward করে, সেটা dependency injection সহ জামাল ভাই।
২. Feature শেষ হওয়ার পরেও টিকে থাকা wrapper। CachingProductClient যার cache একটা redis migration-এ সরিয়ে নেওয়া হয়েছে; RetryingHttpService যার retry একটা resilience library-তে চলে গেছে। Class-এর নামের adjective টা এখন মিথ্যা, শুধু forwarding বাকি।
৩. Manager-of-a-manager chain। TaskManager call করে TaskCoordinator-কে যে call করে TaskExecutor-কে — আর শুধু শেষেরটাতে logic আছে। Enterprise codebase কখনো কখনো একটা working basement-এর উপর তিনটা hollow floor stack করে।
৪. Framework-এর চারপাশে wrapper, পরে abandoned। একটা team logging বা HTTP library wrap করে "পরে swap করতে পারবো বলে", কিন্তু wrapper library-র exact একই method গুলো এক-to-one expose করে। Interface photocopying কোনো swap-protection দেয় না — shapes যদি identical হয়, inside swap করলে সব কিছু break হবে। হয় একটা real abstraction design করো (আলাদা, ছোট interface — সেটা হলো Adapter বা Facade) অথবা সরাসরি library ব্যবহার করো।
৫. Over-Demeter'd domain model। Law-of-Demeter cleanup-এর পর, Order-এ ত্রিশটা method, পঁচিশটা যেগুলো Customer, Address, আর Invoice-এ forward করে। Chain গুলো চলে গেছে — আর Order একটা switchboard হয়ে গেছে। Balance beam মাঝের পার হয়ে গেছে।
৬. Re-export barrel আর god interface। যে module শুধু অন্য module-এর API one-to-one re-expose করতে exist করে। একটা ছোট curated facade useful; একটা full mirror হলো maintenance debt।
কখন ignore করা ঠিক আছে
এটা এই post-এর সবচেয়ে গুরুত্বপূর্ণ section। কিছু middle man জেনেশুনে hire করা হয়। আমাদের চারটা structural design pattern, structurally, middle man — আর এরা excellent design, কারণ তাদের forwarding কিছু যোগ করে:
| Delegating class | Forwarding কী যোগ করে | Verdict |
|---|---|---|
| Proxy | Access control, lazy loading, caching, logging — decide করে call যাবে কিনা/কখন | রাখো — pattern, smell না |
| Facade | একটা জটিল subsystem লুকিয়ে একটা simple, stable entry point; caller-রা internals-এর উপর নির্ভর করা বন্ধ করে | রাখো — pattern, smell না |
| Adapter | এক interface কে অন্যটায় convert করে যাতে incompatible class একসাথে কাজ করতে পারে | রাখো — pattern, smell না |
| Decorator | কিছু call-এ extra behaviour যোগ করে; বাকিগুলোর pass-through intentional | রাখো — pattern, smell না |
| Unstable structure-এর উপর sensible Hide Delegate-এর ফেলে যাওয়া forwarder | Model পরিবর্তন absorb করা stable boundary | রাখো — এটাই Demeter trade কাজ করছে |
| System boundary-তে API gateway / anti-corruption layer | তোমার model-কে external system-এর shape থেকে রক্ষা | রাখো |
| এমন class যার প্রতিটা method unchanged relay করে, উপরের কিছুই যোগ না করে | কিছু না | Remove Middle Man |
একই structure এক post-এ celebrated pattern আর অন্য post-এ smell কীভাবে হতে পারে? কারণ structure মানে intent না। রুবেল ভাই আর জামাল ভাই দুজনেই তোমার আর একজন গুরুত্বপূর্ণ মানুষের মাঝখানে দাঁড়িয়ে আছে। একজন filter করে, protect করে, record করে; অন্যজন শুধু চিরকুট হাঁটায়। Diagram দেখতে একই; প্রতিটা hop-এ added value বিপরীত। এই জন্যই শুধু shape দিয়ে smell খোঁজা যাবে না — সবসময় জিজ্ঞেস করো layer টা কী করে, কোথায় দাঁড়িয়ে আছে সেটা না।
পুরো judgment একটা map-এ দেখো:
Law of Demeter trade-off নিয়ে একটা note, শেষ তিনটা post যুক্ত করতে। Demeter ("শুধু তোমার immediate friend-দের সাথে কথা বলো") তোমাকে delegating method ADD করতে push করে, যাতে caller-রা object graph navigate করা বন্ধ করে — এটা Message Chains cure করে আর Inappropriate Intimacy কমায়। Middle Man smell পাল্টা বলে: delegation-কে একটা class-এর একমাত্র কাজ হতে দিও না। কোনো rule সব জায়গায় জেতে না। তুমি যে skill build করছো সেটা হলো case by case judge করা — একটা hop তার cost-এর দাম দিচ্ছে কিনা।
কোন refactoring দিয়ে ঠিক করবে
| Refactoring | কখন ব্যবহার করবে | কী করে |
|---|---|---|
| Remove Middle Man | Class টা mostly/entirely pass-through | Caller-রা delegate-এর সাথে সরাসরি কথা বলে; খালি layer মুছে যায় |
| Inline Method | শুধু কয়েকটা forwarder একটা otherwise-working class দূষিত করছে | Trivial forwarder গুলো caller-এ fold হয়; class তার real কাজ রাখে |
| Replace Delegation with Inheritance | Middle man একটা truly related class-এর প্রায় সব কিছু forward করছে | Inherited method হাতে লেখা forwarder replace করে — শুধু যখন "is-a" সৎভাবে সত্য |
| Hide Delegate | (উল্টোটা!) বেশি সরিয়েছো আর chain ফিরে এসেছে | একটা purposeful forwarder ফিরিয়ে যোগ করে — balance beam দুই দিকেই যায় |
Quick Revision
+=================================================================+
| MIDDLE MAN — QUICK REVISION |
+=================================================================+
| STORY : Shankar bhaiya only carries chits to the principal. |
| No checking, no protecting, no translating. |
| Why not talk to the principal directly? |
| |
| SMELL : A class whose methods mostly just forward: |
| doThing(x) { return delegate.doThing(x); } |
| Fowler's hint: ~half the methods delegating. |
| |
| COSTS : pointless hop, double maintenance (every new |
| feature = 2 edits), hidden real design, |
| false look of importance |
| |
| TEST : "Delete it mentally — what would we lose?" |
| Nothing lost -> Middle Man, remove it |
| Check/cache/simplify/convert lost -> keep it! |
| |
| CURE : Remove Middle Man (callers go direct) |
| Inline Method (few forwarders only) |
| Replace Delegation with Inheritance (honest is-a) |
| |
| NOT A : Proxy (controls access), Facade (simplifies), |
| SMELL : Adapter (converts), Decorator (adds behaviour) |
| — these forward ON PURPOSE and earn every hop. |
| |
| BALANCE : Hide Delegate <-> Remove Middle Man are inverses. |
| Message Chains and Middle Man are opposite ditches; |
| healthy delegation walks between them. |
+=================================================================+Practice করো
ধরো একটা food-delivery codebase আছে তিনটা delegating class সহ। তোমার কাজ: প্রতিটাকে judge করো।
class KitchenSystem {
prepare(orderId: number): void { /* real cooking workflow */ }
estimateMinutes(orderId: number): number { return 25; }
cancel(orderId: number): void { /* real cancellation */ }
}
// Class 1
class KitchenCoordinator {
constructor(private kitchen: KitchenSystem) {}
prepare(id: number) { return this.kitchen.prepare(id); }
estimateMinutes(id: number) { return this.kitchen.estimateMinutes(id); }
cancel(id: number) { return this.kitchen.cancel(id); }
}
// Class 2
class KitchenGate {
constructor(private kitchen: KitchenSystem, private auth: AuthService) {}
cancel(id: number, staff: Staff) {
if (!this.auth.canCancel(staff)) throw new Error("Not allowed");
this.kitchen.cancel(id);
}
prepare(id: number) { return this.kitchen.prepare(id); }
}
// Class 3
class LegacyPosAdapter {
constructor(private kitchen: KitchenSystem) {}
// The 20-year-old POS terminal sends strings like "PREP|1042"
handle(message: string) {
const [cmd, id] = message.split("|");
if (cmd === "PREP") this.kitchen.prepare(Number(id));
if (cmd === "CANC") this.kitchen.cancel(Number(id));
}
}তোমার tasks:
- প্রতিটা class-এ এক-প্রশ্নের test apply করো। "মুছে দিলে কী হারাবো?" প্রতিটা class-এর জন্য এক লাইন লিখো।
- তিনটাকেই Figure 4-এর chart-এ plot করো। দুটো axis-এ (value added, fraction forwarded) প্রতিটা class-এর position estimate করো। কোন quadrant-এ পড়ে?
- Verdict দাও। কোনটা true Middle Man, কোনটা (partial) protection Proxy, আর কোনটা Adapter? মনে রাখো একটা class mixed — এর একটা earning method আছে আর একটা pure forwarder। সেই forwarder-টার সাথে কী করবে? (Hint: Inline Method।)
- Class 1 refactor করো। Step-by-step Remove Middle Man describe করো: কোন caller আগে বদলায়, শেষে কী মুছে যায়?
- Class 3 defend করো এক বাক্যে এমন teammate-কে যে বলছে "এটা শুধু kitchen-এ forward করে, মুছে দাও।"
- Balance question। ছয় মাস পর, সব জায়গায় caller লিখছে
kitchen.scheduler().queue().position(orderId)— একটা fresh Message Chain। এখন কোন refactoring ব্যবহার করবে, আর সেটা apply করার সময় কোন smell আবার তৈরি না করার ব্যাপারে সাবধান থাকতে হবে? - Bonus। তোমার team-এর rulebook বলে "controller কখনো সরাসরি repository touch করবে না; সবসময় service দিয়ে যেতে হবে।" দুটো বাক্য লিখো: একটা rule টা defend করে, একটা explain করে কখন এটা Middle Men manufacture করে। (কোনো single right answer নেই — এটাই পুরো post-এর judgment।)
যদি structurally similar তিনটা class-কে তিনটা আলাদা verdict দিয়ে থাকো, তুমি এখানের সবচেয়ে গভীর lesson শিখেছো: layer কী যোগ করে সেটা দিয়ে judge করো, দেখতে কেমন সেটা দিয়ে না। একদম first class!
সচরাচর জিজ্ঞাসা
- Middle Man code smell মানে কী?
- এমন একটা class যার প্রায় সব method শুধু অন্য object-এ call forward করে — এরকম এক-লাইনের forwarder: return delegate.doThing()। Fowler বলেন: একটা class-এর অর্ধেক method যদি শুধু delegation করে, তাহলে সেটা Middle Man সন্দেহ করো। এই layer একটা extra hop যোগ করে — কোনো decision নেই, কোনো transformation নেই, কোনো protection নেই।
- Middle Man ঠিক করবো কীভাবে?
- Remove Middle Man ব্যবহার করো: client-দের সরাসরি real object-এর সাথে কথা বলতে দাও, আর forwarding layer টা মুছে ফেলো। যদি মাত্র কয়েকটা forwarder থাকে, Inline Method দিয়ে সেগুলো caller-এ ঢুকিয়ে দাও। আর যদি class টা একটা related class-এর প্রায় পুরো interface forward করে, তাহলে Replace Delegation with Inheritance ব্যবহার করতে পারো।
- Proxy, Facade, আর Adapter কি middle man?
- এরা call forward করে, কিন্তু এরা smell না — কারণ এরা একটা কারণে forward করে। Proxy access control করে (lazy loading, caching, permissions), Facade একটা জটিল subsystem-এর সামনে একটা সহজ entry দেয়, Adapter এক interface কে অন্যটায় convert করে, আর Decorator call-এর চারপাশে extra behaviour যোগ করে। একটা delegating class কে judge করো সে কী ADD করছে সেটা দিয়ে। যদি সে decision, transformation, protection, বা stable boundary যোগ করে — রেখে দাও।
- Middle Man আর Message Chains-এর সম্পর্ক কী?
- এরা একই রাস্তার দুই দিকের খাদ। Message Chains হয় যখন caller নিজেই object-এর মধ্য দিয়ে navigate করে; এর cure হলো Hide Delegate, যেটা forwarding method যোগ করে। সেই cure বেশি করলে Middle Men তৈরি হয় — শুধু forward করা class। Remove Middle Man হলো Hide Delegate-এর ঠিক উল্টো refactoring। ভালো design এই দুইয়ের মাঝে balance রাখে।
- অকেজো middle man class কেন এত দিন codebase-এ টিকে থাকে?
- তিনটা কারণ: প্রতিটা client তার interface-এর উপর নির্ভর করে, তাই সেটা মুছতে গেলে অনেক জায়গায় ছোট ছোট edit করতে হয়; দেখতে meaningful architecture-এর মতো লাগে, তাই মানুষ ভাবে এটা কিছু একটা করছে; আর এটা একসময় সত্যিকারের কাজ করতো (logging, caching, checks) যেটা পরে সরিয়ে নেওয়া হয়েছে — খালি খোল পড়ে আছে। class টা তার কাজ শেষ করে ফেলেছে, কিন্তু জায়গা ছাড়েনি।
আরো দেখো
সম্পর্কিত পাঠ
Message Chains: বন্ধুকে জিজ্ঞেস করো, সে কাজিনকে জিজ্ঞেস করে, কাজিন চাচাকে জিজ্ঞেস করে
Message Chains code smell শেখো একটা মজার গল্পের মাধ্যমে — রুটি আছে কিনা জানতে চারজন মানুষের মধ্য দিয়ে যেতে হয়। a.getB().getC().getD() লিখলে caller পুরো রাস্তার সাথে coupled হয়ে যায়। Law of Demeter কী, আর Hide Delegate দিয়ে কীভাবে chain ঠিক করতে হয় সেটা শেখো।
Inappropriate Intimacy: দুটো class যারা একে অপরের রান্নাঘরে ঢুকে পড়ে
দুই প্রতিবেশীর গল্প দিয়ে Inappropriate Intimacy বোঝো — যারা একে অপরের রান্নাঘর সাজিয়ে দেয়। দুটো class যখন একে অপরের private অংশে হাত দেয়, তখন কেউ একা কিছু বদলাতে পারে না। Law of Demeter আর privacy ফিরিয়ে আনার refactoring শেখো।
Lazy Class: যে চাকরির কাজ শুধু একটা বাটন চাপা
Lazy Class code smell শিখো একটা মজার গল্পের মাধ্যমে। কোন class-গুলো টিকে থাকার যোগ্যতা রাখে না সেটা বুঝতে পারবে, আর Inline Class দিয়ে সেগুলো ঠিক করতে পারবে।
Remove Middle Man: পিয়ন শুধু ফরওয়ার্ড করলে, সরাসরি হেড স্যারের কাছে যাও
Remove Middle Man রিফ্যাক্টরিং শেখো একটা স্কুলের পিয়নের গল্প দিয়ে — যে প্রতিটা প্রশ্ন হেডমাস্টারের কাছে ফরওয়ার্ড করে, নিজে কিছু যোগ না করেই। যখন একটা class শুধু delegate-কে call ফরওয়ার্ড করে, তখন সেই ফরওয়ার্ডিং মুছে দাও আর client-দের সরাসরি delegate-এর সাথে কথা বলতে দাও। TypeScript আর C#-এ ধাপে ধাপে walkthrough।