পাঠ ১৪ · ৫৮-এর মধ্যে · মডিউল ৪
Home / Courses / Software Engineering Principles & Git / সফটওয়্যার ডিজাইন প্রিন্সিপল

কাপলিং ও কোহেশন

Coupling & cohesion
৮ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কাপলিং কী এবং কেন হাই কাপলিং একটি সিস্টেমকে ভঙ্গুর করে তোলে
  • কোহেশন কী এবং কেন লো কোহেশন একটি ক্লাসকে বোঝা ও পুনঃব্যবহার করা কঠিন করে তোলে
  • "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-এর একটি সংজ্ঞায়িত, পাবলিক মেথড কল করে — কখনো অভ্যন্তরীণ ফিল্ডে হাত দেয় না।

Python
# --- টাইট কাপলিং: 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()-এর স্থিতিশীল পাবলিক চুক্তির উপর নির্ভর করে।
মূল কথা · Key takeaway

লো কাপলিং মানে একটি ক্লাস অন্য ক্লাসের অভ্যন্তরীণ বিস্তারিত না জেনেও কাজ করতে পারে — শুধু তার সংজ্ঞায়িত, স্থিতিশীল পাবলিক ইন্টারফেসের উপর নির্ভর করে। এটিই মেইনটেইনেবিলিটির একটি বাস্তব, পরিমাপযোগ্য ভিত্তি — উপরের কোড সেলে আমরা এটি শুধু বলিনি, প্রমাণ করেছি।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ একটি ক্লাসের লো কোহেশন থাকলে সেটি কীভাবে বোঝা যায় — বাস্তবে কী লক্ষণ দেখা যায়?

একটি সাধারণ লক্ষণ: ক্লাসটির নাম বা তার একক দায়িত্ব বর্ণনা করা কঠিন হয়ে পড়ে (যেমন "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 শুধু এই সীমানার বাইরের দিকটাই দেখে — এই সীমানা পার হয়ে ভেতরের কোনো ডিটেইলের উপর নির্ভর করে না, তাই ভেতরের যেকোনো পরিবর্তন থেকে সে সুরক্ষিত থাকে।

অনুশীলন

  1. চিন্তা করুন: একটি Order ক্লাস যদি অর্ডারের ডেটা রাখার পাশাপাশি সরাসরি ইমেইল পাঠানো, PDF ইনভয়েস তৈরি করা, এবং ডাটাবেসে সংরক্ষণ করার কোডও নিজের ভেতরে রাখে — এটি লো নাকি হাই কোহেশন? কেন?

    এটি লো কোহেশনের একটি সরাসরি উদাহরণ — Order ক্লাসটি অন্তত চারটি অসম্পর্কিত দায়িত্ব পালন করছে (ডেটা রাখা, ইমেইল পাঠানো, PDF তৈরি, ডাটাবেসে সংরক্ষণ), প্রতিটির বদলানোর কারণ সম্পূর্ণ আলাদা (যেমন ইমেইল সার্ভিস বদলানো বনাম PDF ফরম্যাট বদলানো)। M4/L16-এ আমরা ঠিক এই ধরনের ক্লাসকে SRP অনুযায়ী কীভাবে আলাদা আলাদা ক্লাসে ভাগ করা হয় তা বিস্তারিত দেখব।

  2. পরীক্ষা করুন: উপরের কোড সেলে TightDatabaseV2-এ একটি নতুন প্রোডাক্ট ("মনিটর": ১২০০০) যোগ করুন এবং Run চেপে দেখুন TightReportGenerator এখনও একই ধরনের এরর দেয় কিনা এবং LooseReportGenerator সঠিক নতুন যোগফল দেয় কিনা।

    TightReportGenerator এখনও একই AttributeError দেবে — কারণ সমস্যাটা কোনো নির্দিষ্ট ডেটার সাথে সম্পর্কিত নয়, বরং _rows ফিল্ডটিই আর নেই। LooseReportGenerator একইভাবে get_all_sales()-এর মাধ্যমে নতুন ডেটাসহ সঠিক যোগফল (৫৭৩০০ + ১২০০০ = ৬৯৩০০) দেবে — নতুন প্রোডাক্ট যোগ করা তার জন্য সম্পূর্ণ স্বচ্ছ, কারণ সে কখনোই অভ্যন্তরীণ গঠনের উপর নির্ভরশীল ছিল না।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
ইউজার স্টোরি ও অ্যাকসেপ্টেন্স ক্রাইটেরিয়া