পাঠ ১৭ · ৫৮-এর মধ্যে · মডিউল ৪

SOLID প্রিন্সিপল — LSP ও ISP

SOLID principles — LSP & ISP
৯ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • LSP-এর সুনির্দিষ্ট সংজ্ঞা এবং কেন এটি নিছক ইনহেরিটেন্স নয়, বরং আচরণগত প্রতিশ্রুতি
  • Rectangle/Square-এর মতো একটি জ্যামিতিকভাবে যৌক্তিক ইনহেরিটেন্স কীভাবে LSP ভাঙতে পারে — সত্যিকারের কোড চালিয়ে
  • ISP-এর সংজ্ঞা এবং "fat interface" ডিজাইন স্মেল চেনা
  • fat ইন্টারফেসকে ছোট, ফোকাসড ইন্টারফেসে ভেঙে ISP মেনে চলার প্র্যাকটিক্যাল কৌশল

১ · Liskov Substitution Principle (LSP) কী

Liskov Substitution PrincipleLSPসাবক্লাসের অবজেক্ট সুপারক্লাসের অবজেক্টের জায়গায় বসালে প্রোগ্রামের সঠিকতা অক্ষুণ্ণ থাকা উচিত — সাবক্লাসকে সুপারক্লাসের আচরণগত প্রতিশ্রুতি ভাঙা চলবে না। (LSP, SOLID-এর তৃতীয় অক্ষর) বলে — সুপারক্লাসের জন্য লেখা কোড যদি কোনো সাবক্লাসের অবজেক্ট দিয়ে চালানো হয়, তাহলেও সেই কোড সঠিকভাবে কাজ করা উচিত, কোনো পরিবর্তন ছাড়াই। এটি নিছক "সাবক্লাস তৈরি করা যায় কিনা" (সিনট্যাক্টিক ইনহেরিটেন্স) নয় — বরং সাবক্লাস সুপারক্লাসের আচরণগত প্রতিশ্রুতি বজায় রাখছে কিনা, তার প্রশ্ন (Concepts of Programming Languages কোর্সের সাবটাইপ পলিমরফিজম আলোচনা-র একটি সরাসরি গভীরকরণ)।

২ · ক্লাসিক উদাহরণ: Rectangle বনাম Square

LSP-এর সবচেয়ে বিখ্যাত উদাহরণ হলো Rectangle ও Square। জ্যামিতিকভাবে একটি স্কয়ার একটি রেক্টাঙ্গেলেরই বিশেষ রূপ ("is-a" সম্পর্ক সত্যি মনে হয়) — তাই স্বাভাবিকভাবেই মনে হতে পারে Square-কে Rectangle-এর সাবক্লাস বানানো উচিত। কিন্তু Rectangle-এর set_width() ও set_height() স্বাধীনভাবে কাজ করে — প্রস্থ বদলালে উচ্চতা অপরিবর্তিত থাকে। একটি স্কয়ারে সব বাহু সমান রাখতে হলে Square-এর set_width() ডাকলে উচ্চতাও বদলে যেতে হয় (নাহলে সেটা আর স্কয়ার থাকে না) — অর্থাৎ Square-কে এই দুই সেটার override করতে হয়, যা Rectangle-এর জন্য লেখা কোডের প্রত্যাশা ভেঙে দেয়।

নিচের কোড সেলে ঠিক এই সিকোয়েন্সটি — set_width(5); set_height(10); area() — সত্যিকারের Rectangle ও Square অবজেক্টের উপর চালিয়ে দেখা যাক।

Python
# LSP লঙ্ঘনের ক্লাসিক উদাহরণ -- Rectangle বনাম Square

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

    def set_width(self, width):
        self._width = width

    def set_height(self, height):
        self._height = height

    def area(self):
        return self._width * self._height


class Square(Rectangle):
    # স্কয়ারের ক্ষেত্রে সব বাহু সমান রাখতে দুটো setter-ই override করা হলো
    def set_width(self, width):
        self._width = width
        self._height = width

    def set_height(self, height):
        self._width = height
        self._height = height


def probe(shape_name, shape):
    shape.set_width(5)
    shape.set_height(10)
    result = shape.area()
    print(f"{shape_name}: set_width(5) -> set_height(10) -> area() = {result}")
    return result


print("একই কোড ('set_width(5); set_height(10); area()') দুই ধরনের অবজেক্টে চালানো হলো:\n")

rect = Rectangle(0, 0)
rect_area = probe("Rectangle", rect)

sq = Square(0, 0)
sq_area = probe("Square    ", sq)

print(f"\nপ্রত্যাশিত (Rectangle-এর জন্য যেভাবে কোড লেখা হয়েছিল): area == 50")
print(f"Rectangle প্রকৃত ফলাফল: {rect_area} -> {'ঠিক আছে' if rect_area == 50 else 'ভুল'}")
print(f"Square প্রকৃত ফলাফল:    {sq_area} -> {'প্রত্যাশিত মতোই' if sq_area == 50 else 'LSP লঙ্ঘন! (' + str(sq_area) + ' != 50)'}")

    
লক্ষ্য করুন — Rectangle-এর জন্য area() ঠিক ৫০ (৫ × ১০)। কিন্তু Square-এর জন্য একই কল-সিকোয়েন্স area() = ১০০ দেয় (১০ × ১০), কারণ set_height(10) চুপচাপ প্রস্থও ১০ করে দিয়েছে। যে কোড Rectangle ধরে নিয়ে লেখা হয়েছিল, সেই একই কোড Square দিয়ে চালালে ভুল ফলাফল দেয় — এটাই LSP লঙ্ঘনের নির্দিষ্ট, পরিমাপযোগ্য প্রমাণ, শুধু তাত্ত্বিক দাবি নয়।
তাহলে সমাধান কী?

Square-কে Rectangle-এর সাবক্লাস না বানিয়ে, দুটোকেই একটি সাধারণ Shape ইন্টারফেস থেকে স্বাধীনভাবে ইমপ্লিমেন্ট করা যেত (L16-এর OCP-কমপ্লায়েন্ট Shape হায়ারার্কির ধাঁচে) — অথবা Rectangle-কে অপরিবর্তনীয় (immutable) করে তৈরি করা যেত, যাতে "প্রস্থ বদলালে উচ্চতা অক্ষুণ্ণ থাকবে" এই প্রতিশ্রুতিটাই আর না থাকে। মূল শিক্ষা: ইনহেরিটেন্স ডিজাইন করার সময় শুধু "is-a" জ্যামিতিক সম্পর্ক নয়, বরং সুপারক্লাসের আচরণগত প্রতিশ্রুতি সাবক্লাস মেনে চলতে পারবে কিনা তা যাচাই করা জরুরি।

৩ · Interface Segregation Principle (ISP) কী

Interface Segregation PrincipleISPক্লায়েন্টকে এমন কোনো ইন্টারফেস/মেথডের উপর নির্ভর করতে বাধ্য করা উচিত নয় যা সে আসলে ব্যবহারই করে না — একটি বড় সাধারণ-উদ্দেশ্যের ইন্টারফেসের চেয়ে কয়েকটি ছোট, ফোকাসড ইন্টারফেস পছন্দনীয়। (ISP, SOLID-এর চতুর্থ অক্ষর) বলে — ক্লায়েন্ট ক্লাসকে এমন কোনো ইন্টারফেসের উপর নির্ভর করতে বাধ্য করা উচিত নয় যার সব মেথড সে ব্যবহার করে না। একটি বড়, "fat" (মোটা) ইন্টারফেস — যাতে অনেকগুলো ভিন্ন, অসম্পর্কিত মেথড একসাথে বাঁধা — প্রায়ই কোনো ক্লাসকে অপ্রাসঙ্গিক মেথডের জন্য অর্থহীন/স্টাব ইমপ্লিমেন্টেশন লিখতে বাধ্য করে — এটি একটি বাস্তব, সহজে চেনা যায় এমন ডিজাইন স্মেল।

fat ইন্টারফেস (খারাপ)
একটি Worker ইন্টারফেসে work() ও eat() দুটোই বাধ্যতামূলক — একটি RobotWorker-কে অর্থহীন eat() লিখতে হয়।
বিভক্ত ইন্টারফেস (ভালো)
Workable ও Eatable আলাদা — RobotWorker শুধু Workable ইমপ্লিমেন্ট করে, eat()-এর দরকারই নেই।

৪ · কোডে দেখা: Worker/Robot উদাহরণ

নিচের কোড সেলে প্রথমে fat FatWorker ইন্টারফেসের সমস্যা দেখানো হয়েছে — একটি RobotWorkerBad ক্লাসকে বাধ্য হয়ে eat() ইমপ্লিমেন্ট করতে হয়, যা কল করলে ব্যর্থ হয়। তারপর Workable ও Eatable-এ বিভক্ত ISP-কমপ্লায়েন্ট ভার্সন দেখানো হয়েছে, যেখানে RobotWorkerGood নিরাপদে শুধু Workable ইমপ্লিমেন্ট করে।

Python
from abc import ABC, abstractmethod

# --- ISP লঙ্ঘন: একটি "fat" ইন্টারফেস যাতে অপ্রাসঙ্গিক মেথডও বাধ্যতামূলক ---
class FatWorker(ABC):
    @abstractmethod
    def work(self):
        ...
    @abstractmethod
    def eat(self):
        ...

class HumanWorkerBad(FatWorker):
    def work(self):
        return "মানুষ কর্মী কাজ করছে"
    def eat(self):
        return "মানুষ কর্মী দুপুরের খাবার খাচ্ছে"

class RobotWorkerBad(FatWorker):
    def work(self):
        return "রোবট কর্মী কাজ করছে"
    def eat(self):
        # রোবট খায় না -- অর্থহীন স্টাব মেথড, ISP লঙ্ঘনের সরাসরি প্রমাণ
        raise NotImplementedError("রোবট eat() করতে পারে না -- এই মেথডটাই অর্থহীন")


# --- ISP-কমপ্লায়েন্ট: ছোট, আলাদা ইন্টারফেসে বিভক্ত ---
class Workable(ABC):
    @abstractmethod
    def work(self):
        ...

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        ...

class HumanWorkerGood(Workable, Eatable):
    def work(self):
        return "মানুষ কর্মী কাজ করছে"
    def eat(self):
        return "মানুষ কর্মী দুপুরের খাবার খাচ্ছে"

class RobotWorkerGood(Workable):
    # শুধু Workable ইমপ্লিমেন্ট করে -- eat() নিয়ে মিথ্যা প্রতিশ্রুতি দিতে হয় না
    def work(self):
        return "রোবট কর্মী কাজ করছে"


print("-- ISP লঙ্ঘন: RobotWorkerBad.eat() ডাকলে কী হয় --")
bad_robot = RobotWorkerBad()
print(bad_robot.work())
try:
    bad_robot.eat()
except NotImplementedError as e:
    print(f"এরর: {e}")

print("\n-- ISP-কমপ্লায়েন্ট: RobotWorkerGood-এর eat() নেই, প্রয়োজনও নেই --")
good_robot = RobotWorkerGood()
print(good_robot.work())
print("RobotWorkerGood শুধু Workable ইন্টারফেস ইমপ্লিমেন্ট করে -- eat() ধারণ করারই প্রয়োজন নেই।")
print(f"RobotWorkerGood is Eatable? {isinstance(good_robot, Eatable)}")

good_human = HumanWorkerGood()
print(f"HumanWorkerGood is Workable and Eatable? {isinstance(good_human, Workable) and isinstance(good_human, Eatable)}")

    
মূল কথা · Key takeaway

LSP নিশ্চিত করে সাবক্লাস সুপারক্লাসের আচরণগত প্রতিশ্রুতি ভাঙে না — Rectangle/Square প্রমাণ করে এটি নিছক তাত্ত্বিক ভয় নয়, বাস্তব বাগের উৎস। ISP নিশ্চিত করে কোনো ক্লাসকে এমন মেথড ইমপ্লিমেন্ট করতে বাধ্য করা না হয় যা তার জন্য প্রাসঙ্গিকই নয়। দুটোই মূলত L14-এর কাপলিং/কোহেশন লক্ষ্যেরই আরেকটু সুনির্দিষ্ট রূপ — পরের পাঠে (L18) SOLID-এর শেষ অক্ষর DIP দিয়ে এই সিরিজ সম্পূর্ণ হবে।

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

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

প্র ০১ জ্যামিতিকভাবে একটি স্কয়ার তো সত্যিই একটি রেক্টাঙ্গেল — তাহলে কেন Square extends Rectangle খারাপ ডিজাইন?

কারণ OOP-তে ইনহেরিটেন্স শুধু জ্যামিতিক/বাস্তব-জগতের "is-a" সম্পর্ক নয় — এটি একটি আচরণগত প্রতিশ্রুতি। Rectangle-এর ব্যবহারকারী কোড ধরে নেয় set_width() শুধু প্রস্থ বদলায়, উচ্চতা নয়। Square এই প্রতিশ্রুতি ভাঙতে বাধ্য (নাহলে সেটা স্কয়ার থাকে না)। তাই জ্যামিতিক সম্পর্ক সত্যি হলেও, প্রোগ্রামিং-এর "is-a" পরীক্ষা ভিন্ন — "সুপারক্লাসের জন্য লেখা সব কোড কি সাবক্লাসেও সঠিক থাকে?"

প্র ০২ উপরের কোড সেলে Rectangle-এর জন্য area() = ৫০ কিন্তু Square-এর জন্য area() = ১০০ হলো কেন, ধাপে ধাপে ব্যাখ্যা করুন।

Rectangle-এ: set_width(5) শুধু _width-কে ৫ করে, তারপর set_height(10) শুধু _height-কে ১০ করে — ফলাফল ৫ × ১০ = ৫০। কিন্তু Square-এ প্রতিটি সেটার প্রস্থ ও উচ্চতা দুটোই একসাথে বদলায় (স্কয়ারের সংজ্ঞা রক্ষা করতে) — তাই set_width(5)-এর পর প্রস্থ=উচ্চতা=৫, কিন্তু পরের set_height(10) কল দুটোকেই ১০ করে দেয় (প্রস্থ সহ) — শেষ ফলাফল ১০ × ১০ = ১০০, যদিও কলকারী আশা করেছিল ৫ প্রস্থ অক্ষুণ্ণ থাকবে।

প্র ০৩ ISP আর SRP (L16) — দুটোই তো "একটি জিনিস একটাই কাজ করুক" জাতীয় শোনায়। এই দুটোর মধ্যে সুনির্দিষ্ট পার্থক্য কী?

SRP প্রযোজ্য একটি ক্লাসের নিজের দায়িত্বের উপর — একটি ক্লাসের বদলানোর একটাই কারণ থাকা উচিত। ISP প্রযোজ্য ইন্টারফেস ডিজাইনের উপর — ক্লায়েন্ট ক্লাসকে অপ্রয়োজনীয় মেথডসহ একটি ইন্টারফেসে বাঁধা উচিত নয়। সম্পর্ক ঘনিষ্ঠ (দুটোই "একসাথে অনেক কিছু গুঁজে দেওয়া" এড়াতে চায়), কিন্তু SRP ক্লাসের ইমপ্লিমেন্টেশনের কথা বলে, ISP ইন্টারফেস/কনট্র্যাক্টের ডিজাইনের কথা বলে — যা ক্লায়েন্ট কোড কীভাবে একটি টাইপের সাথে ইন্টার‌অ্যাক্ট করে তা নিয়ন্ত্রণ করে।

অনুশীলন

  1. চিন্তা করুন: একটি Bird সুপারক্লাসে fly() মেথড থাকলে, এবং একটি Penguin সাবক্লাস (যে উড়তে পারে না) তৈরি করলে — এটি কি LSP লঙ্ঘন করবে? কীভাবে সমাধান করা যায় তা লিখুন।

    হ্যাঁ, এটি LSP লঙ্ঘন করবে — যে কোড ধরে নেয় যেকোনো Bird উড়তে পারে (কারণ সুপারক্লাসে fly() আছে), সেই কোড একটি Penguin দিয়ে চালালে ভুল আচরণ পাবে (হয় এরর, নয়তো অর্থহীন no-op)। সমাধান — ঠিক ISP-এর মতোই — fly()-কে একটি আলাদা FlyingBird ইন্টারফেসে সরিয়ে ফেলা, যা শুধু উড়তে-পারা পাখিরা (যেমন Sparrow) ইমপ্লিমেন্ট করবে; Penguin শুধু বেস Bird ইমপ্লিমেন্ট করবে, fly() নিয়ে মিথ্যা প্রতিশ্রুতি দেবে না।

  2. পরীক্ষা করুন: উপরের LSP কোড সেলে একটি তৃতীয় probe() কল যোগ করুন — একটি নতুন Rectangle(3, 3) অবজেক্টের উপর — এবং Run চেপে দেখুন এই কল কি Square-এর মতো সমস্যা দেখায়, নাকি Rectangle-এর মতো সঠিক থাকে।

    এই নতুন Rectangle(3, 3) অবজেক্টেও set_width(5); set_height(10); area() সঠিকভাবে ৫০-ই দেবে — কারণ এটি এখনও প্রকৃত Rectangle, শুরুর মান যাই হোক না কেন। এটি নিশ্চিত করে সমস্যাটা "প্রাথমিক মান সমান" এই কাকতালীয়তায় নয়, বরং Square-এর overridden সেটার মেথডের নিজস্ব আচরণে — যেখানেই Square ব্যবহৃত হবে, সেখানেই এই লঙ্ঘন ঘটবে।

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

আগের পাঠ
SOLID প্রিন্সিপল — SRP ও OCP