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

Message Chains: বন্ধুকে জিজ্ঞেস করো, সে কাজিনকে জিজ্ঞেস করে, কাজিন চাচাকে জিজ্ঞেস করে

Message Chains code smell শেখো একটা মজার গল্পের মাধ্যমে — রুটি আছে কিনা জানতে চারজন মানুষের মধ্য দিয়ে যেতে হয়। a.getB().getC().getD() লিখলে caller পুরো রাস্তার সাথে coupled হয়ে যায়। Law of Demeter কী, আর Hide Delegate দিয়ে কীভাবে chain ঠিক করতে হয় সেটা শেখো।

21 মিনিট আপডেট: June 11, 2026beginner
code-smellscouplersmessage-chainslaw-of-demeterhide-delegatetrain-wrecktypescriptcsharp

রুটির খোঁজে চারজন মানুষের সফর

ধরো রবিবার সকাল। তুমি জানতে চাও একটাই ছোট্ট জিনিস: পাড়ার মোড়ের দোকানে রুটি আছে কিনা।

কিন্তু তুমি সরাসরি দোকানদারকে জিজ্ঞেস করলে না। তোমার পাড়ায় ব্যাপারটা এভাবে কাজ করে না। তাই তুমি তোমার বন্ধু রুবেলকে জিজ্ঞেস করলে, কারণ রুবেল সবাইকে চেনে। রুবেল রুটির ব্যাপারে জানে না, কিন্তু তার কাজিন জামাল দোকানের কাছেই থাকে — তাই রুবেল জামালকে ফোন দিলো। জামালও জানে না, কিন্তু তার চাচার বাড়ি ঠিক দোকানের উল্টোদিকে — তাই জামাল চিৎকার করে চাচাকে ডাকল। চাচা স্যান্ডেল পরে হেঁটে গিয়ে দোকানদারকে জিজ্ঞেস করলেন। দোকানদার বললেন, "হ্যাঁ, সকাল সাতটায় তাজা রুটি এসেছে।"

এবার উত্তর ফিরে আসতে লাগলো। চাচা জামালকে বললেন। জামাল রুবেলকে ফোন দিলো। রুবেল তোমাকে জানালো। কুড়ি মিনিট, চারজন মানুষ, একটা হ্যাঁ।

চিত্র ১: রবিবার সকালের রুটির খোঁজ — প্রতিটা hop মানে নতুন মানুষ আর নতুন নির্ভরতা

একটু ভাবো — এই "system" কাজ করতে কী কী দরকার ছিল। রুবেলকে বাড়িতে থাকতে হবে। রুবেলকে মনে রাখতে হবে যে জামালই সেই সঠিক কাজিন। জামালকে জানতে হবে কোন চাচা দোকানের কাছে থাকেন। জামাল যদি ঢাকায় চলে যায়, পুরো chain ভেঙে পড়বে — তোমার রুটির প্রশ্ন fail করবে জামালের বাড়ি বদলানোর কারণে, আর জামাল জানেও না যে তুমি বলে কেউ আছো!

আর সবচেয়ে হাস্যকর ব্যাপারটা হলো: তোমার প্রশ্ন ছিল রুটি নিয়ে। কিন্তু উত্তর পেতে তোমাকে জানতে হলো রুবেলের কাজিনের কথা, কাজিনের চাচার কথা — যাদের রুটির সাথে কোনো সম্পর্ক নেই। পরের রবিবার দুধ লাগলেও একই চারজনের রাস্তায় যেতে হবে। তার পরের রবিবারও। প্রতিটা ছোট প্রশ্নের জন্য পুরো toll দিতে হবে।

একটা সহজ উপায় সবসময় ছিল — তুমি মনে মনে জানতেও: রুবেলকে জিজ্ঞেস করো "রুটি আছে?" আর রুবেলকেই তার নিজের network বের করতে দাও। অথবা — আরও সহজ চিন্তা — দোকানের বাইরে একটা বোর্ড লাগাক: তাজা রুটি পাওয়া যাচ্ছে। একটা প্রশ্ন, একটা উত্তর, কারো পরিচিতির দরকার নেই।

Code-এ এই চারজনের রাস্তাটা দেখতে এরকম:

const hasBread = me.friend().cousin().uncle().shopkeeper().hasBread();

একটার পর একটা call, প্রতিটা আগেরটার result থেকে আরও গভীরে যাচ্ছে। এই smell-এর নাম Message Chains। programmers এই ধরনের line-কে train wreck-ও বলে — dot গুলো গুনে দেখো, train-এর bogey-র মতো একটার পর একটা সাজানো।

এই smell আসলে কী?

Message Chain হয় যখন code একটা object-কে আরেকটা object চায়, তারপর সেই object থেকে আরেকটা চায়, তারপর আরেকটা — object graph-এর ভেতর দিয়ে hop করতে করতে একটা value-তে পৌঁছায়:

order.getCustomer().getAddress().getCountry().getTaxRate()

Martin Fowler তার Refactoring বইয়ে বলেন: client একটা object-কে আরেকটা object চায়, সেটাকে আবার আরেকটা চায়, এভাবে চলতে থাকে। caller শুধু destination-এর সাথে না, পুরো রাস্তার সাথে coupled হয়ে যায়। Refactoring Guru-র catalog-এ এটা Couplers-এর মধ্যে আছে — মানে class গুলোকে অতিরিক্ত tight করে বেঁধে ফেলার smell — Feature Envy, Inappropriate Intimacy, আর Middle Man-এর পাশাপাশি।

একটু খেয়াল করো chain-টা কী কী দিয়ে যাচ্ছে। প্রতিটা .get একটা আলাদা class-এর আলাদা object return করছে: Order থেকে Customer, Customer থেকে Address, Address থেকে Country। caller চারটা class পার হয়ে একটা প্রশ্ন করছে। এই "আলাদা object" ব্যাপারটা মনে রাখো — এটাই fluent API থেকে আলাদা করে, সেটা একটু পরে দেখাবো।

💡

একটা সহজ reading trick: message chain পড়তে দিকনির্দেশনার মতো লাগে, প্রশ্নের মতো না। "Order থেকে customer-এ যাও, address-এ যাও, country-তে যাও, tax rate নাও" — এটা একটা route map। সুস্থ code পড়তে প্রশ্নের মতো লাগে: "Order, তোমার tax rate কত?" তোমার line-টা যদি কারো বাড়ির দিকনির্দেশনার মতো শোনায়, সেটা সম্ভবত chain।

Law of Demeter: chain যে rule ভাঙে

এই smell-এর পেছনে একটা বিখ্যাত law আছে। ১৯৮০-র দশকের শেষে Karl Lieberherr আর তার সহকর্মীরা Northeastern University-র Demeter project-এ object-oriented program-এর জন্য একটা style rule লিখলেন। এটাকে বলে Law of Demeter, বা Principle of Least Knowledge, আর এর motto টা চমৎকার সহজ:

"শুধু তোমার immediate friend-দের সাথে কথা বলো। Stranger-দের সাথে কথা বলো না।"

একটা method-এর friend হলো:

  1. নিজের object (this) আর তার field,
  2. নিজের parameter,
  3. নিজে তৈরি করা object।

Law বলে: তোমার friend-দের method call করো — কিন্তু তোমার friend তার ভেতর থেকে যা return করে তার উপর method call করো না। Friend-এর friend হলো stranger।

এবার chain-টা দেখো। order হলো friend (parameter হিসেবে — ঠিক আছে)। কিন্তু order.getCustomer() তোমাকে একজন stranger দিচ্ছে। সেই stranger-এর উপর .getAddress() call করা law ভাঙছে। সেই result হলো stranger-এর stranger। প্রতিটা extra dot তোমাকে এমন বাড়িতে নিয়ে যাচ্ছে যেখানে তোমাকে কেউ invite করেনি।

রবিবার সকালের rule: তুমি রুবেলকে, তোমার direct friend-কে, জিজ্ঞেস করতে পারো "রুবেল, রুটি আছে?" — আর রুবেল তার মতো বের করুক। তুমি যা করতে পারবে না সেটা হলো রুবেলকে door হিসেবে ব্যবহার করে তার কাজিন আর চাচার কাছে নিজে পৌঁছানো।

College corner: Law-এর formal object form, তোমার notes-এর জন্য: class C-এর method M শুধু এদের কাছে message পাঠাতে পারে — (১) C নিজে আর তার instance variable, (২) M-এর parameter, (৩) M-এর তৈরি object, (৪) global object। "শুধু এক dot ব্যবহার করো" slogan-টা rough shortcut, আর এটা বেশি-ই fire করে: numbers.filter(isEven).map(square)-এ দুটো dot আছে কিন্তু কোনো violation নেই, কারণ প্রতিটা call একই abstraction return করছে আর কারো internal expose করছে না। প্রশ্নটা কখনো "কতটা dot?" না বরং "আমি কার internal structure traverse করছি?" Researchers পরে এটাকে formally বলেছেন: object graph navigate করা (structure-এ coupling) আর এক abstraction-এ operation compose করা (শুধু interface-এ coupling) — এই দুটোর পার্থক্য।

চিত্র ২: Law of Demeter-এর memory map — কার সাথে কথা বলা যাবে, কার সাথে না

কীভাবে চিনবে

চেকলিস্ট:

  • একটা expression-এ আলাদা object-এর মধ্য দিয়ে দুই বা তার বেশি hop: a.getB().getC().getD()
  • একই deep path অনেক জায়গায় লেখা — পাঁচটা file-এ order.customer.address.city বানান করা।
  • Calling class এমন type-এর নাম জানছে যা জানার দরকার নেই। একটা Renderer Address আর Country mention করছে, যদিও তার দরকার ছিল শুধু tax rate।
  • একটা middle getter rename বা remove করলে অবাক করা দূরের file গুলোতে compile error।
  • Line-টা route-এর মতো পড়ায় ("X থেকে Y হয়ে Z-তে") result-এর request-এর মতো না।
  • প্রতিটা hop-এ defensive null check: if (o.getCustomer() != null && o.getCustomer().getAddress() != null …) — ভয়ের সিঁড়ি।

এই table দিয়ে chain কতটা serious সেটা judge করো:

যা দেখছোমাত্রাকারণ
এক hop: order.customer()নেইDirect friend-এর সাথে কথা — সম্পূর্ণ স্বাভাবিক
দুই hop, একবার লেখা, stable code-এমৃদুসহনীয়; note করো আর এগিয়ে যাও
দুই hop, অনেক file-এ বারবারমাঝারিএকটা structure change অনেক file ভাঙবে — wrap করো
তিন বা তার বেশি hop আলাদা class-এর মধ্য দিয়েবেশিCaller পুরো internal map জানে; ঠিক করো
পরিবর্তনশীল structure-এর উপর লম্বা chainগুরুতরপ্রতিটা model change project-wide hunt হয়ে যাবে
builder.setA().setB().build() — প্রতিবার একই objectChain-ই নাFluent API, ইচ্ছা করে design করা (নিচের table দেখো)

দুটো জিনিস — chain কতটা লম্বা আর কতবার বারবার লেখা — সেটা urgency ঠিক করে। plot করো আর decision নিজেই হয়ে যায়:

চিত্র ৩: Chain triage — দৈর্ঘ্য আর পুনরাবৃত্তি মিলিয়ে ঠিক হয় কোনটা আগে fix করবে

Top-right — লম্বা আর বারবার লেখা — এখানেই Hide Delegate সবচেয়ে দ্রুত কাজে লাগে। Bottom-left হলো স্বাভাবিক collaboration; ছুঁলে শুধু কাজ বাড়বে।

কেন এটা সমস্যা

১. Caller পুরো রাস্তার সাথে coupled হয়। Client এখন Customer, Address, আর Country-এর existence, নাম, আর shape-এর উপর নির্ভর করছে। দুটো link-এর মাঝে নতুন class ঢোকাও, বা একটা getter rename করো — যে file গুলো রাস্তা বানান করেছে সব ভেঙে পড়বে, প্রায়ই অন্য team-এর file। একটা change, অনেক ভাঙা জায়গা: chain চুপচাপ Shotgun Surgery তৈরি করে।

২. Structure leak হয়। Chain কাজ করতে হলে প্রতিটা intermediate class-কে তার internal public getter হিসেবে expose করে রাখতে হবে। Object graph-এর private map হয়ে যায় সবার সম্পত্তি, চিরকালের জন্য জমাট।

৩. Intent লুকিয়ে যায়। Reader-কে মানসিকভাবে route হাঁটতে হয় বুঝতে যে লক্ষ্য ছিল শুধু "tax rate"। Code বলা উচিত সে কী চায়, কীভাবে সেখানে হামাগুড়ি দিয়ে যাবে সেটা না।

৪. Null handling বাড়ে। প্রতিটা hop null/missing হতে পারে। তাই caller-রা chain-কে check-এর pyramid দিয়ে মোড়ে, নয়তো একদিন production-এ classic NullReferenceException আসে train wreck-এর মাঝখান থেকে — আর stack trace-ও বলতে পারে না কোন bogey null ছিল।

এই chain-টা একটা object diagram হিসেবে দেখো — রুটির খোঁজকে:

চিত্র ৪: Message chain — তোমার code চারজন stranger-এর মধ্য দিয়ে একটা উত্তর খুঁজছে

সেই line-এর প্রতিটা box হলো একটা class যার উপর তোমার code এখন নির্ভর করছে। একটা boolean-এর জন্য পাঁচটা dependency। আর মনে রেখো: মাঝের যেকোনো box-এর shape পাল্টালে বামের লাল box-টাই ভাঙে।

কতটা ভাঙে? নির্ভর করে কতজন caller রাস্তা বানান করেছে। হিসাবটা কঠিন — chain যত লম্বা, caller তত বেশি জমে, আর একটা link পাল্টালে blast radius তত বড় হয়:

চিত্র ৫: Blast radius — একটা middle link পাল্টালে কতজন caller ভাঙে

College corner: coupling-এর ভাষায়, length n-এর chain caller-কে একটার বদলে nটা class-এর efferent coupling দেয়, আর প্রতিটা intermediate class পায় এমন caller-দের থেকে afferent coupling যাদের serve করার কথা সে ভাবেইনি। change propagation নিয়ে research দেখায় যে একটা change dependent file-এ ripple করার সম্ভাবনা প্রতিটা structural dependency বাড়লে বাড়ে — তাই চারটা class-এর spelled-out path মানে প্রতিটা model change-এ চারটা ভাঙার সুযোগ, আর সেটা path repeat করা প্রতিটা file-এ গুণ হয়। Chain একটা logical dependency ("আমার tax rate দরকার") কে structural dependency-র bundle-এ বদলে দেয় ("তোমার পুরো family tree আমি জানি")। এটাই পুরো smell-এর মূল কথা।

⚠️

Message chain-এর সবচেয়ে নিষ্ঠুর দিক: cost পড়ে ভুল মানুষের উপর। Chain লেখা সহজ — IDE প্রতিটা hop autocomplete করে দেয়, dot dot dot, দশ সেকেন্ড শেষ। দাম দিতে হয় মাস পরে — যে model পাল্টাতে গিয়ে দেখে চল্লিশটা file পুরনো রাস্তা বানান করে আছে। লিখতে সস্তা, রাখতে দামি।

Real-life code example

ধরো school app-এ report card পোস্ট করার জন্য delivery label print করতে হবে। দেখো renderer কীভাবে graph-এর মধ্য দিয়ে হামাগুড়ি দিচ্ছে:

class TaxTable { rateFor(state: string): number { return state === "MH" ? 0.18 : 0.12; } }
 
class Country {
  constructor(public name: string, public taxTable: TaxTable) {}
}
 
class Address {
  constructor(public city: string, public state: string, public country: Country) {}
}
 
class Customer {
  constructor(public name: string, public address: Address) {}
}
 
class Order {
  constructor(public customer: Customer, public amount: number) {}
}
 
// The smelly part — every method is a little train journey:
class LabelRenderer {
  shippingLine(order: Order): string {
    return `Ship to: ${order.customer.address.city}`;            // 3 hops
  }
 
  taxLine(order: Order): string {
    const rate = order.customer.address.country.taxTable        // 4 hops!
      .rateFor(order.customer.address.state);                   // and again!
    return `Tax: ${(order.amount * rate).toFixed(2)}`;
  }
}

LabelRenderer-এর ভেতরে কতটা knowledge আছে গুনো। এটা একটা printing class, তবু জানে: order-এ customer আছে; customer-এ address আছে; address-এ city, state, country আছে; country-তে tax table আছে; tax table state দিয়ে answer করে। অন্যদের internal-এর ছয়টা fact — দুটো line print করতে। এই class-টা আসলে কী নিয়ে ভরা দেখো:

চিত্র ৬: LabelRenderer-এর ভেতরে কী আছে — বেশিরভাগই অন্য class-এর private map, printing খুব কম

এবার disaster scenario। Model team সিদ্ধান্ত নিলো address-এ multiple shipping address support হবে: customer.addresses[] আর একটা primary() method। ছোট, sensible model change। Result: .address দিয়ে যাওয়া প্রতিটা chain প্রতিটা file-এ ভেঙে পড়বে। Model team-এর change ripple করবে rendering, billing, SMS, export-এ... কেউ address list নিয়ে care করে না। এটাই chain-এর bill। জামাল ঢাকায় চলে গেল, আর পাড়ার কেউ রুটির খোঁজ পাবে না।

একই wreck Python-এ, যেখানে dynamic typing মানে compile time-এ fail হয় না — রাত ১১টায় production-এ fail হয়:

# Smelly: a route map, written in three different files
def tax_line(order):
    rate = order.customer.address.country.tax_table.rate_for(
        order.customer.address.state
    )
    return f"Tax: {order.amount * rate:.2f}"
 
# Healthy: one friend, one question
def tax_line(order):
    return f"Tax: {order.tax():.2f}"

ধাপে ধাপে ঠিক করো

সমাধান হলো দিকনির্দেশনা বন্ধ করো, প্রশ্ন শুরু করো। মূল tool হলো Hide Delegate

ধাপ ১ — Chain repeat হলে আগে Extract Method দিয়ে এক জায়গায় আনো। রাস্তাটা ঠিক একটা জায়গায় লেখো:

class LabelRenderer {
  shippingLine(order: Order): string {
    return `Ship to: ${this.cityOf(order)}`;
  }
  private cityOf(order: Order): string {
    return order.customer.address.city;   // the only copy of the route
  }
}

এটাই অনেক ভালো: route একবারই আছে। কিন্তু renderer এখনো জানছে। এবার নিচে push করো।

ধাপ ২ — Hide Delegate apply করো, এক link করে। ভাবো: renderer-এর direct friend কে? Order। তাই Order-ই প্রশ্নের উত্তর দিক, নিজের internal লুকিয়ে রেখে:

class Customer {
  shippingCity(): string  { return this.address.city; }    // hop hidden inside Customer
  shippingState(): string { return this.address.state; }
  taxRate(): number {
    return this.address.country.taxTable.rateFor(this.address.state);
  }
}
 
class Order {
  shippingCity(): string { return this.customer.shippingCity(); }  // one hop, hidden
  tax(): number          { return this.amount * this.customer.taxRate(); }
}

প্রতিটা class শুধু নিজের পরের hop লুকাচ্ছে। Order জানে তার customer আছে — ঠিক আছে, সে direct friend। Customer জানে তার address আছে — তার direct friend। কেউ দুই ধাপ দূরে হাত বাড়াচ্ছে না।

ধাপ ৩ — Caller এখন এক friend-কে এক প্রশ্ন করে:

class LabelRenderer {
  shippingLine(order: Order): string {
    return `Ship to: ${order.shippingCity()}`;   // one friend, no strangers
  }
  taxLine(order: Order): string {
    return `Tax: ${order.tax().toFixed(2)}`;
  }
}

দুটো conversation পাশাপাশি তুলনা করো — chain বনাম প্রশ্ন:

চিত্র ৭: আগে তুমি পুরো পরিবারকে জিজ্ঞাসাবাদ করছিলে; এখন এক friend-কে জিজ্ঞেস করো, hop গুলো তাদের owner-এর ভেতরে লুকিয়ে যায়

Disaster scenario আবার চালাও: customer-এ multiple address আসলো। কী ভাঙবে? শুধু Customer.shippingCity() আর তার বন্ধুরা — Customer-এর ভেতরের method, পরিবর্তিত data-র ঠিক পাশেই। Renderer, billing, SMS, export — একটা character-ও বদলাবে না। Change হয় যেখানে ঘটে সেখানেই absorbed হয়।

ধাপ ৪ — সবচেয়ে deep logic-এর জন্য Move Method ভাবো। খেয়াল করো tax calculation আসলে data-র কাছেই থাকা উচিত: Order-এ tax() একটা Feature Envy-ও ঠিক করছে — renderer আগে অন্য object-এর data থেকে tax calculate করছিল। Chain আর envy প্রায়ই একসাথে আসে; behaviour-কে data-র পাশে নিয়ে গেলে দুটোই ঠিক হয়।

ধাপ ৫ — বেশি করার আগেই থামো। প্রতিটা delegate লুকালে middle class-এ একটা ছোট forwarding method যোগ হয়। পুরো project-এ সব hop লুকালে middle class গুলো forwarding method-এর দেয়াল হয়ে যায় — এটাই Middle Man smell, ঠিক উল্টো failure। Fowler এই trade-off নিয়ে সৎ: Hide Delegate আর Remove Middle Man হলো inverse refactoring, আর সঠিক জায়গাটা মাঝখানে। লম্বা, বারবার লেখা, বা অস্থির structure-এর chain-গুলো wrap করো। ছোট, local, stable দুই-hop chain একা থাকতে দাও।

Refactoring-টা state machine হিসেবে দেখো — নিচে loop আছে, কারণ এই balance project-এর জীবনে বারবার adjust হয়:

চিত্র ৮: Train wreck থেকে clean — আর নিচে Middle Man-এর গর্ত অপেক্ষা করছে বেশি লুকালে

আর final structure। প্রতিটা class একটা প্রশ্নের উত্তর দেয়, নিজের map private রাখে:

চিত্র ৯: Hide Delegate-এর পরে — প্রতিটা class শুধু নিজের পরের hop-এর উত্তর দেয়

C#-এ একই smell

Classic null-decorated train wreck, C#-এ:

// Smelly: the controller knows the whole object map
public class InvoiceController
{
    public string TaxLabel(Order order)
    {
        var rate = order.Customer.Address.Country.TaxTable
                        .RateFor(order.Customer.Address.State);
        return $"GST: {order.Amount * rate:F2}";
    }
}

(C#-এর ?. operator chain-কে run করা safe করে — order?.Customer?.Address?.City throw করবে না — কিন্তু খেয়াল করো: এটা design ভালো করে না। পুরো রাস্তার সাথে coupling থাকেই; null-conditional শুধু crash চুপ করায়।)

Hide Delegate-এর পরে:

public class Customer
{
    public decimal TaxRate() => _address.Country.TaxTable.RateFor(_address.State);
}
 
public class Order
{
    public decimal Tax() => Amount * _customer.TaxRate();
}
 
public class InvoiceController
{
    public string TaxLabel(Order order) => $"GST: {order.Tax():F2}";
}

এক friend, এক প্রশ্ন, এক জায়গায় পরিবর্তন।

Real project-এ এই smell কোথায় লুকায়

১. ORM entity navigation। Object-relational mapper graph effortlessly walkable করে দেয়: order.Customer.Address.Country.Name compile হয়, autocomplete হয়, আর সাথে তিনটা lazy-loading SQL query fire করতে পারে যা তুমি কখনো notice করোনি। Deep entity chain ORM code-এ double smell — design coupling আর লুকানো database trip (N+1 query problem)। Aggregate boundary আর intention-revealing query method হলো standard সমাধান।

২. Configuration আর settings tree। config.getServer().getSecurity().getTls().getCertPath()। প্রতিটা consumer পুরো config layout জানে, আর config file reorganise করলে codebase-এর অর্ধেক ভাঙে। প্রতিটা consumer-এর জন্য flat, typed options object বেশি শান্তির।

৩. Getter-only "struct" model। যখন class behaviour-এর (shippingCity()) বদলে structure expose করে (getAddress()), সে caller-কে navigate করতে invite করে। Chain হলো symptom; structure-shaped API হলো রোগ।

৪. UI code view model-এ ঢুকছে। screen.form().section(2).field("pin").value() — form সাজাও, প্রতিটা event handler-এর code ভেঙে পড়ে।

৫. Test setup chain। order.getCustomer().getAddress().setCity("Dhaka")-এর মতো data arrange করা test-রা প্রতিটা model change-এ একসাথে ভাঙে। Test-এ builder আর factory helper হলো Hide Delegate-এর cousin।

৬. Service locator। context.getRegistry().getService("billing").getClient() — infrastructure-এর মধ্য দিয়ে chain domain object-এর মতোই binding।

কখন ignore করা যায়

এই section এই smell-এর জন্য সবচেয়ে গুরুত্বপূর্ণ, কারণ একটা খুব common pattern দেখতে একই কিন্তু সম্পূর্ণ healthy।

Fluent interface message chain না। এই দুটো line দেখো:

// A: message chain (smell)
const rate = order.customer().address().country().taxRate();
 
// B: fluent builder (deliberate, healthy design)
const ticket = new TicketBuilder().forMovie("3 Idiots").seats(2).snack("popcorn").build();

dot-এর সংখ্যা একই। কিন্তু ভেতরে সম্পূর্ণ উল্টো:

  • A-তে প্রতিটা call আলাদা class-এর আলাদা object return করছে — তুমি stranger-দের graph-এ travel করছো, প্রতিটা ধাপে অন্যদের internal-এ আরও গভীরে।
  • B-তে প্রতিটা call একই builder object return করছে — তুমি এক friend-কে ধাপে ধাপে instruction দিচ্ছো। Chain কখনো immediate friend ছেড়ে যায় না, তাই Law of Demeter মোটেই violated না।

LINQ আর array pipeline-এও তাই: orders.Where(...).OrderBy(...).Take(10) intentionally একই ধরনের জিনিস (sequence) return করতে থাকে। Test question করো: "আমি একটা জিনিস configure/transform করছি, নাকি অনেক আলাদা জিনিসের ভেতর দিয়ে travel করছি?" এক জিনিস configure করা = fluent API, feature। Stranger-দের মধ্য দিয়ে travel = message chain, smell।

College corner: design theory-তে পার্থক্যটা পরিষ্কার। Fluent interface (term এসেছে Eric Evans আর Martin Fowler থেকে, ২০০৫) এমন API যার method receiver return করে — বা একই abstraction-এর আরেকটা value — ঠিক এজন্যই যে call গুলো compose হয়; dot গুলো তোমাকে একটাই published interface-এ couple করে। Message chain-এর dot তোমাকে concrete structure-এর একটা path-এ couple করে। Formally: fluent pipeline-এ প্রতিটা dot-এ static type একই (বা intentional supertype); message chain-এ type প্রতিটা dot-এ বদলায়, আর প্রতিটা নতুন type coupling graph-এ নতুন dependency edge। তাই stream().filter().map().collect() collection-এর যেকোনো internal refactoring survive করে, আর school.dept().head().phone() die করে যদি path-এর একটাও class বদলায়। সন্দেহ হলে প্রতিটা dot-এ type লিখে ফেলো — list যদি domain model-এর tour-এর মতো পড়ায়, সেটা chain।

পরিস্থিতিসিদ্ধান্তকারণ
Fluent builder: b.setX().setY().build()Smell নাপ্রতিটা call একই object return করে; এক friend, অনেক instruction
LINQ / stream pipeline: xs.filter(...).map(...)Smell নাপ্রতিটা ধাপ একই abstraction (sequence) return করে, chaining-এর জন্য design করা
দুই-hop chain, একবার লেখা, stable modelসহনীয়Wrap করার cost benefit ছাড়িয়ে যেতে পারে; note করো আর এগিয়ে যাও
Owning module-এর ভেতরে internal chainপ্রায়ই ঠিক আছেএকটা class তার নিজের composed part navigate করতে জানে legitimately
Domain object-এর মধ্য দিয়ে ৩+ hop, অনেক file-এ বারবারঠিক করোএকটা model change সব ভাঙবে — Hide Delegate worth it
প্রতি sprint-এ পরিবর্তনশীল model-এর উপর chainআগে ঠিক করোUnstable structure + spelled-out route = ক্রমাগত breakage
ℹ️

আর উল্টো দিকের গর্তটা মনে রেখো: সব delegate লুকালে middle class-এ forwarding method যোগ হয়, আর বেশি হলে Middle Man smell তৈরি হয়। Demeter তোমাকে delegation-এর দিকে ঠেলে; Middle Man সতর্ক করে যখন delegation-ই একটা class-এর একমাত্র কাজ হয়ে যায়। ভালো design দুটো গর্তের মাঝখানে হাঁটে — পরের post-এর পুরো বিষয়টাই এই balance।

কোন refactoring কাজে লাগে

Refactoringকখন ব্যবহার করবেকী করে
Hide DelegateChain-এর primary cureপ্রথম object-কে method দেয় যা deep value আনে; hop গুলো তাদের owner-এর ভেতরে লুকায়
Extract Methodএকই chain অনেক জায়গায় আছেRoute-কে একটা named method-এ আনে, তারপর নিচে push করে
Move MethodChained code আসলে chain-এর শেষের data দিয়ে কাজ করছেপুরো behaviour-কে data-র পাশে নিয়ে যায়; chain মিলিয়ে যায়
Remove Middle Manবেশি করে ফেলেছ, forwarding-only class তৈরি হয়েছেInverse correction — caller-দের এক ধাপ কাছে কথা বলতে দাও

Quick revision box

+=================================================================+
|                 MESSAGE CHAINS — QUICK REVISION                  |
+=================================================================+
| STORY    : You ask Amit, who asks Raju, who asks uncle, who      |
|            asks the shopkeeper — 20 minutes to learn if          |
|            bread is available.                                   |
|                                                                  |
| SMELL    : a.getB().getC().getD() — a train wreck of hops        |
|            through DIFFERENT objects to dig out one value.       |
|                                                                  |
| LAW      : Law of Demeter — "only talk to your immediate         |
|            friends." Friends: this, own fields, parameters,      |
|            objects you create. A friend's friend = stranger.     |
|                                                                  |
| COSTS    : coupled to the whole path, leaked structure,          |
|            hidden intent, null-check pyramids                    |
|                                                                  |
| CURE     : Extract Method (gather the route)                     |
|            -> Hide Delegate (each class hides its own hop)       |
|            -> Move Method (put behaviour beside its data)        |
|                                                                  |
| NOT A    : fluent builders & LINQ — every call returns the       |
| CHAIN    : SAME object/abstraction. Configuring one friend       |
|            is fine; touring strangers is the smell.              |
|                                                                  |
| BALANCE  : hide too many delegates and you create the            |
|            MIDDLE MAN smell. Wrap long/repeated/unstable         |
|            chains; leave short stable ones alone.                |
+=================================================================+

Practice exercise

ধরো একটা hospital app-এ বিশাল train wreck:

class Pharmacy   { stockOf(med: string): number { return 42; } }
class Ward       { constructor(public pharmacy: Pharmacy, public floor: number) {} }
class Department { constructor(public ward: Ward, public head: string) {} }
class Hospital   { constructor(public department: Department) {} }
 
class ReceptionDesk {
  canDispense(h: Hospital, med: string): boolean {
    return h.department.ward.pharmacy.stockOf(med) > 0;          // chain 1
  }
  directions(h: Hospital): string {
    return `Go to floor ${h.department.ward.floor}`;              // chain 2
  }
  noticeFor(h: Hospital): string {
    return `Dept head: ${h.department.head}`;                     // chain 3
  }
}

তোমার কাজ:

  1. Wreck-গুলো measure করো। তিনটা method-এর প্রতিটায় hop গুনো আর list করো ReceptionDesk কোন কোন class-এর সাথে coupled। কোন method সবচেয়ে খারাপ train?
  2. Law of Demeter apply করো। canDispense-এ College corner-এর formal statement ব্যবহার করে method-এর friend-দের নাম দাও। ঠিক কোন dot-এ code stranger-এর সাথে কথা বলা শুরু করে?
  3. Hide Delegate দিয়ে refactor করো। Hospital-এ hasStock(med) যোগ করো, level by level নিচে delegate করে (HospitalDepartmentWardPharmacy), যাতে ReceptionDesk শুধু h.hasStock(med) call করে। যেতে যেতে প্রতিটা field private করো।
  4. Trade-off feel করো। ধাপ ৩-এর পরে Department আর Ward দেখো। তাদের কতটা method এখন pure forwarding? এক sentence-এ লেখো তারা কি Middle Man-এর দিকে যাচ্ছে, আর আরও তিনটা chain hide করার দাবি এলে কী করবে।
  5. Impostor sort করো। এগুলোর মধ্যে কোনগুলো real message chain, কোনগুলো healthy fluent design? প্রতিটা dot-এ type লিখে এক line verdict দাও:
    • patient.history().lastVisit().doctor().phone()
    • new Query().from("patients").where("age > 60").limit(10)
    • report.sections.filter(s => s.urgent).map(s => s.title)
    • app.config().database().pool().maxSize()
  6. Bonus। Hospital-এ multiple department যোগ হলো (h.departments[])। তোমার refactored code-এ কোন class change absorb করবে? Original code-এ কতটা file বদলাতে হতো?

প্রশ্ন ৫-এর উত্তর যদি দাও "একই object/abstraction return করা = fluent; আলাদা stranger = chain" — তাহলে Law of Demeter এখন তোমার বন্ধু। একদম পারফেক্ট!

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

Message Chains code smell আসলে কী?
এটা হলো order.getCustomer().getAddress().getCity() এই ধরনের লম্বা call-এর chain — code একটার পর একটা object ধরে hop করতে করতে একটা মাত্র value বের করে। caller পুরো রাস্তার সাথে coupled হয়ে যায়, শুধু destination-এর সাথে না। মাঝের যেকোনো একটা link পাল্টালে সব ভেঙে পড়ে। programmers এটাকে 'train wreck'-ও বলে — এক লাইনে এতগুলো dot দেখলে train-এর bogey মনে হয়।
Message chains কীভাবে Law of Demeter ভাঙে?
Law of Demeter বলে একটা method শুধু তার immediate friend-দের সাথে কথা বলবে: নিজে, নিজের field, নিজের parameter, আর নিজে যেসব object বানায়। কিন্তু chain-এ তুমি একটা method-এর result-এর উপর আবার method call করছ, তারপর সেই result-এর উপরেও — প্রতিটা extra dot মানে একজন stranger-এর সাথে কথা বলা। সমাধান হলো তোমার direct friend-কে একটা question করো, সে বাকি navigation নিজেই করুক।
Fluent builder আর LINQ query কি message chain?
না! builder.setName().setAge().build() বা orders.Where(...).OrderBy(...) এই ধরনের fluent interface-এ প্রতিটা call একই object বা একই ধরনের object return করে — ইচ্ছা করেই। তুমি একটাই জিনিস configure করছো, অনেক stranger-এর বাড়িতে ঘুরছো না। একটা abstraction-এর উপর method chaining করা feature; অনেক object-এর ভেতর দিয়ে navigate করা হলো smell।
Message Chains ঠিক করার refactoring কোনটা?
Hide Delegate হলো মূল সমাধান — প্রথম object-কে একটা method দাও যেটা deep value এনে দেবে, ভেতরের hop গুলো লুকিয়ে রাখবে। Extract Method একটা repeated chain-কে আগে একজায়গায় এনে দেয়, আর Move Method পুরো calculation-টা data-র পাশে নিয়ে যায় যাতে chain-ই না থাকে।
Message chains ঠিক করতে গিয়ে কি নতুন smell তৈরি হতে পারে?
হ্যাঁ। প্রতিটা hidden hop মাঝের class-এ একটা forwarding method যোগ করে। সব জায়গায় সব delegate লুকালে সেই class গুলো Middle Man হয়ে যায় — এমন class যেগুলো শুধু forward করে। যেসব chain লম্বা, বারবার লেখা, বা পরিবর্তনশীল structure-এর উপর তৈরি — সেগুলো wrap করো। ছোট, stable, local chain গুলো একা থাকতে দাও।

আরো দেখো

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

Middle Man: যে helper শুধু তোমার message পৌঁছে দেয়, নিজে কিছু করে না

Middle Man code smell টা বোঝো একটা school-এর সেই পিয়নের গল্প দিয়ে — যে শুধু চিরকুট বহন করে, নিজে কিছু যোগ করে না। যখন একটা class শুধু সব call forward করে, সেটা সরিয়ে দাও। কিন্তু Proxy, Facade, আর Adapter কেন জেনেশুনে middle man হয় — সেটাও জানো।

আরও পড়ুন

Feature Envy: যে method সারাদিন অন্যের class-এ বসে থাকে

Feature Envy code smell শেখো একটা সহজ স্কুলের গল্পের মাধ্যমে। যখন একটা method নিজের class-এর চেয়ে অন্য class-এর data বেশি ব্যবহার করে, তখন সেটা আসলে ওই অন্য class-এই থাকার কথা। সারানোর উপায় হলো Move Method।

আরও পড়ুন

Inappropriate Intimacy: দুটো class যারা একে অপরের রান্নাঘরে ঢুকে পড়ে

দুই প্রতিবেশীর গল্প দিয়ে Inappropriate Intimacy বোঝো — যারা একে অপরের রান্নাঘর সাজিয়ে দেয়। দুটো class যখন একে অপরের private অংশে হাত দেয়, তখন কেউ একা কিছু বদলাতে পারে না। Law of Demeter আর privacy ফিরিয়ে আনার refactoring শেখো।

আরও পড়ুন

Hide Delegate: মনিটরকে জিজ্ঞেস করো, মনিটর নিজেই দৌড়াবে

Hide Delegate রিফ্যাক্টরিং শেখো একটা মজার গল্পের মাধ্যমে। employee.department.manager-এর মতো chain লেখা বন্ধ করো — প্রথম object-কে একটা সহজ method দাও আর ভেতরের জার্নি লুকিয়ে রাখো। TypeScript আর C#-এ ধাপে ধাপে উদাহরণসহ।

আরও পড়ুন