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

SOLID প্রিন্সিপল — SRP ও OCP

SOLID principles — SRP & OCP
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • SOLID কী — পাঁচটি প্রিন্সিপলের একটি পরিচিতি ও এই কোর্সে সেগুলো কীভাবে ভাগ করা আছে
  • Single Responsibility Principle এবং L14-এর কোহেশনের সাথে এর সরাসরি সম্পর্ক
  • Open/Closed Principle এবং M5-এর ডিজাইন প্যাটার্নের সাথে এর সরাসরি সম্পর্ক
  • Python-এ SRP-লঙ্ঘনকারী Employee গড-ক্লাস ও বিভক্ত সংস্করণ বাস্তবে তুলনা করা
  • Python-এ OCP-লঙ্ঘনকারী if/elif শেপ-এরিয়া ফাংশন বনাম পলিমরফিক সংস্করণ — একটি নতুন শেপ যোগ করে জিরো মডিফিকেশন প্রমাণ করা

১ · SOLID পরিচিতি

SOLIDSOLIDপাঁচটি ব্যাপকভাবে-উদ্ধৃত অবজেক্ট-ওরিয়েন্টেড সফটওয়্যার ডিজাইন প্রিন্সিপল, প্রতিটির নামের প্রথম অক্ষর মিলিয়ে গঠিত। হলো পাঁচটি ব্যাপকভাবে-উদ্ধৃত অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্রিন্সিপল — L14-এর কাপলিং/কোহেশন লক্ষ্যকে নির্দিষ্ট, কার্যকরযোগ্য নিয়মে রূপান্তরিত ও বিস্তৃত করে। এই পাঠে প্রথম দুটি (SRP, OCP), L17-এ পরের দুটি (LSP, ISP), এবং L18-এ পঞ্চমটি (DIP) কভার করা হবে।

২ · S — Single Responsibility Principle

Single Responsibility PrincipleSRPএকটি ক্লাসের পরিবর্তনের একটিমাত্র কারণ থাকা উচিত — অর্থাৎ একটি ক্লাস একটিমাত্র, ভালোভাবে-সংজ্ঞায়িত ফাংশনালিটির জন্য দায়ী হওয়া উচিত। বলে — একটি ক্লাসের পরিবর্তনের একটিমাত্র কারণ থাকা উচিত — L14-এর "হাই কোহেশন" লক্ষ্যকে একটি নির্দিষ্ট নিয়মে রূপান্তরিত করা। নিচের কোডে দেখা যাক একটি Employee ক্লাস যা একই সাথে এমপ্লয়ির ডেটা রাখে, পেরোল হিসাব করে, এবং ডাটাবেসে রেকর্ড সংরক্ষণ করে — তিনটি সম্পূর্ণ অসম্পর্কিত দায়িত্ব, প্রতিটির বদলানোর কারণ ভিন্ন — বনাম একটি রিফ্যাক্টর করা সংস্করণ যা এগুলোকে Employee, PayrollCalculator, EmployeeRepository-তে বিভক্ত করে।

Python
# --- SRP লঙ্ঘন: একটি Employee ক্লাস তিনটি অসম্পর্কিত দায়িত্ব পালন করছে ---
class Employee:
    def __init__(self, name, hours_worked, hourly_rate):
        self.name = name
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_pay(self):
        # দায়িত্ব ১: পেরোল হিসাব -- বদলানোর কারণ: কোম্পানির পেরোল নীতি বদলালে
        return self.hours_worked * self.hourly_rate

    def save_to_database(self):
        # দায়িত্ব ২: ডেটা সংরক্ষণ -- বদলানোর কারণ: ডাটাবেস/স্টোরেজ টেকনোলজি বদলালে
        print(f"[DB] {self.name}-এর রেকর্ড সংরক্ষণ করা হলো")


# --- SRP-কমপ্লায়েন্ট: প্রতিটি ক্লাসের একটিমাত্র দায়িত্ব ---
class EmployeeRecord:
    def __init__(self, name, hours_worked, hourly_rate):
        self.name = name
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate


class PayrollCalculator:
    def calculate_pay(self, employee):
        return employee.hours_worked * employee.hourly_rate


class EmployeeRepository:
    def save(self, employee):
        print(f"[DB] {employee.name}-এর রেকর্ড সংরক্ষণ করা হলো")


print("=== SRP-লঙ্ঘনকারী সংস্করণ ===")
emp = Employee("রহিম", 160, 250)
print("বেতন:", emp.calculate_pay())
emp.save_to_database()

print("\n=== SRP-কমপ্লায়েন্ট সংস্করণ ===")
record = EmployeeRecord("করিম", 160, 250)
calculator = PayrollCalculator()
repository = EmployeeRepository()
print("বেতন:", calculator.calculate_pay(record))
repository.save(record)

    
দুটি সংস্করণই একই সঠিক বেতন দেয়: ১৬০ ঘণ্টা × ২৫০ টাকা = ৪০,০০০ টাকা। পার্থক্যটা ফলাফলে নয়, গঠনে — SRP-কমপ্লায়েন্ট সংস্করণে যদি পেরোল হিসাবের নিয়ম বদলায়, শুধু PayrollCalculator বদলাতে হয়; ডাটাবেস প্রযুক্তি বদলালে শুধু EmployeeRepository বদলাতে হয় — EmployeeRecord কখনো স্পর্শ করতে হয় না। SRP-লঙ্ঘনকারী সংস্করণে এই তিনটি বদলানোর কারণই একটিমাত্র ক্লাসে জড়িয়ে আছে।

৩ · O — Open/Closed Principle

Open/Closed PrincipleOCPসফটওয়্যার এনটিটি (ক্লাস, ফাংশন) এক্সটেনশনের জন্য উন্মুক্ত কিন্তু মডিফিকেশনের জন্য বদ্ধ হওয়া উচিত -- নতুন আচরণ যোগ করা যায় বিদ্যমান, পরীক্ষিত কোড পরিবর্তন না করেই। বলে — সফটওয়্যার এনটিটি এক্সটেনশনের জন্য উন্মুক্ত, কিন্তু মডিফিকেশনের জন্য বদ্ধ হওয়া উচিত — নতুন আচরণ যোগ করা যাবে বিদ্যমান, ইতিমধ্যে-পরীক্ষিত কোড পরিবর্তন না করেই। এটি M5-এর ডিজাইন প্যাটার্নের (বিশেষত M5/L23-এর Strategy) সরাসরি ভূমিকা — এই প্যাটার্নগুলো ঠিক এই OCP-কমপ্লায়েন্স সম্ভব করার জন্যই তৈরি।

নিচে একটি if/elif চেইন দিয়ে শেপের এরিয়া হিসাব করা একটি ফাংশন — নতুন শেপ যোগ করতে হলে এই ফাংশনটাই পরিবর্তন করতে হয় (OCP-লঙ্ঘন) — বনাম পলিমরফিজম ব্যবহার করা একটি সংস্করণ, যেখানে প্রতিটি শেপ নিজের area() মেথড বাস্তবায়ন করে।

Python
# --- OCP লঙ্ঘন: নতুন shape যোগ করতে এই ফাংশনটাই পরিবর্তন করতে হয় ---
def calculate_area_bad(shape_type, **kwargs):
    if shape_type == "circle":
        return 3.1416 * kwargs["radius"] ** 2
    elif shape_type == "rectangle":
        return kwargs["width"] * kwargs["height"]
    else:
        raise ValueError(f"অজানা shape: {shape_type}")


print("=== OCP-লঙ্ঘনকারী সংস্করণ ===")
print("বৃত্ত (radius=3):", calculate_area_bad("circle", radius=3))
print("আয়তক্ষেত্র (4x5):", calculate_area_bad("rectangle", width=4, height=5))


# --- OCP-কমপ্লায়েন্ট: প্রতিটি shape নিজের area() মেথড বাস্তবায়ন করে ---
class Shape:
    def area(self):
        raise NotImplementedError


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.1416 * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height


def total_area(shapes):
    # লক্ষ্য করুন -- এই ফাংশনে কোনো shape-এর নাম হার্ডকোড করা নেই, তাই নতুন shape যোগ করলেও এটি অপরিবর্তিত থাকে
    return sum(shape.area() for shape in shapes)


shapes = [Circle(3), Rectangle(4, 5)]
print("\n=== OCP-কমপ্লায়েন্ট সংস্করণ (Triangle যোগ করার আগে) ===")
print("মোট এরিয়া:", total_area(shapes))

# নতুন shape: Triangle -- বিদ্যমান Shape, Circle, Rectangle, বা total_area()-এর একটিও পরিবর্তন করা হয়নি
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height


shapes.append(Triangle(6, 4))
print("\n=== Triangle যোগ করার পরে (বিদ্যমান কোনো কোডে জিরো পরিবর্তন) ===")
print("মোট এরিয়া:", total_area(shapes))

    
হিসাব যাচাই করা যাক: বৃত্ত (radius 3) = ৩.১৪১৬ × ৩² = ২৮.২৭৪৪; আয়তক্ষেত্র (৪×৫) = ২০ — দুটোর যোগফল = ৪৮.২৭৪৪। Triangle (base 6, height 4) যোগ করার পরে = ০.৫ × ৬ × ৪ = ১২, তাই নতুন মোট = ৪৮.২৭৪৪ + ১২ = ৬০.২৭৪৪। লক্ষ্য করুন Triangle ক্লাস যোগ করা হয়েছে, কিন্তু Shape, Circle, Rectangle, বা total_area() — এদের একটিতেও একটি অক্ষর পরিবর্তন করতে হয়নি। এটাই OCP-এর "এক্সটেনশনের জন্য উন্মুক্ত, মডিফিকেশনের জন্য বদ্ধ" — কোডে সরাসরি প্রমাণিত।
মূল কথা · Key takeaway

SRP নিশ্চিত করে একটি ক্লাসের বদলানোর কারণ একটিমাত্র হয় — এটি L14-এর হাই কোহেশনকে একটি সুনির্দিষ্ট নিয়মে রূপান্তরিত করে। OCP নিশ্চিত করে নতুন আচরণ যোগ করতে বিদ্যমান, পরীক্ষিত কোড ভাঙতে হয় না — পলিমরফিজম এটি সম্ভব করে তোলে, এবং M5-এর অনেক ডিজাইন প্যাটার্ন ঠিক এই লক্ষ্যেই তৈরি। L17-এ আমরা SOLID-এর পরের দুটি প্রিন্সিপল — LSP ও ISP — দেখব।

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

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

প্র ০১ SRP-এ "একটিমাত্র কারণ" বলতে ঠিক কী বোঝানো হয়েছে — একটি ক্লাসে কি শুধুমাত্র একটিমাত্র মেথড থাকতে হবে?

না — SRP মেথডের সংখ্যা নিয়ে নয়, বরং বদলানোর কারণ নিয়ে। উপরের PayrollCalculator ক্লাসে একাধিক মেথড থাকতে পারে (যেমন calculate_pay(), calculate_overtime(), calculate_bonus()) — যতক্ষণ পর্যন্ত সবগুলোই একই বিষয়ের (পেরোল হিসাব) সাথে সম্পর্কিত এবং একই কারণে (পেরোল নীতি বদল) বদলায়, ততক্ষণ SRP লঙ্ঘিত হয় না। সমস্যা তখনই হয় যখন সম্পূর্ণ অসম্পর্কিত কারণে বদলানো লজিক (যেমন ডাটাবেস স্টোরেজ) একই ক্লাসে মিশে যায়।

প্র ০২ OCP মেনে চলতে গিয়ে প্রতিটি সম্ভাব্য পরিবর্তনের জন্য আগে থেকেই একটি abstraction তৈরি করা কি সবসময় ভালো ধারণা?

না — এটি L15-এর YAGNI প্রিন্সিপলের সরাসরি বিপরীত হতে পারে। প্রতিটি সম্ভাব্য ভবিষ্যৎ পরিবর্তনের জন্য আগে থেকেই abstraction তৈরি করা অপ্রয়োজনীয় জটিলতা যোগ করে (KISS-এর লঙ্ঘন) এমন এক্সটেনশন পয়েন্টের জন্য যা হয়তো কখনো ব্যবহৃত হবে না। বাস্তবসম্মত পন্থা: যেসব জায়গায় পরিবর্তন সত্যিকারে ঘন ঘন হওয়ার সম্ভাবনা আছে (যেমন এই লেসনের শেপ-এরিয়া উদাহরণ, যেখানে নতুন শেপ যোগ হওয়া স্বাভাবিক), সেখানেই OCP-কমপ্লায়েন্ট ডিজাইন প্রয়োগ করা।

প্র ০৩ উপরের কোড সেলে total_area() ফাংশনটি কীভাবে OCP মেনে চলে, যদিও এটি নিজে কোনো নতুন শেপ ক্লাস তৈরি করে না?

total_area() কখনো কোনো নির্দিষ্ট শেপের নাম (যেমন "circle" বা "triangle") হার্ডকোড করে না — এটি শুধু ধরে নেয় প্রতিটি অবজেক্টের একটি area() মেথড আছে (Shape-এর ইন্টারফেসের উপর নির্ভরশীল, কোনো নির্দিষ্ট কনক্রিট ক্লাসের উপর নয়)। তাই যখন Triangle যোগ করা হলো, total_area()-এর একটি অক্ষরও বদলানো লাগেনি — এটিই পলিমরফিজমের মাধ্যমে OCP অর্জনের মূল কৌশল।

অনুশীলন

  1. চিন্তা করুন: একটি OrderProcessor ক্লাস যদি অর্ডার ভ্যালিডেশন, প্রাইসিং হিসাব, এবং একই সাথে ইমেইল কনফার্মেশন পাঠানোর কোড — সবকিছু একসাথে রাখে, তাহলে এটি SRP-এর সাপেক্ষে কী সমস্যা তৈরি করবে, এবং কীভাবে এটি ভাগ করা যেতে পারে?

    এটি SRP-লঙ্ঘন — তিনটি ভিন্ন কারণে বদলাতে পারে: ভ্যালিডেশন নিয়ম বদলালে, প্রাইসিং নীতি বদলালে, অথবা ইমেইল সার্ভিস/টেমপ্লেট বদলালে। যুক্তিসঙ্গত বিভাজন: OrderValidator (শুধু ভ্যালিডেশন), PricingCalculator (শুধু প্রাইসিং), এবং OrderNotifier (শুধু ইমেইল/নোটিফিকেশন) — ঠিক এই লেসনের Employee-কে তিনটি ক্লাসে ভাগ করার একই প্যাটার্ন অনুসরণ করে।

  2. পরীক্ষা করুন: উপরের OCP কোড সেলে একটি নতুন Square ক্লাস যোগ করুন (side প্যারামিটার নিয়ে, area() মেথডে side ** 2 রিটার্ন করে), এটি shapes লিস্টে যোগ করুন, এবং Run চেপে দেখুন total_area()-এর কোনো পরিবর্তন ছাড়াই সঠিক নতুন যোগফল আসে কিনা।

    হ্যাঁ — Square(5) যোগ করলে এরিয়া ২৫ যোগ হবে (৬০.২৭৪৪ + ২৫ = ৮৫.২৭৪৪), এবং total_area() ফাংশনে কোনো পরিবর্তন লাগবে না, কারণ এটি শুধু shape.area() কল করে, শেপের ধরন যাচাই করে না। এটি আবারও নিশ্চিত করে Shape হায়ারার্কি সত্যিকারেই এক্সটেনশনের জন্য উন্মুক্ত।

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

আগের পাঠ
DRY, KISS ও YAGNI প্রিন্সিপল