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

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

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

23 মিনিট আপডেট: June 11, 2026beginner
code-smellscouplersinappropriate-intimacylaw-of-demeterencapsulationcouplingtypescriptcsharp

দুই প্রতিবেশী যারা সব কিছু ভাগ করে নিত — এমনকি রান্নাঘরও

ধরো মীরপুরের একটা কলোনিতে রহেলা বেগম আর নাসরিন বেগম পাশাপাশি থাকেন, বাড়ি নম্বর ১৪ আর ১৫। শুরুটা ভালোই ছিল। এক কাপ চিনি, এক বাটি ডাল, ঈদে মিষ্টি বিনিময় — দিব্যি চলছিল।

কিন্তু আস্তে আস্তে বিষয়টা অন্যদিকে গেল। রহেলা বেগম দরজা না ঠকিয়েই ঢুকতে শুরু করলেন। সোজা নাসরিনের রান্নাঘরে গিয়ে মশলার আলমারি খোলেন, যা দরকার নেন। নাসরিনও পাশের বাড়িতে একই। তারপর আরো একধাপ এগিয়ে গেল। এক মঙ্গলবার রহেলা বেগম নাসরিনের পুরো তাক সাজিয়ে দিলেন — "হলুদটা বাম দিকে রাখলে ভালো না? আমি যখন আসি তখন সুবিধা হয়।" নাসরিনও কম যাননি, রহেলার সব কৌটায় নিজের হাতের লেখায় লেবেল লাগিয়ে দিলেন। শীতের মধ্যে দুজনেই নিজের চাবির রিং-এ অপরজনের বাড়ির একটা করে চাবি রাখতে শুরু করলেন।

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

তারপর এলো মার্চ মাস। নাসরিনের ছেলে ভালো চাকরি পেল। তিনি ঠিক করলেন রান্নাঘর রিনোভেট করবেন। নতুন আলমারি, modular shelf, সব কিছু নতুন জায়গায়। কাঠমিস্ত্রি এলো, মাপ নিল, দাম বলল। আর তখনই নাসরিন থেমে গেলেন।

রিনোভেট করা সম্ভব না। রহেলার রোজকার রান্না পুরনো সাজানোর উপর নির্ভর করে। হলুদের কৌটা বাম shelf থেকে সরালে রহেলার দুপুরের রান্না ভেস্তে যাবে — আর রহেলা বুঝতেই পারবেন না কেন, যতক্ষণ না পাল্টানো আলমারির সামনে দাঁড়াবেন আর বাড়িতে গরম তাওয়া অপেক্ষা করছে। নির্ভরতাটা উল্টো দিকেও আছে: রহেলা বেগম ঘর তালা দিয়ে এক মাসের জন্য গ্রামে যেতে পারবেন না, কারণ নাসরিনের রাতের রান্নার জন্য তার রান্নাঘর দরকার।

নাসরিনের স্বামী রাতের খাওয়ায় বললেন: "কাগজে কলমে দুটো পরিবার। কিন্তু বাস্তবে? কেউই একা কিছু বদলাতে পারি না। দুটো রান্নাঘর, একটা জট পাকানো সংসার। কোনো privacy নেই, পুরো নির্ভরতা।"

চিত্র ১: নাসরিনের রিনোভেশনের পরিকল্পনা — আর যেখানে সেটা শেষ হয়

কোডে দুটো class যখন বেশি ঘনিষ্ঠ হয়ে যায় তখন ঠিক এটাই হয়। Class A class B-এর field পড়ে আর লেখে। B পাল্টা A-এর ভেতরের অংশে হাত দেয়। প্রত্যেকে অপরজনের একটা reference রাখে — একটা স্পেয়ার চাবির মতো। তারপর আর একটাকে ছাড়া অন্যটা test বা পরিবর্তন করা যায় না। এই smell-টার নাম Inappropriate Intimacy

এই smell-টা আসলে কী?

Inappropriate Intimacy হলো সেই code smell যেখানে দুটো class একে অপরের private অংশ সম্পর্কে অনেক বেশি জেনে ফেলে। ছোট, ভদ্র interface-এর মাধ্যমে কথা বলার বদলে — যেমন দরজা ঠকিয়ে চাওয়া — তারা সরাসরি একে অপরের field, internal method, আর private state-এ ঢুকে পড়ে।

Martin Fowler Refactoring-এ এটাকে coupling smell-এর মধ্যে রেখেছেন। Refactoring Guru-ও একইভাবে বলে — একটা class অন্য class-এর internal field আর method ব্যবহার করে।

এটা তার ছোট ভাই Feature Envy-এর সাথে তুলনা করো:

  • Feature Envy একদিকে: A-এর একটা method বারবার B-এর data পড়ে। একজন নাক-গলানো প্রতিবেশী।
  • Inappropriate Intimacy পারস্পরিক আর গভীর: A আর B একে অপরকে পড়ে এবং লেখে, একে অপরের reference রাখে, একে অপরের ভেতরের shape-এর উপর নির্ভর করে। দুটো জট পাকানো সংসার।
💡

একটা দ্রুত পরীক্ষা করো: একটা class-এর জন্য অপরটার সহজ একটা fake দিয়ে unit test লেখার চেষ্টা করো। যদি না পারো — যদি test করতে অপর class-টার সব internal সহ পুরোটা দরকার হয় — তাহলে দুটো class ঘনিষ্ঠ। সুস্থ collaborator-দের একটা stub দিয়ে test করা যায়; ঘনিষ্ঠরা শুধু জুটি বেঁধে আসে।

কলেজ কর্নার: ক্লাসিক্যাল structured-design theory (Stevens, Myers, আর Constantine, ১৯৭৪) coupling-কে সেরা থেকে সবচেয়ে খারাপ পর্যন্ত rank করেছে: data coupling (সহজ value পাঠানো), stamp coupling (structure পাঠানো), control coupling (callee-কে নির্দেশ করে এমন flag পাঠানো), common coupling (global data share করা), আর একদম নিচে content coupling — একটা module সরাসরি অন্য module-এর internal data পড়ে বা বদলায়। Inappropriate Intimacy হলো object-oriented পোশাক পরা content coupling। যখন Transaction নিজের হাতে account.balance লেখে, সেটা পঞ্চাশ বছর পুরনো scale-এ সবচেয়ে খারাপ rank-এর textbook content coupling। Ranking টিকে আছে কারণ যুক্তি টিকে আছে: coupling rank যত কম, পরিবর্তন ছড়ানোর পৃষ্ঠতল তত ছোট।

Law of Demeter: "শুধু তোমার কাছের বন্ধুদের সাথে কথা বলো"

কেন এটা ভুল সেটা বোঝার জন্য একটা বিখ্যাত নিয়ম জানো। ১৯৮৭ সালে Demeter project-এর গবেষকরা (Karl Lieberherr আর Northeastern University-তে তার সহকর্মীরা) object-oriented program-এর জন্য একটা style rule লিখলেন, তাদের OOPSLA 1988 paper-এ প্রকাশিত হলো। এটা Law of Demeter নামে পরিচিত হলো, সাথে একটা সহজ মূলনীতি:

"শুধুমাত্র তোমার সরাসরি বন্ধুদের সাথে কথা বলো।"

সহজ কথায়: একটা method কথা বলতে পারে —

  1. নিজের সাথে (তার নিজের field আর method),
  2. তার parameter-এর সাথে (যা তার কাছে পাঠানো হয়েছে),
  3. যেসব object সে নিজে তৈরি করে,
  4. তার সরাসরি component object-এর সাথে (তার নিজের field)।

এরা তার কাছের বন্ধু। এটা যা করতে পারবে না সেটা হলো বন্ধুর ভেতর দিয়ে বন্ধুর ভেতরের জিনিসে পৌঁছানো — বন্ধুর বন্ধু হলো অপরিচিত। প্রতিবেশীর কাছে চিনি চাইতে পারো। তার রান্নাঘরে হেঁটে ঢুকে, আলমারি খুলে, কৌটা সাজাতে পারো না।

কলেজ কর্নার: আনুষ্ঠানিক বিবৃতিটা exam আর interview-এর জন্য মনে রাখার মতো। সকল class C আর C-এর সাথে যুক্ত সকল method M-এর জন্য, M যে সকল object-এ message পাঠায় সেগুলো অবশ্যই এর instance হতে হবে: (১) M-এর argument object-এর class-এর, C নিজে সহ; (২) C-এর instance variable-এর class-এর; (৩) M-এর তৈরি করা object-এর; বা (৪) global object-এর। এই আইনকে Principle of Least Knowledge-ও বলা হয়, আর এর দুটো variant আছে: object form (উপরে বলা, runtime object সম্পর্কে) আর class form (declared type সম্পর্কে)। linter-রা যে practical compile-time approximation ব্যবহার করে: "navigation-এ একের বেশি dot নয়" — তবে এই slogan আসল আইনের চেয়ে মোটা। this.a.b একটা violation কিন্তু list.map(f).filter(g) না, কারণ আইনটা কার internal তুমি traverse করছো সেটা নিয়ে, dot গণনা নিয়ে না।

Inappropriate Intimacy হলো এই আইনের সরাসরি লঙ্ঘন, একসাথে দুই দিক থেকে। প্রতিটা class অন্যটার private structure-এ navigate করে — যা দরকার সেটা চাওয়ার বদলে। (লঙ্ঘনের এককমুখী, chained সংস্করণ হলো Message Chains smell — এরপর সেটা পড়ো; সেখানেও এই আইনটা আসে।)

চিত্র ২: Law of Demeter একটা memory map হিসেবে — তোমার বন্ধুরা আর বাকি সবাই

কীভাবে চিনবে

ধরো দুটো class সবসময় একসাথে দেখা দেয়। এই checklist-টা তাদের উপর চালাও:

  • A সরাসরি B-এর field পড়ে বা লেখে, আর B-ও A-এর সাথে একই কাজ করে।
  • দ্বিমুখী reference: A একটা B রাখে, আর B তার A-এর back-reference রাখে — দুটোই স্বাধীনভাবে চলাফেরা করে।
  • একটা class internal / friend / package-private member উন্মুক্ত করে শুধুমাত্র partner সেগুলোতে হাত দেওয়ার জন্য।
  • একটা class বদলালে অন্যটাতেও বদলাতে হয়। তাদের commit সবসময় জুটিতে আসে।
  • একটা subclass parent-এর protected internal-এর উপর এত নির্ভর করে যে inheritance একটা চুক্তি না হয়ে পিছনের দরজা হয়ে গেছে।
  • অন্য class-টা পুরোপুরি তৈরি না করে একটা class unit-test করা যায় না।

দেখো কতটা ঘনিষ্ঠতা অনেক বেশি সেটা বিচার করার জন্য একটা table:

দুটো class-এর মধ্যে আচরণরায়
A B-এর একটা public method call করে আর উত্তর ব্যবহার করেঠিক আছে — এটা শুধু কথা বলা
A মাঝে মাঝে B-এর একটা getter পড়েঠিক আছে — বন্ধুরা একটু share করে
A জিনিস হিসাব করতে B-এর অনেক field পড়েFeature Envy — একদিকের, Move Method দিয়ে ঠিক করো
A সরাসরি B-এর field লেখেসতর্কের ঘণ্টা — B আর তার নিজের state নিয়ন্ত্রণ করে না
A B-এর field লেখে আর B A-এর field লেখেInappropriate Intimacy — পুরো smell
A আর B একে অপরের reference রাখে আর একে অপরের collection বদলায়মারাত্মক ঘনিষ্ঠতা — একটা বদলাও, দুটো ভাঙো
Subclass স্বাধীনভাবে parent-এর protected state-এ খোঁজাখুঁজি করেInheritance-এর মাধ্যমে ঘনিষ্ঠতা — "is-a" পিছনের দরজা হয়ে গেছে

একই table একটা chart হিসেবে দেখো। দুটো প্রশ্ন সব কিছু ঠিক করে দেয়: access কি লেখে নাকি শুধু পড়ে, আর এটা কি দুই দিক থেকে যায় নাকি একদিক থেকে?

চিত্র ৩: কতটা ঘনিষ্ঠতা অনেক বেশি — লেখা আর দুই দিক মিলিয়ে বিপদের কোণ

নিচে-বামে হলো স্বাভাবিক জীবন: ভদ্র, বেশিরভাগ-পড়া, একদিকের কথোপকথন। ডানে গেলে (write আসে) তুমি তোমার নিজের state-এর নিয়ন্ত্রণ হারাও। উপরে গেলে (দুই দিক) তুমি একা পরিবর্তন করার ক্ষমতা হারাও। উপরে-ডানে কোণটা হলো দুটো রান্নাঘর।

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

১. পরিবর্তনের একক দ্বিগুণ হয়। একটা class এমন একটা একক হওয়ার কথা যেটা একা বদলানো যায়। দুটো class internal share করলে প্রতিটা পরিবর্তন ছড়িয়ে পড়ে। একটা file ঠিক করতে এসে দুটো সম্পাদনা করে গেলে, দুটো test করলে, আর প্রার্থনা করলে।

২. Encapsulation মরে যায়। যেসব field private হওয়ার কথা সেগুলো partner-এর জন্য উন্মুক্ত। একবার উন্মুক্ত হলে class-টা কখনো তার internal representation বদলাতে পারে না। নাসরিন তার রান্নাঘর কখনো রিনোভেট করতে পারবেন না।

৩. Reuse আটকে যায়। একটা নতুন reporting module-এ Transaction ব্যবহার করতে চাও? এটা BankAccount-এর সাথে চেইনে আসে, BankAccount আসে অর্ধেক ব্যাংকের সাথে। তুমি একটা class চেয়েছিলে, পেলে পুরো বিয়ের মিছিল।

৪. Testing কষ্টের হয়। কোনো class-ই অপরটার stub নেয় না, কারণ প্রতিটা partner-এর concrete internal-এর উপর নির্ভর করে, interface-এর উপর নয়। Test-গুলো বড়, ধীর, আর ভঙ্গুর হয়।

৫. Bug shared state-এ লুকিয়ে থাকে। দুটো class একই field বদলালে "কে এই value বদলাল?" এর দুটো উত্তর আছে। Debug মানে একসাথে দুটো class পড়া, মাথায় দুটো ধরে রাখা।

পঞ্চম কারণটা একটু ভাবো। আমরা যে নোংরা code পড়তে যাচ্ছি তা নিয়ে একটা সহজ প্রশ্ন করো: account-এর balance কে লেখে?

চিত্র ৪: account balance কে লেখে? ঘনিষ্ঠ code-এ উত্তরের অনেক ভাগ থাকে

সুস্থ code-এ এই pie-তে একটাই ভাগ থাকে। প্রতিটা অতিরিক্ত ভাগ রাত ২টায় balance ভুল হলে আরেকটা সন্দেহভাজন — আর একটা field বোঝার জন্য আরো একটা file পড়তে হবে।

আর এই খরচটা বাড়তেই থাকে। দেখো সম্পর্ক গভীর হওয়ার সাথে সাথে একটা ছোট পরিবর্তনে কতটা file ছুঁতে হয়:

চিত্র ৫: ঘনিষ্ঠতা বাড়ার সাথে একটা ছোট পরিবর্তনে ছোঁয়া file-এর সংখ্যা
চিত্র ৬: দুটো ঘনিষ্ঠ class — তীর দুই দিক থেকে বেড়া পেরিয়ে private এলাকায় যায়

তীরের label-গুলো দেখো। তীরের সংখ্যা সবচেয়ে বেশি ভয় দেখায় না — ভয় দেখায় writes শব্দটা। প্রতিবেশীর নামফলক পড়া একটা জিনিস; তার দেয়াল রঙ করা আরেকটা জিনিস।

⚠️

দ্বিমুখী association এই smell-এর সবচেয়ে সাধারণ প্রবেশদ্বার। যেই মুহূর্তে Order তার Customer-কে চেনে আর Customer তার Orders-এর তালিকা রাখে, উভয় পক্ষই সরাসরি অপরজনের collection বদলাতে প্রলুব্ধ হয়। যদি শুধু একটা দিক দরকার হয়, শুধু একটা রাখো। তুমি যে reference রাখো না সেটা একটা প্রলোভন যার মুখোমুখি হতে হয় না।

বাস্তব code-এর উদাহরণ

দুই প্রতিবেশীকে code হিসেবে লিখি। একটা bank account আর তার transaction, ঠিক দুই রান্নাঘরের মতো জট পাকানো:

class BankAccount {
  transactions: Transaction[] = [];   // public — Transaction reaches in
  balance = 0;                         // public — Transaction reaches in
 
  apply(t: Transaction): void {
    this.balance += t.amount;   // reads Transaction's field
    t.account = this;           // WRITES Transaction's field
    t.posted = true;            // WRITES Transaction's field
    this.transactions.push(t);
  }
}
 
class Transaction {
  amount = 0;
  account: BankAccount | null = null;  // spare key to the neighbour's house
  posted = false;
 
  reverse(): void {
    if (!this.account) return;
    this.account.balance -= this.amount;              // WRITES account's field
    const i = this.account.transactions.indexOf(this);
    this.account.transactions.splice(i, 1);           // mutates account's list!
  }
}

কলোনির গল্পের মতো করে পড়ো। BankAccount.apply Transaction-এর ঘরে ঢুকে তার posted flag উল্টায় আর একটা back-reference বসিয়ে দেয় — রহেলা বেগম কৌটায় লেবেল লাগাচ্ছেন। Transaction.reverse account-এর ঘরে ঢুকে নিজের হাতে balance বদলায় আর account-এর নিজের তালিকা থেকে নিজেকে সরিয়ে দেয় — নাসরিন বেগম shelf সাজাচ্ছেন। প্রতিটা class অপরজনের রান্নাঘরে রান্না করছে।

কথোপকথন হিসেবে বললে, নোংরা code-টা এরকম শোনায় — একটু ভাবো, এতে একটাও প্রশ্ন নেই, শুধু অনুপ্রবেশ:

চিত্র ৭: নোংরা কথোপকথন — কোনো প্রশ্ন নেই, শুধু একে অপরের রান্নাঘরে হাত

বাস্তবে কী কী সমস্যা হয়?

  • ধরো account-এর balance বদলালে সুদ পুনরায় হিসাব করা উচিত। সেই নিয়মটা কোথায় রাখবে? apply balance বদলায়... কিন্তু reverse-ও বদলায়, অন্য class থেকে। নিয়মটার কোনো একক ঘর নেই।
  • ধরো balance কখনো শূন্যের নিচে যাওয়া উচিত না। কে সেটা নিশ্চিত করবে? BankAccount পারবে না, কারণ Transaction পিছন থেকে balance সম্পাদনা করে।
  • Transaction.reverse একা test করার চেষ্টা করো। পারবে না — এর জন্য আসল তালিকাসহ আসল BankAccount দরকার।

একই জট Python-এ লেখা, যাতে বুঝতে পারো smell-টার কোনো পছন্দের language নেই:

# Smelly: each class mutates the other's state directly
class BankAccount:
    def apply(self, t):
        self.balance += t.amount
        t.account = self          # writes into Transaction
        t.posted = True           # writes into Transaction
        self.transactions.append(t)
 
class Transaction:
    def reverse(self):
        self.account.balance -= self.amount        # writes into account
        self.account.transactions.remove(self)     # mutates its list

Python-এ private keyword-ও নেই এটা থামাতে — convention অনুযায়ী underscore prefix (_balance) বলে "ছুঁয়ো না", আর দলের শৃঙ্খলাই রান্নাঘরের দরজার একমাত্র তালা। তাই এই smell-টা মন দিয়ে শেখার দরকার।

ধাপে ধাপে পরিষ্কার করা

প্রতিকারের মূলকথা: প্রতিটা class-কে একটা স্পষ্ট দায়িত্ব দাও, আর সব কথোপকথন ছোট, ভদ্র interface-এর মাধ্যমে করাও। কয়েকটা refactoring একসাথে ব্যবহার করব।

ধাপ ১ — কে কী ধারণ করে সেটা ঠিক করো। Balance আর transaction history হলো account-এর সম্পত্তি। শুধু account-এরই সেগুলো বদলানো উচিত। Amount হলো transaction-এর সম্পত্তি। এই সিদ্ধান্তটাই অর্ধেক সমস্যা সমাধান করে।

ধাপ ২ — Move Method: balance-পরিবর্তনের logic ঘরে আনো। reverse()-এর body account-এর state বদলায়, তাই সেই behavior BankAccount-এ থাকা উচিত। সেটা সেখানে সরিয়ে একটা সৎ নাম দাও:

class BankAccount {
  private transactions: Transaction[] = [];  // private again!
  private balance = 0;                        // private again!
 
  post(t: Transaction): void {
    this.balance += t.amount();
    this.transactions.push(t);
  }
 
  unpost(t: Transaction): void {
    this.balance -= t.amount();
    const i = this.transactions.indexOf(t);
    if (i >= 0) this.transactions.splice(i, 1);
  }
 
  currentBalance(): number {
    return this.balance;   // a value handed over, not a field exposed
  }
}

ধাপ ৩ — Back-reference কাটো (Change Bidirectional Association to Unidirectional)। Transaction-এর কি সত্যিই তার account জানার দরকার আছে? ধাপ ২-এর পর — না। যে কেউ transaction reverse করতে চায় সে account-কে বলে: account.unpost(t)। স্পেয়ার চাবি ফিরিয়ে দেওয়া হলো:

class Transaction {
  constructor(private readonly _amount: number) {}
 
  amount(): number {
    return this._amount;   // exposes a value, not its guts
  }
}

ধাপ ৪ — Field-গুলো private করো আর compiler-কে সাহায্য করতে দাও। balance আর transactions private করো। Compiler যে লাল error দেখাবে প্রতিটা এক জায়গা যেখানে অনুপ্রবেশ হচ্ছিল। প্রতিটা post, unpost, বা currentBalance-এর মাধ্যমে route করে ঠিক করো।

ধাপ ৫ — যেখানে এখনো reaching বাকি, Hide Delegate ব্যবহার করো। যদি কোনো caller transaction.account.balance করছিল, তাকে জিজ্ঞেস করার একটা সঠিক উপায় দাও। Navigation একটা method-এর পিছনে লুকানোটাই হলো Law of Demeter মানার উপায়। (সাবধান — এটা অতিরিক্ত করলে Middle Man তৈরি হবে। ভারসাম্য, সবসময় ভারসাম্য।)

ধাপ ৬ — যদি shared কোনো concept দেখা দেয়, Extract Class ব্যবহার করো। কখনো কখনো দুটো class ঘনিষ্ঠ কারণ ভেতরে একটা তৃতীয় class লুকিয়ে আছে — এমন কোনো concept যার অর্ধেক দুটো class-ই সামলাচ্ছে। উদাহরণস্বরূপ, account আর transaction উভয়ই currency-conversion data নিয়ে কাজ করলে, একটা Money বা CurrencyConverter class বের করো। দুই জট পাকানো প্রতিবেশীর প্রায়ই শুধু একটা সঠিক shared community hall দরকার।

ধাপ ৭ — Parent-child ঘনিষ্ঠতার জন্য, Replace Inheritance with Delegation যখন একটা subclass parent-এর protected field-এ খোঁজাখুঁজি করে, "is-a"-কে "has-a"-তে বদলাও। এখন সম্পর্কটা একটা defined interface-এর মাধ্যমে যায়, বাকি সবার মতো।

জট থেকে পরিষ্কার পর্যন্ত পুরো যাত্রাটা state হিসেবে:

চিত্র ৮: জট ছাড়ানোর প্রক্রিয়া, state অনুযায়ী — প্রতিটা transition একটা refactoring সিদ্ধান্ত

আর চূড়ান্ত structure class diagram হিসেবে — এখানের ছোট, পরিষ্কার surface-টা চিত্র ৬-এর খোলা রান্নাঘরের সাথে তুলনা করো:

চিত্র ৯: পরিষ্কার করার পরে — একদিকের association, private field, দুটো public প্রশ্ন
চিত্র ১০: পরিষ্কার করার পরের flow — ভদ্র প্রশ্ন আর অনুরোধ, কোনো অনুপ্রবেশ নেই

চিত্র ৬-এর সাথে তুলনা করো। প্রতিটা তীর এখন এক দিকে যায়, আর প্রতিটা তীর একটা প্রশ্ন ("তোমার amount কত?") বা একটা অনুরোধ ("অনুগ্রহ করে এটা post করো") বহন করে। কোনো তীর অন্য class-এর private field-এ লেখে না। নাসরিন অবশেষে রিনোভেট করতে পারবেন: যতদিন amount() সঠিকভাবে উত্তর দেয়, Transaction তার ভেতর যেকোনোভাবে বদলাতে পারে — আর BankAccount-ও পারে।

C#-এ একই smell

এখানে C#-এ ঘনিষ্ঠতার pattern দেওয়া হলো। একটা Course আর Student জুটি — তারা একে অপরকে বদলায়:

// Smelly: both classes reach into each other
public class Course
{
    public List<Student> Enrolled = new();
    public int Capacity = 30;
 
    public void Add(Student s)
    {
        Enrolled.Add(s);
        s.Courses.Add(this);      // writes Student's collection
        s.CreditsTaken += 4;      // writes Student's field
    }
}
 
public class Student
{
    public List<Course> Courses = new();
    public int CreditsTaken;
 
    public void Drop(Course c)
    {
        Courses.Remove(c);
        c.Enrolled.Remove(this);  // mutates Course's collection
    }
}

পরিষ্কার করা: enrollment সম্পর্কের একজন মালিক বেছে নাও (ধরো Course), collection-গুলো private করো, আর method-এর মাধ্যমে কথা বলো:

public class Course
{
    private readonly List<Student> _enrolled = new();
    private readonly int _capacity = 30;
 
    public bool Enroll(Student s)
    {
        if (_enrolled.Count >= _capacity) return false;
        _enrolled.Add(s);
        s.RecordCredits(4);          // a request, not a trespass
        return true;
    }
 
    public void Withdraw(Student s)
    {
        if (_enrolled.Remove(s)) s.RecordCredits(-4);
    }
}
 
public class Student
{
    private int _creditsTaken;
    public void RecordCredits(int delta) => _creditsTaken += delta;
}

এখন Student তার নিজের credit পাহারা দেয় — এক জায়গায় "কখনো ২৪-এর উপরে না" এই নিয়ম যোগ করতে পারে। C# আরো internal access দেয়: যদি দুটো class সত্যিই একটা module তৈরি করে, তুমি assembly-এর ভেতরে নিয়ন্ত্রিত ঘনিষ্ঠতা রাখতে পারো বাইরের জগতের কাছে private থেকে। Module-এর ভেতরে ঘোষিত, নিয়ন্ত্রিত ঘনিষ্ঠতা হলো design; module পেরিয়ে গোপন পারস্পরিক হাতুড়ানো হলো smell।

কলেজ কর্নার: তুমি একটাও class না পড়েই একটা repository-তে এই smell পরিমাপ করতে পারো। logical coupling (change coupling বা co-change-ও বলে) এর জন্য version history থেকে data নাও: গণনা করো একই commit-এ দুটো file কতবার একসাথে আসে। যেসব জুটির মডিউলের মধ্যে কোনো declared dependency নেই কিন্তু co-change অনেক বেশি সেগুলো prime ঘনিষ্ঠতার সন্দেহভাজন। সেটার সাথে structural metric মেলাও — mutual CBO (প্রতিটা class অপরটার coupling-এ গণনা) আর Robert Martin-এর package metric থেকে instability I = Ce / (Ca + Ce) — আর তুমি জট পাকানো জুটির একটা ranked তালিকা পাবে। অনেক দল বড় refactoring sprint-এর আগে ঠিক এই analysis চালায়: history বলে দেয় কোথায় রান্নাঘরগুলো shared।

বাস্তব project-এ এই smell কোথায় লুকিয়ে থাকে

১. দ্বিমুখী navigation সহ ORM entity। Entity Framework আর Hibernate দুই-দিকের link সহজ করে: Order.Customer আর Customer.Orders। Query-র জন্য সুবিধাজনক — কিন্তু application code হাতে উভয় প্রান্ত বদলাতে শুরু করে, আর জুটিটা অবিচ্ছেদ্য হয়ে যায়। অনেক দল এখন একদিকের reference আর aggregate root (একটা boss object যে তার সন্তানদের মালিকানা রাখে) পছন্দ করে এটা ঠেকাতে।

২. Parent-child UI component। একটা screen widget যেটা তার child-এর internal state-এ পৌঁছায়, যেখানে child আবার উপরে call করে parent-এর flag উল্টায়। Parent layout বদলাও, child ভাঙো; child বদলাও, parent ভাঙো। Component framework-গুলো এটা একদিকের রাখতে props-down/events-up pattern ব্যবহার করে।

৩. একটা class আর তার "সেরা বন্ধু" helper। একটা ReportGenerator আর ReportGeneratorHelper যারা internal access-এর মাধ্যমে field share করে আর বারবার একে অপরকে call করে। এরা দুটো নাম নিয়ে একটাই class। হয় সৎভাবে একত্রিত করো নয়তো সঠিকভাবে আলাদা করো।

৪. Subclass-superclass পিছনের দরজা। একটা base Controller যাতে ডজন খানেক protected field আছে আর প্রতিটা subclass স্বাধীনভাবে সেগুলো বদলায়। Protected state পুরো পরিবার hierarchy-র জন্য একটা shared রান্নাঘর। এটা Refused Bequest-এর কাজিন, আর Replace Inheritance with Delegation হলো বের হওয়ার পথ।

৫. Internal mirror করা test code। যেসব test reflection বা @VisibleForTesting পিছনের দরজার মাধ্যমে private state-এ পৌঁছায় তারা production class-এর সাথে ঘনিষ্ঠ। Refactoring যখন behavior না ভেঙে পঞ্চাশটা test ভাঙে, ঘনিষ্ঠতাই কারণ।

৬. পারস্পরিক-পরিচিত manager-দের একটা জাল। যখন বেশ কয়েকটা "manager" class একে অপরের reference রাখে আর স্বাধীনভাবে একে অপরকে call করে, পুরো colony জুড়ে ঘনিষ্ঠতা। Mediator pattern এই জালকে একটা hub দিয়ে প্রতিস্থাপন করে যার মাধ্যমে সবাই যোগাযোগ করে।

কখন উপেক্ষা করা যায়

কিছু ঘনিষ্ঠতা পরিকল্পিত, সেটা ভাঙা ভুল হবে। সৎভাবে বিচার করো:

পরিস্থিতিরায়কারণ
Iterator আর তার collectionগ্রহণযোগ্যতারা একটা concept তৈরি করে; iterator-কে collection-এর structure জানতে হবে সেটা হাঁটতে
Builder আর সে যে product তৈরি করেগ্রহণযোগ্যBuilder-টা product-এর internal set up করতেই আছে; build()-এর পরে ঘনিষ্ঠতা শেষ
internal / package-private scope ব্যবহার করে একই module-এ দুটো classইচ্ছাকৃত হলে গ্রহণযোগ্যLanguage-গুলো বৃহত্তর জগত থেকে লুকিয়ে নিয়ন্ত্রিত ঘনিষ্ঠতা রাখতে এই scope দেয়
দুটো class যারা সবসময় একটা unit হিসেবে ship, পরিবর্তন আর version হয়সহনীয়সত্যিই পরিবর্তনের একটা unit হলে খরচ কম — কিন্তু shared surface ন্যূনতম রাখো
ভিন্ন module-এ দুটো class একে অপরের field বদলাচ্ছেঠিক করোCross-module ঘনিষ্ঠতা reuse, testing আর স্বাধীন পরিবর্তন আটকায়
Subclass parent-এর ঠিক internal layout-এর উপর নির্ভর করছেঠিক করোInheritance চুক্তি ভাঙা; delegation ব্যবহার করো
ℹ️

Smell-এর নামটাই সঠিকভাবে বলে দেয়: আমরা inappropriate ঘনিষ্ঠতা সারাই, সব ধরনের ঘনিষ্ঠতা নয়। একটা প্রশ্ন জিজ্ঞেস করো: "এই দুটো class-কে কি কখনো আলাদাভাবে পরিবর্তন, ship, বা test করার দরকার হতে পারে?" যদি হ্যাঁ — আর module সীমানার ওপারে প্রায় সবসময়ই হ্যাঁ — তাহলে তাদের ঘনিষ্ঠতা অনুপযুক্ত, আর প্রতিটা shared internal কোনো একদিন তার বিল পাঠাবে।

কোন refactoring এটা সারায়

Refactoringকখন ব্যবহার করবেএটা কী করে
Move Method / Move FieldBehavior আর data বিপরীত দিকে থাকেData-কে যে code এটা ব্যবহার করে তার সাথে মেলায়, reach-across বন্ধ করে
Extract Classঘনিষ্ঠতা একটা হারানো shared concept লুকিয়ে রাখেএকটা তৃতীয় class তৈরি করে যেটার উপর দুটো পরিষ্কারভাবে নির্ভর করতে পারে
Hide Delegateএকটা class অন্যটার মধ্য দিয়ে তৃতীয়তে navigate করেNavigation-কে একটা ভদ্র প্রশ্ন দিয়ে প্রতিস্থাপন করে — Law of Demeter পুনরুদ্ধার
Change Bidirectional Association to Unidirectionalএকটা back-reference আছে কিন্তু সত্যিই দরকার নেইস্পেয়ার চাবি আর এর সাথে আসা প্রলোভন সরিয়ে দেয়
Replace Inheritance with DelegationSubclass parent-এর internal-এর সাথে ঘনিষ্ঠপিছনের দরজার "is-a"-কে interface-এর মাধ্যমে পরিষ্কার "has-a"-তে বদলায়

দ্রুত পুনর্বিবেচনা বক্স

+=================================================================+
|             INAPPROPRIATE INTIMACY — QUICK REVISION              |
+=================================================================+
| STORY    : Mrs. Sharma & Mrs. Verma rearrange each other's       |
|            kitchens. Neither can renovate. No privacy,           |
|            total dependency.                                     |
|                                                                  |
| SMELL    : Two classes read AND WRITE each other's internals,    |
|            often with references in both directions.             |
|                                                                  |
| LAW      : Law of Demeter — "talk only to your immediate         |
|            friends." Ask for what you need; never walk in        |
|            and take it.                                          |
|                                                                  |
| SPOT IT  : paired commits, bidirectional refs, fields exposed    |
|            only for the partner, untestable alone                |
|                                                                  |
| CURE     : decide ownership -> Move Method / Move Field          |
|            -> cut the back-reference -> make fields private      |
|            -> Hide Delegate for leftover reaching                |
|            -> Extract Class if a shared concept appears          |
|                                                                  |
| IGNORE   : iterator+collection, builder+product,                 |
|            declared internal/friend scope within one module      |
|                                                                  |
| RULE     : Collaborate through small polite interfaces,          |
|            never through each other's private state.             |
+=================================================================+

অনুশীলন প্রশ্ন

ধরো একটা school sports app-এ এই দুটো জট পাকানো class আছে:

class Team {
  players: Player[] = [];
  totalSkill = 0;
 
  recruit(p: Player) {
    this.players.push(p);
    p.team = this;                 // writes Player's field
    p.isCaptain = this.players.length === 1;  // writes Player's field
    this.totalSkill += p.skill;    // reads Player's field
  }
}
 
class Player {
  skill = 0;
  team: Team | null = null;
  isCaptain = false;
 
  quit() {
    if (!this.team) return;
    this.team.totalSkill -= this.skill;            // writes Team's field
    const i = this.team.players.indexOf(this);
    this.team.players.splice(i, 1);                // mutates Team's list
    if (this.isCaptain && this.team.players.length > 0) {
      this.team.players[0].isCaptain = true;       // writes ANOTHER player's field!
    }
  }
}

তোমার কাজ:

  1. অনুপ্রবেশগুলো তালিকা করো। প্রতিটা লাইন লিখে দাও যেখানে একটা class অন্য object-এর field পড়ে বা লেখে। কতটা পড়া আর কতটা লেখা? (ইঙ্গিত: এমনকি একটা player আরেকটা player-এর ঘরে লিখছে।)
  2. Law of Demeter লঙ্ঘন খোঁজো। quit()-এ, method-এর কোন object-গুলো "কাছের বন্ধু" আর কোনগুলো সে যেকোনোভাবে ছুঁয়েছে? পূর্ণ নম্বর চাইলে College corner-এর আনুষ্ঠানিক বিবৃতি ব্যবহার করো।
  3. মালিকানা ঠিক করো। Player তালিকা আর captain নির্বাচন কে ধারণ করা উচিত — Team নাকি Player? একটা বাক্যে কারণ লেখো।
  4. জুটিটা plot করো। চিত্র ৩-এর chart-এ, তোমার refactoring-এর আগে Team-Player জুটি কোথায় থাকে? পরে কোথায় থাকা উচিত?
  5. Refactor করো। জুটিটা এভাবে পুনর্লিখন করো যাতে: Team-এ recruit(p) আর release(p) আছে আর captaincy-র মালিক সে; Player skill() একটা method হিসেবে উন্মুক্ত করে আর Team-এর কোনো back-reference রাখে না। সব field private। Caller-রা player.quit() এর বদলে team.release(player) বলে।
  6. ফলাফল test করো। Skill 7 সহ একটা Player ব্যবহার করে Team.release-এর জন্য একটা unit test লেখো (বা শুধু বর্ণনা করো)। Player-এর ভেতরে পুরো আসল Team ছাড়া কি পুরনো code-এ এই test লেখা যেত? কেন না?
  7. বোনাস। ধরো captain-নির্বাচনের নিয়ম জটিল হয়ে যায় (সিনিয়রিটি, উপস্থিতি, কোচের পছন্দ)। তুমি কি এটা Team-এর ভেতরে রাখবে, নাকি একটা CaptainPolicy class বের করবে? এই post থেকে সেটা কোন refactoring?

তোমার refactor করা code-এ যদি class সীমানার ওপারে শূন্য write থাকে, তাহলে দুটো রান্নাঘরই আবার private। শাবাশ!

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

Inappropriate Intimacy code smell জিনিসটা কী?
এটা হলো সেই অবস্থা যখন দুটো class একে অপরের ভেতরের সব কিছু জেনে ফেলে। তারা একে অপরের field পড়ে, লেখে, private structure-এর উপর নির্ভর করে, আর প্রায়ই দুই দিক থেকেই একে অপরের reference রাখে। একটা বদলালে অন্যটাতেও বদলাতে হয় — আসলে দুই নাম নিয়ে এরা একটাই class হয়ে গেছে।
Inappropriate Intimacy আর Feature Envy-এর পার্থক্যটা কী?
Feature Envy একদিকে: class A-এর একটা method বারবার class B-এর data ব্যবহার করে। Inappropriate Intimacy হলো সেটার পরিণত, দুমুখী সংস্করণ — A, B-এর ভেতরে হাত দেয় আর B-ও A-এর ভেতরে হাত দেয়। Feature Envy হলো একজন নাক-গলানো প্রতিবেশী; Inappropriate Intimacy হলো দুজন প্রতিবেশী যারা একে অপরের রান্নাঘর সাজিয়ে দেয়।
Law of Demeter সহজ কথায় কী?
শুধু তোমার কাছের বন্ধুদের সাথে কথা বলো, অপরিচিতদের সাথে না। একটা method শুধু তার নিজের object-এর method, নিজের field, তার parameter, আর নিজে তৈরি করা object-এর সাথে কথা বলতে পারবে — কিন্তু বন্ধুর ভেতর দিয়ে বন্ধুর ভেতরের জিনিসে পৌঁছানো যাবে না। যা দরকার সেটা চাও, ভেতরে ঢুকে নিজে নিয়ে নিও না।
Inappropriate Intimacy ঠিক করতে কোন refactoring কাজে লাগে?
Move Method আর Move Field data আর behavior-কে একই class-এ রাখে, ফলে reach-across বন্ধ হয়। Extract Class একটা shared concept বের করে যেটা নিয়ে দুটো class টানাটানি করছিল। Hide Delegate গভীরে পৌঁছানোর বদলে ভদ্রভাবে জিজ্ঞেস করার ব্যবস্থা করে। Change Bidirectional Association to Unidirectional অপ্রয়োজনীয় back-reference সরায়। আর subclass যদি parent-এর সাথে বেশি ঘনিষ্ঠ হয়, তাহলে Replace Inheritance with Delegation।
কিছু class-এর মধ্যে ঘনিষ্ঠতা কি ঠিক আছে?
হ্যাঁ, কিছু ক্ষেত্রে। একটা iterator আর তার collection, একটা builder আর তার product — এই জুটিগুলো মিলে একটাই concept তৈরি করে, তাই তাদের একে অপরকে চেনা স্বাভাবিক। Language-গুলো এমনকি friend, internal, আর package-private scoping দেয় যাতে একটা module-এর ভেতরে নিয়ন্ত্রিত ঘনিষ্ঠতা রাখা যায়। সমস্যা হলো inappropriate ঘনিষ্ঠতা, সব ধরনের ঘনিষ্ঠতা না।

আরো দেখো

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

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

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

আরও পড়ুন

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

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

আরও পড়ুন

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

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

আরও পড়ুন

Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে

একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।

আরও পড়ুন