SOLID প্রিন্সিপল — SRP ও OCP
এই পাঠে যা শিখবেন
- 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-তে বিভক্ত করে।
# --- 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)
PayrollCalculator বদলাতে হয়;
ডাটাবেস প্রযুক্তি বদলালে শুধু EmployeeRepository বদলাতে হয় — EmployeeRecord কখনো
স্পর্শ করতে হয় না। SRP-লঙ্ঘনকারী সংস্করণে এই তিনটি বদলানোর কারণই একটিমাত্র ক্লাসে জড়িয়ে আছে।
৩ · O — Open/Closed Principle
Open/Closed PrincipleOCPসফটওয়্যার এনটিটি (ক্লাস, ফাংশন) এক্সটেনশনের জন্য উন্মুক্ত কিন্তু মডিফিকেশনের জন্য বদ্ধ হওয়া উচিত -- নতুন আচরণ যোগ করা যায় বিদ্যমান, পরীক্ষিত কোড পরিবর্তন না করেই। বলে — সফটওয়্যার এনটিটি এক্সটেনশনের জন্য উন্মুক্ত, কিন্তু মডিফিকেশনের জন্য বদ্ধ হওয়া উচিত — নতুন আচরণ যোগ করা যাবে বিদ্যমান, ইতিমধ্যে-পরীক্ষিত কোড পরিবর্তন না করেই। এটি M5-এর ডিজাইন প্যাটার্নের (বিশেষত M5/L23-এর Strategy) সরাসরি ভূমিকা — এই প্যাটার্নগুলো ঠিক এই OCP-কমপ্লায়েন্স সম্ভব করার জন্যই তৈরি।
নিচে একটি if/elif চেইন দিয়ে শেপের এরিয়া হিসাব করা একটি ফাংশন — নতুন শেপ যোগ করতে হলে এই ফাংশনটাই
পরিবর্তন করতে হয় (OCP-লঙ্ঘন) — বনাম পলিমরফিজম ব্যবহার করা একটি সংস্করণ, যেখানে প্রতিটি শেপ নিজের
area() মেথড বাস্তবায়ন করে।
# --- 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))
Triangle ক্লাস যোগ করা হয়েছে, কিন্তু Shape, Circle,
Rectangle, বা total_area() — এদের একটিতেও একটি অক্ষর পরিবর্তন করতে হয়নি। এটাই OCP-এর
"এক্সটেনশনের জন্য উন্মুক্ত, মডিফিকেশনের জন্য বদ্ধ" — কোডে সরাসরি প্রমাণিত।
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 অর্জনের মূল
কৌশল।
অনুশীলন
-
চিন্তা করুন: একটি
OrderProcessorক্লাস যদি অর্ডার ভ্যালিডেশন, প্রাইসিং হিসাব, এবং একই সাথে ইমেইল কনফার্মেশন পাঠানোর কোড — সবকিছু একসাথে রাখে, তাহলে এটি SRP-এর সাপেক্ষে কী সমস্যা তৈরি করবে, এবং কীভাবে এটি ভাগ করা যেতে পারে?এটি SRP-লঙ্ঘন — তিনটি ভিন্ন কারণে বদলাতে পারে: ভ্যালিডেশন নিয়ম বদলালে, প্রাইসিং নীতি বদলালে, অথবা ইমেইল সার্ভিস/টেমপ্লেট বদলালে। যুক্তিসঙ্গত বিভাজন:
OrderValidator(শুধু ভ্যালিডেশন),PricingCalculator(শুধু প্রাইসিং), এবংOrderNotifier(শুধু ইমেইল/নোটিফিকেশন) — ঠিক এই লেসনেরEmployee-কে তিনটি ক্লাসে ভাগ করার একই প্যাটার্ন অনুসরণ করে। -
পরীক্ষা করুন: উপরের OCP কোড সেলে একটি নতুন
Squareক্লাস যোগ করুন (side প্যারামিটার নিয়ে,area()মেথডেside ** 2রিটার্ন করে), এটিshapesলিস্টে যোগ করুন, এবং Run চেপে দেখুনtotal_area()-এর কোনো পরিবর্তন ছাড়াই সঠিক নতুন যোগফল আসে কিনা।হ্যাঁ —
Square(5)যোগ করলে এরিয়া ২৫ যোগ হবে (৬০.২৭৪৪ + ২৫ = ৮৫.২৭৪৪), এবংtotal_area()ফাংশনে কোনো পরিবর্তন লাগবে না, কারণ এটি শুধুshape.area()কল করে, শেপের ধরন যাচাই করে না। এটি আবারও নিশ্চিত করেShapeহায়ারার্কি সত্যিকারেই এক্সটেনশনের জন্য উন্মুক্ত।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পাঠ ১৭ · SOLID প্রিন্সিপল — LSP ও ISP পরবর্তী পাঠ SOLID-এর পরের দুটি প্রিন্সিপল — সাবক্লাস সাবস্টিটিউশন ও ছোট, ফোকাসড ইন্টারফেস।
- পাঠ ১৪ · কাপলিং ও কোহেশন সরাসরি সম্পর্কিত SRP আসলে এই লেসনের "হাই কোহেশন" লক্ষ্যেরই একটি সুনির্দিষ্ট, কার্যকরযোগ্য রূপ।