কাপলিং ও কোহেশন
এই পাঠে যা শিখবেন
- কাপলিং কী এবং কেন হাই কাপলিং একটি সিস্টেমকে ভঙ্গুর করে তোলে
- কোহেশন কী এবং কেন লো কোহেশন একটি ক্লাসকে বোঝা ও পুনঃব্যবহার করা কঠিন করে তোলে
- "low coupling, high cohesion" — এই দুটোর মধ্যে সম্পর্ক ও ভারসাম্য
- Python-এ একটি টাইট-কাপলড বনাম লুজ-কাপলড
ReportGenerator/Databaseজোড়া বাস্তবে তৈরি ও তুলনা করা
১ · কাপলিং কী
কাপলিংCouplingবিভিন্ন মডিউল/ক্লাস একে অপরের উপর কতটা নির্ভরশীল, তার মাপ। হলো ভিন্ন ভিন্ন মডিউল বা ক্লাস একে অপরের উপর কতটা নির্ভরশীল তার একটি মাপ। লো কাপলিং — যেখানে মডিউলগুলো তুলনামূলকভাবে স্বাধীন, একটিতে পরিবর্তন করলে অন্যগুলোতে বদল করতে হয় না — সাধারণত পছন্দনীয়। হাই কাপলিং — যেখানে মডিউলগুলো ঘনিষ্ঠভাবে জড়িয়ে থাকে — সিস্টেমকে ভঙ্গুর ও পরিবর্তন করা কঠিন করে তোলে: একটি জায়গায় করা পরিবর্তন অপ্রত্যাশিতভাবে অন্য জায়গায় গিয়ে পৌঁছায় — L03-এর মেইনটেইনেবিলিটি অ্যাট্রিবিউটের সাথে সরাসরি সম্পর্কিত।
২ · কোহেশন কী
কোহেশনCohesionএকটি একক মডিউল/ক্লাসের ভেতরের দায়িত্বগুলো একে অপরের সাথে কতটা সম্পর্কিত ও একটিমাত্র স্পষ্ট উদ্দেশ্যে কেন্দ্রীভূত, তার মাপ। হলো একটি একক মডিউল/ক্লাসের ভেতরের দায়িত্বগুলো একে অপরের সাথে কতটা সম্পর্কিত, তার একটি মাপ। হাই কোহেশন — একটি ক্লাস একটিমাত্র স্পষ্ট, ভালোভাবে সংজ্ঞায়িত কাজ করে — সাধারণত পছন্দনীয়। লো কোহেশন — একটি ক্লাস অনেকগুলো অসম্পর্কিত কাজ করে, একটি "গ্র্যাব-ব্যাগ" ক্লাস — বোঝা কঠিন করে তোলে ক্লাসটি আসলে কীসের জন্য, এবং পুনঃব্যবহারও কঠিন করে তোলে।
"Low coupling, high cohesion" — এই সংক্ষিপ্ত, স্মরণীয় নীতিটি সফটওয়্যার ডিজাইনের সবচেয়ে মৌলিক লক্ষ্যগুলোর একটি। M4/L16-এ আমরা দেখব Single Responsibility Principle মূলত এই লক্ষ্যের "হাই কোহেশন" অংশকেই একটি সুনির্দিষ্ট, কার্যকরযোগ্য নিয়মে পরিণত করে — একটি ক্লাসের পরিবর্তনের একটিমাত্র কারণ থাকা উচিত।
৩ · ওয়ার্কড এক্সাম্পল — টাইট বনাম লুজ কাপলিং
একটি বাস্তব উদাহরণ দেখা যাক: একটি ReportGenerator ক্লাস, যেটি একটি Database ক্লাস
থেকে বিক্রয়ের তথ্য নিয়ে মোট হিসাব করে। টাইট-কাপলড সংস্করণে ReportGenerator
সরাসরি Database-এর প্রাইভেট, অভ্যন্তরীণ ফিল্ডে হাত দেয়। লুজ-কাপলড সংস্করণে
ReportGenerator শুধু Database-এর একটি সংজ্ঞায়িত, পাবলিক মেথড কল করে — কখনো
অভ্যন্তরীণ ফিল্ডে হাত দেয় না।
# --- টাইট কাপলিং: ReportGenerator সরাসরি Database-এর প্রাইভেট ফিল্ডে হাত দিচ্ছে ---
class TightDatabase:
def __init__(self):
self._rows = [
{"product": "ল্যাপটপ", "amount": 55000},
{"product": "মাউস", "amount": 800},
{"product": "কীবোর্ড", "amount": 1500},
]
class TightReportGenerator:
def __init__(self, db):
self.db = db
def total_sales(self):
# সরাসরি db._rows-এ হাত দিচ্ছে -- Database-এর অভ্যন্তরীণ representation-এর উপর নির্ভরশীল
return sum(row["amount"] for row in self.db._rows)
# --- লুজ কাপলিং: ReportGenerator শুধু একটি সংজ্ঞায়িত পাবলিক মেথড কল করছে ---
class LooseDatabase:
def __init__(self):
self._records = [
{"product": "ল্যাপটপ", "amount": 55000},
{"product": "মাউস", "amount": 800},
{"product": "কীবোর্ড", "amount": 1500},
]
def get_all_sales(self):
# একমাত্র পাবলিক ইন্টারফেস -- অভ্যন্তরীণ ফিল্ডের নাম/গঠন যাই হোক, এই মেথড স্থিতিশীল থাকে
return list(self._records)
class LooseReportGenerator:
def __init__(self, db):
self.db = db
def total_sales(self):
return sum(row["amount"] for row in self.db.get_all_sales())
print("=== Database-এর অভ্যন্তরীণ গঠন বদলানোর আগে ===")
print("Tight total:", TightReportGenerator(TightDatabase()).total_sales())
print("Loose total:", LooseReportGenerator(LooseDatabase()).total_sales())
# এখন Database-এর অভ্যন্তরীণ গঠন বাস্তবসম্মতভাবে রিফ্যাক্টর করা হলো:
# dict-এর list-এর বদলে {product: amount} একটি সরাসরি dict ব্যবহার করা হচ্ছে
class TightDatabaseV2:
def __init__(self):
self._sales_by_product = {"ল্যাপটপ": 55000, "মাউস": 800, "কীবোর্ড": 1500} # নাম ও গঠন দুটোই বদলে গেছে
class LooseDatabaseV2:
def __init__(self):
self._sales_by_product = {"ল্যাপটপ": 55000, "মাউস": 800, "কীবোর্ড": 1500}
def get_all_sales(self):
# পাবলিক ইন্টারফেস অপরিবর্তিত -- ভেতরে যাই বদলাক, বাইরে থেকে একই ফরম্যাট দেখা যায়
return [{"product": p, "amount": a} for p, a in self._sales_by_product.items()]
print("\n=== Database-এর অভ্যন্তরীণ গঠন বদলানোর পরে ===")
try:
print("Tight total:", TightReportGenerator(TightDatabaseV2()).total_sales())
except AttributeError as e:
print("Tight version ভেঙে গেছে:", e)
print("Loose total:", LooseReportGenerator(LooseDatabaseV2()).total_sales())
Database-এর
অভ্যন্তরীণ গঠন বাস্তবসম্মতভাবে বদলানোর পরে (একটি সম্পূর্ণ স্বাভাবিক রিফ্যাক্টরিং, বাইরের কেউ যা জানার কথা না),
টাইট-কাপলড TightReportGenerator AttributeError দিয়ে ভেঙে পড়ে — কারণ এটি
_rows নামের একটি ফিল্ডের অস্তিত্বের উপর সরাসরি নির্ভরশীল ছিল, যা আর নেই। লুজ-কাপলড
LooseReportGenerator একই সঠিক ফলাফল (৫৭৩০০) দিয়েই চলতে থাকে, কারণ এটি শুধুমাত্র
get_all_sales()-এর স্থিতিশীল পাবলিক চুক্তির উপর নির্ভর করে।
লো কাপলিং মানে একটি ক্লাস অন্য ক্লাসের অভ্যন্তরীণ বিস্তারিত না জেনেও কাজ করতে পারে — শুধু তার সংজ্ঞায়িত, স্থিতিশীল পাবলিক ইন্টারফেসের উপর নির্ভর করে। এটিই মেইনটেইনেবিলিটির একটি বাস্তব, পরিমাপযোগ্য ভিত্তি — উপরের কোড সেলে আমরা এটি শুধু বলিনি, প্রমাণ করেছি।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি ক্লাসের লো কোহেশন থাকলে সেটি কীভাবে বোঝা যায় — বাস্তবে কী লক্ষণ দেখা যায়?
একটি সাধারণ লক্ষণ: ক্লাসটির নাম বা তার একক দায়িত্ব বর্ণনা করা কঠিন হয়ে পড়ে (যেমন "Manager" বা "Utils" নামের ক্লাস, যেগুলোতে ধীরে ধীরে অসম্পর্কিত মেথড জমা হতে থাকে)। আরেকটি লক্ষণ — ক্লাসের বিভিন্ন মেথড ক্লাসের সম্পূর্ণ আলাদা আলাদা অংশের ফিল্ড ব্যবহার করে, একে অপরের সাথে খুব কম সম্পর্কিত। M4/L16-এর SRP এই লক্ষণটিকেই একটি সুনির্দিষ্ট নিয়মে ("একটি ক্লাসের পরিবর্তনের একটিমাত্র কারণ থাকা উচিত") রূপান্তরিত করে।
প্র ০২
উপরের উদাহরণে যদি TightDatabase-এর _rows ফিল্ডের নাম কখনো না বদলাতো, তাহলে কি টাইট কাপলিং আসলেই একটি সমস্যা?
হ্যাঁ, তবুও — কারণ ঝুঁকিটা এখানেই: টাইট কাপলিং মানে ভবিষ্যতে যেকোনো অভ্যন্তরীণ পরিবর্তন (নাম বদল, গঠন বদল,
এমনকি একটি নতুন storage backend-এ স্থানান্তর) সম্ভাব্যভাবে অন্য কোডকে ভেঙে দিতে পারে, যদিও সেই কোড
Database-এর "আসল কাজ" (বিক্রয় তথ্য দেওয়া) থেকে কিছুই পরিবর্তন করেনি। বাস্তব সিস্টেমে কোড
ক্রমাগত রিফ্যাক্টর হয় (M11-এর বিষয়) — লো কাপলিং সেই রিফ্যাক্টরিংকে নিরাপদ করে তোলে।
প্র ০৩
উপরের কোড সেলে get_all_sales() মেথডটি কীভাবে "কাপলিং কমানোর সীমানা" (boundary) হিসেবে কাজ করে?
get_all_sales() একটি স্থিতিশীল "চুক্তি" (contract) — এটি সবসময় একই ফরম্যাটে (dict-এর list,
প্রতিটিতে product ও amount key) ডেটা ফেরত দেবে, ভেতরে সেই ডেটা কীভাবে সংরক্ষিত
আছে তা যাই হোক না কেন। ReportGenerator শুধু এই সীমানার বাইরের দিকটাই দেখে — এই সীমানা
পার হয়ে ভেতরের কোনো ডিটেইলের উপর নির্ভর করে না, তাই ভেতরের যেকোনো পরিবর্তন থেকে সে সুরক্ষিত থাকে।
অনুশীলন
-
চিন্তা করুন: একটি
Orderক্লাস যদি অর্ডারের ডেটা রাখার পাশাপাশি সরাসরি ইমেইল পাঠানো, PDF ইনভয়েস তৈরি করা, এবং ডাটাবেসে সংরক্ষণ করার কোডও নিজের ভেতরে রাখে — এটি লো নাকি হাই কোহেশন? কেন?এটি লো কোহেশনের একটি সরাসরি উদাহরণ —
Orderক্লাসটি অন্তত চারটি অসম্পর্কিত দায়িত্ব পালন করছে (ডেটা রাখা, ইমেইল পাঠানো, PDF তৈরি, ডাটাবেসে সংরক্ষণ), প্রতিটির বদলানোর কারণ সম্পূর্ণ আলাদা (যেমন ইমেইল সার্ভিস বদলানো বনাম PDF ফরম্যাট বদলানো)। M4/L16-এ আমরা ঠিক এই ধরনের ক্লাসকে SRP অনুযায়ী কীভাবে আলাদা আলাদা ক্লাসে ভাগ করা হয় তা বিস্তারিত দেখব। -
পরীক্ষা করুন: উপরের কোড সেলে
TightDatabaseV2-এ একটি নতুন প্রোডাক্ট ("মনিটর": ১২০০০) যোগ করুন এবং Run চেপে দেখুনTightReportGeneratorএখনও একই ধরনের এরর দেয় কিনা এবংLooseReportGeneratorসঠিক নতুন যোগফল দেয় কিনা।TightReportGeneratorএখনও একইAttributeErrorদেবে — কারণ সমস্যাটা কোনো নির্দিষ্ট ডেটার সাথে সম্পর্কিত নয়, বরং_rowsফিল্ডটিই আর নেই।LooseReportGeneratorএকইভাবেget_all_sales()-এর মাধ্যমে নতুন ডেটাসহ সঠিক যোগফল (৫৭৩০০ + ১২০০০ = ৬৯৩০০) দেবে — নতুন প্রোডাক্ট যোগ করা তার জন্য সম্পূর্ণ স্বচ্ছ, কারণ সে কখনোই অভ্যন্তরীণ গঠনের উপর নির্ভরশীল ছিল না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পাঠ ১৬ · SOLID প্রিন্সিপল — SRP ও OCP সরাসরি সম্পর্কিত এই লেসনের "হাই কোহেশন" লক্ষ্যকে Single Responsibility Principle একটি সুনির্দিষ্ট নিয়মে পরিণত করে।
- পাঠ ১৫ · DRY, KISS ও YAGNI প্রিন্সিপল পরবর্তী পাঠ আরও তিনটি ক্লাসিক ডিজাইন মাক্সিম — সরল, পুনরাবৃত্তিহীন, প্রয়োজন-ভিত্তিক কোড লেখার নীতি।