সরাসরি পর্বে যান
পর্ব ২৮ · ৭০-এর মধ্যে · মডিউল ৫

SOLID ২ — Liskov substitution, interface segregation আর dependency inversion

SOLID Part 2 — Liskov Substitution, Interface Segregation & Dependency Inversion

Taught in Bangla, this lesson covers the last three SOLID principles: Liskov substitution and behavioural subtyping rules on preconditions, postconditions and invariants, the Rectangle-Square and flightless bird problems, catching violations by running one test suite on base and subclass, splitting fat interfaces into role interfaces, inverting dependency arrows, and the difference between dependency inversion, injection and inversion of control.

এই পর্বে যা শিখবেন

  • Liskov-এর ১৯৮৭ সালের বক্তৃতা আর Liskov ও Wing-এর ১৯৯৪ সালের behavioural subtyping-এর নিয়মগুলো নিজের ভাষায় বলা
  • একটি subclass LSP ভাঙছে কি না, একই test suite base আর subclass-এ চালিয়ে ধরা
  • ভুল hierarchy সারানো — ভালো abstraction, ছোট interface বা composition দিয়ে
  • Fat interface চিনে role interface-এ ভাঙা, আর তার লাভ গুনে দেখানো
  • Dependency-র তীর উল্টে policy-কে detail থেকে মুক্ত করা, আর কখন এটি বাড়াবাড়ি তা বিচার করা

১ · SOLID-এর বাকি তিন অক্ষর — কোথা থেকে শুরু

পর্ব ২৭-এ SOLID-এর পরিচয় আর প্রথম দুই অক্ষর দেখেছেন: S — single responsibility (একটি module বদলানোর একটিই কারণ), আর O — open-closed (নতুন আচরণ যোগ হবে নতুন কোড লিখে, পুরোনো কোড না ভেঙে)। Open-closed-এর একটি গোপন শর্ত ছিল: আমরা ধরে নিয়েছিলাম, নতুন subclass বা নতুন বাস্তবায়ন বসালে পুরোনো কোড ঠিকঠাক চলবে। এই পর্বের তিন নীতি সেই শর্তকে পাকা করে।

L — Liskov substitution

Subtype কি সত্যিই base type-এর জায়গায় বসতে পারে? Inheritance-এর প্রতিশ্রুতির নিয়ম।

প্রশ্ন: “বদলি কি আসলের মতো আচরণ করে?”

I — Interface segregation

একটি interface কত বড় হবে? Client যা চায়, শুধু সেটুকু।

প্রশ্ন: “আমি কোন জিনিসের সাথে অকারণে বাঁধা?”

D — Dependency inversion

কে কার ওপর নির্ভর করবে? Business নিয়ম database-এর ওপর নয়, উল্টোটা।

প্রশ্ন: “তীরটি কোন দিকে?”

তিনটিই আসলে পর্ব ২৫-এর coupling-এর গল্প: কোন অংশ কোন অংশের কতটুকু জানে, আর একটি বদলালে কতগুলো ভাঙে। LSP ভুল inheritance-এর coupling ধরে, ISP অপ্রয়োজনীয় interface-coupling ছাঁটে, আর DIP coupling-এর দিক ঠিক করে। নিচে প্রতিটিকে প্রথমে সহজ উপমায়, তারপর সুনির্দিষ্ট নিয়মে, তারপর চালানো যায় এমন Python কোডে দেখব। Python-এর abc আর typing.Protocol নতুন মনে হলে Python কোর্সের ABC আর Protocol-এর পর্বটি পাশে খুলে রাখতে পারেন।

এই তিন নীতির উৎস ভিন্ন। LSP এসেছে গবেষণা থেকে — MIT-এর অধ্যাপক Barbara Liskov ১৯৮৭ সালের OOPSLA সম্মেলনে “Data Abstraction and Hierarchy” নামে keynote বক্তৃতা দেন (লিখিত রূপ ১৯৮৮ সালে ACM SIGPLAN Notices-এ প্রকাশিত)। ১৯৯৪ সালে তিনি ও Jeannette Wing ACM TOPLAS পত্রিকায় “A Behavioral Notion of Subtyping” লিখে ধারণাটিকে গাণিতিক রূপ দেন। ISP আর DIP এসেছে শিল্পের অভিজ্ঞতা থেকে — Robert C. Martin ১৯৯৬ সালের দিকে C++ Report পত্রিকার কয়েকটি লেখায় এগুলো (আর LSP-র ব্যবহারিক রূপ) ব্যাখ্যা করেন; পরে তাঁর Agile Software Development: Principles, Patterns, and Practices (২০০২) বইয়ে একসাথে আসে। “SOLID” আদ্যক্ষরটি সাধারণত Michael Feathers-এর দেওয়া বলে ধরা হয়, ২০০০-এর দশকের শুরুতে। Liskov ২০০৮ সালের Turing Award পান — data abstraction আর CLU ভাষার মতো কাজের জন্য।

২ · Liskov substitution — বদলি যেন আসলের প্রতিশ্রুতি রাখে

ঢাকা থেকে রাজশাহীর একটি বাসের নিয়মিত চালক অসুস্থ, তাই একজন বদলি চালক এলেন। যাত্রীরা তাঁকে চেনেন না, চেনার দরকারও নেই — তাঁরা টিকিটে লেখা প্রতিশ্রুতিতে ভরসা করেন: একই রুট, একই স্টপেজ, মোটামুটি একই সময়। বদলি চালক যদি বেশি দক্ষ হন, আধা ঘণ্টা আগে পৌঁছান — কারও আপত্তি নেই (প্রতিশ্রুতির চেয়ে বেশি দেওয়া)। কিন্তু তিনি যদি বলেন “আমি শুধু রিজার্ভ টিকিটের যাত্রী নেব” (যাত্রীর কাছে বেশি দাবি), বা নাটোরে না থেমে সোজা চলে যান (প্রতিশ্রুতি কম রাখা) — তখন তিনি আর “বদলি” নন, অন্য একটি সার্ভিস। LSP ঠিক এই কথাটি কোডের জন্য বলে।

Liskov তাঁর ১৯৮৭ সালের বক্তৃতায় একটি “substitution property”-র কথা বলেন, যার সারকথা: যদি type T-এর জন্য লেখা প্রতিটি প্রোগ্রাম P-তে T-এর object-এর বদলে S-এর object বসালে P-এর আচরণ না বদলায়, তবে S হলো T-এর subtype. Liskov ও Wing ১৯৯৪ সালে একে আরও নির্দিষ্ট করেন: T-এর object সম্পর্কে যে বৈশিষ্ট্য প্রমাণ করা যায়, S-এর object-এর ক্ষেত্রেও তা সত্য হতে হবে। আজ প্রচলিত ভাষায় — Liskov substitution principle: S যদি T-এর subtype হয়, তবে প্রোগ্রামের কাঙ্ক্ষিত বৈশিষ্ট্য (ঠিক ফল, কাজ সম্পূর্ণ হওয়া ইত্যাদি) না বদলে T-এর object-এর জায়গায় S-এর object বসানো যাবে।

লক্ষ করুন, কথাটি inheritance নিয়ে নয়, আচরণ নিয়ে। Compiler বা type checker দেখে method-এর নাম আর parameter মেলে কি না; কিন্তু “width বদলালে height বদলাবে না” — এমন প্রতিশ্রুতি কোনো type checker দেখে না। এজন্যই এর নাম behavioural subtyping. প্রতিশ্রুতিগুলো লেখার ভাষা আসে Bertrand Meyer-এর design by contract থেকে (তাঁর Object-Oriented Software Construction, ১৯৮৮): প্রতিটি method-এর precondition (ডাকার আগে caller-কে যা নিশ্চিত করতে হবে), postcondition (শেষে method যা নিশ্চিত করবে), আর class-এর invariant (সবসময় সত্য শর্ত)। চুক্তি লেখার পূর্ণ পদ্ধতি আর error handling-এর সাথে তার সম্পর্ক পর্ব ৩১-এ আসবে; এখানে শুধু LSP-র জন্য যে নিয়মগুলো লাগে সেগুলো দেখি।

নিয়মমানেলঙ্ঘনের উদাহরণ (কাল্পনিক অনলাইন বাজার)
Precondition কড়া করা যাবে নাSubtype caller-এর কাছে base-এর চেয়ে বেশি দাবি করবে না; সমান বা কম দাবি চলবেBase discount(amount) যেকোনো অঋণাত্মক amount নেয়; subclass বলে “১০০ টাকার কম হলে error”
Postcondition দুর্বল করা যাবে নাSubtype base-এর প্রতিশ্রুতির সবটা রাখবে; বেশি দিলে চলবেBase প্রতিশ্রুতি দেয় ছাড় কখনো দামের চেয়ে বেশি হবে না; subclass ৫০ টাকা “cashback” যোগ করে দামের চেয়ে বড় ছাড় দেয়
Invariant রক্ষাBase-এর সবসময়-সত্য শর্ত subtype-এও সবসময় সত্যWallet-এর balance কখনো ঋণাত্মক নয়; subclass ফি কেটে balance ঋণাত্মক করে দেয়
History constraintBase-এর object যেভাবে বদলাতে পারে না, subtype-এর নতুন method দিয়েও তা হবে নাOrder number শুধু বাড়ে; subclass-এ reset() যোগ করে নম্বর আবার ১ থেকে শুরু
নতুন ধরনের exception নয়Subtype শুধু সেই exception (বা তার subtype) ছুড়বে, যা base ঘোষণা করেছেBase save() শুধু StorageError ছোড়ে; subclass হঠাৎ PermissionError ছুড়ে caller-এর except ফাঁকি দেয়

একটি সহজ মুখস্থ-সূত্র Meyer-এর subcontracting থেকে: “বেশি চাইবেন না, কম দেবেন না।” Subclass হলো ঠিকাদারের অধীনে সাব-ঠিকাদার; মালিক মূল চুক্তি দেখে টাকা দিয়েছেন, তাই সাব-ঠিকাদার নতুন শর্ত চাপাতে পারেন না, আর কাজও কম করতে পারেন না। Liskov ও Wing-এর গবেষণাপত্রে signature-এর দুটি নিয়মও আছে — parameter-এর type subtype-এ সমান বা আরও সাধারণ হতে পারে (contravariance), আর return type সমান বা আরও নির্দিষ্ট (covariance) — কিন্তু এগুলো type checker অনেকটা নিজেই ধরে। আসল বিপদ আচরণের নিয়মে, যা কেউ ধরে না।

মূল ধারণা

History constraint সবচেয়ে সূক্ষ্ম। ধরুন OrderCounter-এর একটিই method, next(), যা নম্বর এক বাড়ায় — তাই “নম্বর কখনো কমে না” এর ইতিহাসের বৈশিষ্ট্য। একটি subclass reset() যোগ করল। next()-এর precondition, postcondition সব ঠিক আছে, কোনো পুরোনো method বদলায়নি — তবু যে কোড ধরে নিয়েছিল “নম্বর অনন্য”, সে এখন দুটি অর্ডারে একই নম্বর পাবে। নতুন method যোগ করা নিজে দোষ নয়; দোষ হলো এমন method যোগ করা, যা base-এর ইতিহাস-প্রতিশ্রুতি ভাঙে। এটিই কারণ, কেন একটি mutable (বদলানো যায় এমন) subtype সাধারণত একটি immutable (অপরিবর্তনীয়) type-এর subtype হতে পারে না।

৩ · Rectangle, Square আর উড়তে না-পারা পাখি

সবচেয়ে বিখ্যাত উদাহরণ Martin-এর ১৯৯৬ সালের লেখা থেকে জনপ্রিয়। গণিতে প্রতিটি বর্গ একটি আয়ত — তাহলে class Square(Rectangle) লেখা তো স্বাভাবিক! সমস্যা হলো, কোডের Rectangle গণিতের আয়ত নয়; তার set_width আর set_height method আছে, আর তাদের একটি অলিখিত প্রতিশ্রুতি: width বদলালে height যেমন ছিল তেমন থাকবে। Square বর্গ থাকতে চাইলে এই প্রতিশ্রুতি রাখতে পারে না। নিচের stepper-এ একটি test একই কাজ দুই object-এ করছে — “পরে” চাপুন আর মানগুলো দেখুন।

1def test_resize(shape):2    shape.set_width(4)3    shape.set_height(5)4    assert shape.area() == 2056test_resize(Rectangle(2, 3))7test_resize(Square(3))
Object-এর অবস্থা
object
w
h
area()
প্রতিশ্রুতি
  1. একটি Rectangle(2, 3). w = 2, h = 3, ক্ষেত্রফল ৬। Test-টি লেখা হয়েছে Rectangle-এর চুক্তি মেনে — লেখক Square-এর কথা জানেনই না।
  2. set_width(4). w হলো ৪, h আগের মতো ৩ — প্রতিশ্রুতি রক্ষা। ক্ষেত্রফল ১২।
  3. set_height(5). h হলো ৫, w রইল ৪। ক্ষেত্রফল ৪ × ৫ = ২০।
  4. Assert পাস। ২০ == ২০, Rectangle-এর জন্য test সফল।
  5. এবার একই test-এ Square(3). Square তো Rectangle-এর subclass, তাই Python খুশিমনে নিয়ে নিল। w = h = ৩, ক্ষেত্রফল ৯।
  6. set_width(4) — প্রথম ফাটল। বর্গ থাকতে Square-এর override w আর h দুটিকেই ৪ করল। “Height বদলাবে না” — Rectangle-এর এই postcondition ভেঙে গেল, যদিও কোনো error আসেনি। ক্ষেত্রফল ১৬।
  7. set_height(5). আবার দুটিই বদলাল: w = h = ৫। Test যে width ৪ রেখেছিল, তা হারিয়ে গেছে। ক্ষেত্রফল ২৫।
  8. Assert ব্যর্থ: ২৫ ≠ ২০। Square একা ঠিকই আছে — সে নিখুঁত বর্গ। দোষ সম্পর্কে: Rectangle-এর জায়গায় বসে সে Rectangle-এর প্রতিশ্রুতি রাখতে পারেনি। LSP-র ভাষায়, Square এই Rectangle-এর behavioural subtype নয়।

এবার আসল কোডে একই জিনিস, আর তার একটি সমাধান। সমাধানের মূল চিন্তা: সমস্যাটি “বর্গ” নয়, “setter-এর প্রতিশ্রুতি”। দুটিকেই একটি সাধারণ abstraction Shape-এর নিচে রাখি, যা শুধু সেই প্রতিশ্রুতি দেয় যা দুজনেই রাখতে পারে — area()। আর আকার বদলানোর বদলে নতুন object ফেরত দিই (immutable).

class Rectangle:
    def __init__(self, w, h): self.w, self.h = w, h
    def set_width(self, w): self.w = w      # promise: height unchanged
    def set_height(self, h): self.h = h     # promise: width unchanged
    def area(self): return self.w * self.h

class Square(Rectangle):
    def __init__(self, side): super().__init__(side, side)
    def set_width(self, w): self.w = self.h = w   # keeps it square
    def set_height(self, h): self.w = self.h = h

def test_resize(shape):              # written against Rectangle's contract
    shape.set_width(4)
    shape.set_height(5)
    got = shape.area()
    print(f"{type(shape).__name__:9} area={got}", "PASS" if got == 20 else "FAIL (expected 20)")
test_resize(Rectangle(2, 3))
test_resize(Square(3))
# Part 2: the fix. A common abstraction that promises only what both keep.
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Shape(ABC):
    @abstractmethod
    def area(self): ...
@dataclass(frozen=True)
class Rect(Shape):
    w: int; h: int
    def area(self): return self.w * self.h
    def with_width(self, w): return Rect(w, self.h)    # a NEW Rect
@dataclass(frozen=True)
class Sq(Shape):
    side: int
    def area(self): return self.side ** 2
shapes = [Rect(2, 3).with_width(4), Sq(3)]
print([f"{s} -> {s.area()}" for s in shapes], "total", sum(s.area() for s in shapes))

আউটপুট:

Rectangle area=20 PASS
Square    area=25 FAIL (expected 20)
['Rect(w=4, h=3) -> 12', 'Sq(side=3) -> 9'] total 21

প্রথম দুই লাইন stepper-এর ফল: Rectangle পাস, Square একই test-এ ২৫ পেয়ে ফেল। লক্ষ করুন, Python কোনো error দেয়নি — LSP লঙ্ঘন ধরা পড়ে শুধু আচরণের test-এ। সমাধানের অংশে Rect আর Sq ভাই-ভাই, বাবা-ছেলে নয়: দুজনেই Shape-এর abstract base class থেকে আসে, যা শুধু area()-র প্রতিশ্রুতি দেয়। Rect(2, 3).with_width(4) পুরোনো object বদলায় না, নতুন Rect(w=4, h=3) দেয় (ক্ষেত্রফল ১২); Sq(3)-এর ক্ষেত্রফল ৯; মোট ২১। Shape-এর তালিকায় যেকোনোটি বসালে area()-র প্রতিশ্রুতি অটুট থাকে। আর Sq-এ with_width নেই বলেই কেউ ভুল করে বর্গকে আয়তের মতো টানতে পারে না। (জায়গা বাঁচাতে কয়েকটি method এক লাইনে লেখা।)

পাখি সবাই ওড়ে?

দ্বিতীয় ক্লাসিক উদাহরণ: Bird class-এ fly() আছে, কারণ প্রথম যে পাখিগুলো মডেল করা হয়েছিল — কাক, শালিক, চড়ুই — সবাই ওড়ে। তারপর এল পেঙ্গুইন বা উটপাখি:

class Bird:
    def fly(self): return "flying"

class Penguin(Bird):
    def fly(self): raise NotImplementedError("penguins can't fly")

def migrate(flock):                       # written for Bird
    return [b.fly() for b in flock]       # crashes if a Penguin sneaks in

Penguin.fly()-এ NotImplementedError ছোড়া মানে precondition কড়া করা (“আমাকে ওড়াতে ডাকবেন না”) বা নতুন exception আনা — দুইভাবেই LSP লঙ্ঘন। ভুলটি পেঙ্গুইনের নয়, Bird-এর: সে এমন প্রতিশ্রুতি দিয়েছিল (“সব পাখি ওড়ে”) যা বাস্তবে সব পাখির নয়। Fowler-এর Refactoring বইয়ে এই গন্ধের নাম refused bequest — উত্তরাধিকারে পাওয়া জিনিস subclass ফিরিয়ে দিচ্ছে। সমাধান পরের অংশে।

৪ · LSP লঙ্ঘন চেনা, ধরা আর সারানো

বাস্তব কোডে লঙ্ঘন খুব কম সময় “Square” নামে আসে। কয়েকটি লক্ষণ মনে রাখলে code review-তে (পর্ব ৪৪) চোখে পড়বে:

  • Client-এ type পরীক্ষা: if isinstance(p, CashOnDelivery): ... বা if type(shape) == Square. Caller-কে যদি জানতে হয় ভেতরে কোন subclass, তবে substitution কাজ করছে না — আর প্রতিটি নতুন subclass-এ এই if বাড়বে (open-closed-ও ভাঙল)।
  • Override যা কাজ ফিরিয়ে দেয়: raise NotImplementedError, খালি pass, বা অর্থহীন মান (return 0, return None) — যেখানে base অর্থপূর্ণ কাজের প্রতিশ্রুতি দিয়েছিল। (খেয়াল রাখুন: abstract base-এ NotImplementedError স্বাভাবিক — সেখানে base নিজেই কোনো প্রতিশ্রুতি দেয় না; সমস্যা হলো concrete subclass-এ কাজ “ফিরিয়ে দেওয়া”।)
  • Subclass-এর জন্য test বাদ: “এই test FeeWallet-এর জন্য skip করুন” — অর্থাৎ base-এর চুক্তির কিছু অংশ সে মানে না।
  • Documentation-এ সতর্কবার্তা: “এই subclass-এ refund() ডাকবেন না।” চুক্তি যদি সতর্কবার্তায় বাঁচে, তবে সেটি চুক্তি নয়।

লক্ষণ দেখে সন্দেহ করা যায়, কিন্তু প্রমাণ আসে test থেকে। সবচেয়ে কার্যকর কৌশল: base type-এর চুক্তির জন্য একটি test suite লিখুন, আর সেটি প্রতিটি subtype-এ চালান। কেউ একে “contract test” বা “abstract test case” বলেন। শর্ত একটাই — input নিতে হবে base-এর precondition মেনে, আর যাচাই করতে হবে base-এর postcondition দিয়ে, subclass-এর নিজের নিয়ম দিয়ে নয়।

# Run the SAME contract suite against a base class and its subclasses.
class Discount:                       # contract of discount(amount):
    def discount(self, amount):       #  pre : amount >= 0 (any amount, even 0)
        return amount * 0.10          #  post: 0 <= result <= amount

class FestivalDiscount(Discount):     # same pre, stronger post: allowed
    def discount(self, amount):
        return min(amount * 0.20, 500)

class MinOrderDiscount(Discount):     # demands more from the caller
    def discount(self, amount):
        if amount < 100: raise ValueError("min order 100")
        return amount * 0.15

class CashbackDiscount(Discount):     # promises less to the caller
    def discount(self, amount):
        return amount * 0.10 + 50      # can exceed the amount itself

INPUTS = [0, 40, 99, 100, 2500, 10000]  # all satisfy the BASE precondition

def check(cls):
    obj, problems = cls(), []
    for a in INPUTS:
        try:
            r = obj.discount(a)
        except Exception as e:
            problems.append(f"pre strengthened at {a}: {type(e).__name__}")
            continue
        if not 0 <= r <= a:
            problems.append(f"post weakened at {a}: got {r:g}")
    print(f"{cls.__name__:17}", "OK" if not problems else f"{len(problems)} broken: {problems[:2]}")

for cls in (Discount, FestivalDiscount, MinOrderDiscount, CashbackDiscount):
    check(cls)

আউটপুট:

Discount          OK
FestivalDiscount  OK
MinOrderDiscount  3 broken: ['pre strengthened at 0: ValueError', 'pre strengthened at 40: ValueError']
CashbackDiscount  2 broken: ['post weakened at 0: got 50', 'post weakened at 40: got 54']

Discount নিজের চুক্তি মানে — প্রত্যাশিত। FestivalDiscount ২০% দেয়, সর্বোচ্চ ৫০০ টাকা; ফল base-এর চেয়ে আলাদা, তবু OK — কারণ চুক্তি ছিল “০ থেকে দামের মধ্যে”, ঠিক ১০% নয়। ফল আলাদা হওয়া polymorphism-এর উদ্দেশ্যই; প্রশ্ন শুধু প্রতিশ্রুতি রক্ষা হলো কি না। MinOrderDiscount ০, ৪০ আর ৯৯ — তিনটি বৈধ input-এ ValueError ছুড়েছে: precondition কড়া করা (প্রথম দুটি দেখানো হয়েছে)। CashbackDiscount ০ টাকায় ৫০ আর ৪০ টাকায় ৫৪ টাকা ছাড় দিয়েছে, অর্থাৎ ছাড় দামের চেয়ে বেশি: postcondition দুর্বল। মজার ব্যাপার, ৯৯ থেকে ওপরের দামে Cashback ঠিক চলে — test-এ শুধু বড় অর্ডার থাকলে লঙ্ঘন ধরাই পড়ত না। তাই contract test-এর input-এ base-এর precondition-এর সীমার মান (০, ছোট সংখ্যা) রাখতে হয়; সীমার মান বেছে নেওয়ার নিয়ম পর্ব ৪৭-এ আসবে।

সারানোর তিন পথ

১ · ভালো hierarchy

Base-এর প্রতিশ্রুতি ছোট করুন, যাতে সবাই রাখতে পারে। Bird-এ fly নয়; ওড়ার ক্ষমতা আলাদা FlyingBird বা Flyer ভূমিকায়। migrate() নেবে শুধু Flyer.

Shape → Rect, Sq

২ · Composition

“Is-a” নয়, “has-a”: Stack-কে list-এর subclass না বানিয়ে ভেতরে একটি list রাখুন, আর শুধু push/pop দেখান। Composition উত্তরাধিকারের অপ্রয়োজনীয় প্রতিশ্রুতি বয়ে আনে না।

বিস্তারিত: পর্ব ৩৭

৩ · চুক্তি প্রকাশ্যে

যদি সত্যিই কিছু subtype একটি কাজ না পারে, base-এর চুক্তিতেই সেটি লিখুন: can_refund() জিজ্ঞেস করা যায়, বা ফল হিসেবে “refund সম্ভব নয়” ফেরত আসে। তখন caller প্রস্তুত থাকে।

শেষ উপায়, প্রথম নয়

একটি প্রচলিত ভুল “সমাধান”: Square-এর set_width-এ raise ValueError("use set_side") লেখা। এতে বর্গ বাঁচে, কিন্তু LSP আরও খারাপভাবে ভাঙে — নতুন exception, যা Rectangle-এর caller কখনো আশা করেনি। ভুলটি ঢাকা পড়ে, সারে না। নিয়মটি মনে রাখুন: subclass-এর ভেতরে পাহারা বসিয়ে LSP সারানো যায় না; সারাতে হয় base-এর প্রতিশ্রুতি বা hierarchy বদলে। Inheritance আর composition-এর সাধারণ তুলনা Python কোর্সের inheritance আর composition পর্বেও দেখতে পারেন।

৫ · Interface segregation — যা লাগে না, তার সাথে বাঁধা নয়

পাড়ার একটি ফটোকপির দোকানে একটিমাত্র খাতা: সব অর্ডার — ফটোকপি, স্ক্যান, ল্যামিনেশন, বাঁধাই, ফ্যাক্স — এক পাতায়, এক ছকে। ছক বদলালে (ধরুন ল্যামিনেশনে নতুন ঘর যোগ হলো), দোকানের সব কর্মচারীকে নতুন ছক শিখতে হয়, যাঁরা জীবনে ল্যামিনেশন করেন না তাঁদেরও। আলাদা কাজের জন্য আলাদা ছোট স্লিপ থাকলে ল্যামিনেশনের বদল শুধু ল্যামিনেশনের মানুষকে ছোঁয়।

Martin-এর ভাষায় interface segregation principle: client-কে এমন method-এর ওপর নির্ভর করতে বাধ্য করা উচিত নয়, যা সে ব্যবহার করে না। একটি interface-এ অনেক ভিন্ন ধরনের client-এর method জমে গেলে তাকে বলে fat interface. সমাধান — client-এর প্রয়োজন অনুযায়ী ছোট role interface: “যে ছাপায়”, “যে স্ক্যান করে” — প্রতিটি একটি ভূমিকা।

Martin তাঁর ২০০২ সালের বইয়ে ISP-র জন্মের একটি গল্প বলেন — তাঁর নিজের বর্ণনা অনুযায়ী, Xerox-এর একটি নতুন printer system-এ পরামর্শক হিসেবে কাজের সময়। সেখানে প্রায় সব কাজ (ছাপা, staple করা ইত্যাদি) একটিই বড় Job class ব্যবহার করত; ফলে যেকোনো ছোট বদলে প্রায় সবকিছু নতুন করে build করতে হতো, আর বর্ণনা অনুযায়ী একেকটি build-এ ঘণ্টাখানেক লাগত। সমাধান ছিল প্রতিটি কাজের জন্য আলাদা interface (যেমন staple-এর জন্য একটি, print-এর জন্য একটি), যা Job বাস্তবায়ন করে। গল্পের খুঁটিনাটি এক উৎসেরই — Martin-এর নিজের — তাই একে শিক্ষণীয় গল্প হিসেবে নিন, ইতিহাসের নথি হিসেবে নয়।

Fat interface-এর তিনটি বাস্তব খরচ:

  1. Recompile আর redeploy: C++ বা Java-র মতো compiled ভাষায় interface-এর একটি method বদলালে যত module সেই interface ব্যবহার করে, সবাইকে আবার compile, test আর হয়তো deploy করতে হয় — যারা ওই method কখনো ডাকে না তাদেরও। পর্ব ১৯-এর affected build-এর ভাষায়, fat interface “প্রভাবিত” তালিকা অকারণে লম্বা করে। Python বা JavaScript-এ compile-এর খরচ কম, কিন্তু review, test আর পর্ব ১৮-এর breaking change-এর ঝুঁকি একই।
  2. Stub-এর বোঝা: যে class শুধু একটি কাজ পারে, তাকেও বাকি সব method লিখতে হয় — প্রায়ই NotImplementedError দিয়ে। মানে fat interface সরাসরি LSP লঙ্ঘন তৈরি করে!
  3. Test double-এর বোঝা: Test-এ নকল বানাতে (test double) সব method-এর stub লিখতে হয়, যদিও test শুধু একটি ছোঁয়। কোন double কখন — পর্ব ৪৯-এ।
  1. Machine৪টি methodসবার জন্য এক
  2. ভূমিকা খোঁজাকে কী ডাকেclient ধরে ধরে
  3. Role interfacePrinter · Scanner · Fax · Staplerএক কাজ, এক interface
কাল্পনিক অফিসের software
InvoiceClerk
ArchiveBot
FrontDesk
BasicPrinter-এর stub
fax()-এর বদলে প্রভাবিত
  1. একটি fat interface. Machine-এ চারটি method: print_doc, scan, fax, staple. তিনটি client — হিসাবরক্ষক শুধু ছাপায়, archive bot শুধু স্ক্যান করে, অভ্যর্থনা দুটোই — কিন্তু সবাই চারটির সাথে বাঁধা। সাধারণ printer-কে ৩টি stub লিখতে হয়।
  2. fax()-এর signature বদলাল। একটি cover page parameter যোগ হলো। কেউ fax ডাকে না, তবু তিন client আর দুই machine class (BasicPrinter, বহু-কাজের OfficeMFP) — মোট ৫টি module আবার build, review আর test করতে হবে।
  3. Client ধরে ভূমিকা খোঁজা। Interface-কে “যন্ত্র কী পারে” দিয়ে নয়, “client কী চায়” দিয়ে ভাগ করুন। তিন client-এর ব্যবহার থেকে দুটি ভূমিকা স্পষ্ট: Printer আর Scanner; fax আর staple-এর নিজস্ব ভূমিকা।
  4. Role interface. চারটি ছোট interface. OfficeMFP চারটিই বাস্তবায়ন করে; BasicPrinter শুধু Printer — stub শূন্য। প্রতিটি client ঠিক যা ব্যবহার করে, শুধু তার ওপর নির্ভর করে।
  5. একই বদল আবার। এবার fax() বদলালে প্রভাবিত শুধু Fax interface আর তার একমাত্র বাস্তবায়ন OfficeMFP — ৫টি module থেকে ১টি। ISP-র লাভ এই সংখ্যাতেই।

নিচের প্রোগ্রাম একই হিসাব চালিয়ে দেখায়। Machine.__abstractmethods__ হলো Python-এর রাখা abstract method-এর set; তা থেকে প্রতিটি client কতগুলোর ওপর নির্ভর করে আর printer-কে কতগুলো stub লিখতে হয়, গোনা হয়েছে। শেষে typing.Protocol দিয়ে একটি role interface — structural typing: TinyPrinter কোনো কিছু inherit করে না, শুধু print_doc আছে বলেই Printer-এর চুক্তি মানে।

from abc import ABC, abstractmethod
from typing import Protocol

class Machine(ABC):                     # the fat interface
    @abstractmethod
    def print_doc(self, doc): ...
    @abstractmethod
    def scan(self): ...
    @abstractmethod
    def fax(self, doc, number): ...
    @abstractmethod
    def staple(self, doc): ...

class BasicPrinter(Machine):            # can only print, must "implement" 4
    def print_doc(self, doc): return f"printed {doc}"
    def scan(self): raise NotImplementedError
    def fax(self, doc, number): raise NotImplementedError
    def staple(self, doc): raise NotImplementedError

uses = {"InvoiceClerk": {"print_doc"}, "ArchiveBot": {"scan"},
        "FrontDesk": {"print_doc", "scan"}}
fat = Machine.__abstractmethods__
print("fat interface:", len(fat), "methods")
for client, used in uses.items():
    print(f"  {client:12} uses {len(used)}, depends on {len(fat)}")
stubs = [m for m in fat if m != "print_doc"]
print("BasicPrinter stubs:", len(stubs))
class Printer(Protocol):                # role interfaces: one job each
    def print_doc(self, doc) -> str: ...

class TinyPrinter:                      # no Machine, no stubs
    def print_doc(self, doc): return f"printed {doc}"

def clerk(p: Printer): return p.print_doc("invoice-17")
print(clerk(TinyPrinter()), "| stubs needed now: 0")

আউটপুট:

fat interface: 4 methods
  InvoiceClerk uses 1, depends on 4
  ArchiveBot   uses 1, depends on 4
  FrontDesk    uses 2, depends on 4
BasicPrinter stubs: 3
printed invoice-17 | stubs needed now: 0

চারটি method-এর interface-এ তিন client মিলে ব্যবহার করে ১ + ১ + ২ = ৪টি নির্ভরতা, কিন্তু বাঁধা ৪ + ৪ + ৪ = ১২টিতে — ৮টি অকারণ নির্ভরতা। BasicPrinter-কে ৩টি stub লিখতে হয়েছে, আর প্রতিটি stub একটি ছোট LSP লঙ্ঘন (কেউ Machine ভেবে scan() ডাকলে crash). শেষ লাইনে TinyPrinter কোনো stub ছাড়াই clerk()-এর কাজ করল। একটি সতর্কতা: Python চালানোর সময় Protocol-এর type যাচাই করে না — p: Printer শুধু একটি ঘোষণা, যা mypy বা pyright-এর মতো type checker যাচাই করে। Python-এ ISP-র লাভ তাই মূলত পরিষ্কার চুক্তি আর ছোট test double-এ; recompile-এর লাভ বেশি দেখা যায় C++, Java, C#-এ।

ISP মানে “এক method-এর interface” নয়। ভাগ হয় client-এর প্রয়োজন ধরে, আর একসাথে বদলায় এমন method একসাথেই থাকে (পর্ব ২৫-এর cohesion). open, read, close আলাদা interface-এ ছড়ালে লাভ নেই, কারণ যে পড়ে সে তিনটিই চায়। আর বড় interface নিজে দোষী নয় — Python-এর list-এ অনেক method, কিন্তু client-রা “list” ধারণাটিকেই চায়। দোষ হলো ভিন্ন ধরনের client-এর প্রয়োজন এক জায়গায় জোড়া।

৬ · Dependency inversion — তীরের দিক উল্টানো

বাড়িতে আপনার ফ্যান, ফ্রিজ, ল্যাপটপ — কোনোটি সরাসরি বিদ্যুৎকেন্দ্রের তারের সাথে ঝালাই করা নয়। সবাই একটি standard সকেটে plug লাগায়। সকেটের নিয়ম (কত ভোল্ট, কোন আকারের পিন) যন্ত্র আর বিদ্যুৎ সরবরাহ — দুই পক্ষই মানে। ফলে load-shedding-এর সময় একই সকেটে IPS বা জেনারেটরের লাইন এলে ফ্যান টেরও পায় না। আর সকেটের নিয়ম ঠিক করে যন্ত্র ব্যবহারকারীদের প্রয়োজন — কোনো একটি বিদ্যুৎকেন্দ্রের ইচ্ছা নয়।

Software-এ “ফ্যান” হলো high-level module বা policy — অর্ডার নেওয়ার নিয়ম, ছাড়ের হিসাব, ফলাফল প্রকাশের নিয়ম; software-এর আসল উদ্দেশ্য। “বিদ্যুৎকেন্দ্র” হলো low-level module বা detail — MySQL, SMS gateway, file system. স্বাভাবিকভাবে কোড লিখলে policy সরাসরি detail-কে ডাকে, তাই policy-র কোড detail-কে import করে: অর্ডারের নিয়ম MySQL-এর ওপর নির্ভরশীল। Martin-এর dependency inversion principle-এর দুটি অংশ:

ক. উচ্চ-স্তরের module নিম্ন-স্তরের module-এর ওপর নির্ভর করবে না; দুটিই abstraction-এর ওপর নির্ভর করবে।
খ. Abstraction খুঁটিনাটির ওপর নির্ভর করবে না; খুঁটিনাটি abstraction-এর ওপর নির্ভর করবে।

“Inversion” শব্দটির মানে: প্রচলিত, ওপর-থেকে-নিচে নকশায় source code-এর নির্ভরতার তীর যেদিকে যায়, DIP সেটি উল্টে দেয়। কিন্তু উল্টায় শুধু source code-এর তীর (কে কাকে import করে); চালানোর সময় call তখনও policy থেকে detail-এ যায়। আর এখানেই সবচেয়ে বেশি ভুল হয় — abstraction-এর মালিক কে? Repository interface যদি mysql package-এ থাকে, আর MySQL-এর ভাষায় লেখা হয় (execute_sql(query)), তবে policy এখনো detail-এর package-কে import করছে; তীর আসলে উল্টায়নি। DIP-এ abstraction থাকে policy-র package-এ, policy-র ভাষায় (save(order)) — সকেটের নিয়ম যন্ত্রের প্রয়োজনে লেখা।

DIP-এর আগে আর পরে — source code-এর নির্ভরতা

Dashed open arrow = UML dependency (uses); dashed line with hollow triangle = realization (implements)

DIP-এর আগে ও পরে নির্ভরতার তীর আগে · policy → detail OrderService policy · উচ্চ-স্তর import · uses MySQLRepository detail · নিম্ন-স্তর পরে · দুটিই → abstraction orders package — policy-র মালিকানা OrderService policy «interface» Repository save(order) চুক্তি লেখা policy-র ভাষায়, SQL-এর ভাষায় নয় MySQLRepository detail · production InMemoryRepository fake · test implements
ছবি ২৮.১ — ওপরে: policy সরাসরি detail-কে import করে, তাই MySQL বদলালে বা না থাকলে অর্ডারের নিয়মও আটকে যায়। নিচে: Repository interface থাকে policy-র package-এ; MySQL আর memory-র বাস্তবায়ন দুটিই ওপরের দিকে তীর দিয়ে সেই চুক্তি মানে। চালানোর সময় call তবু OrderService থেকে MySQLRepository-তে যায় — উল্টেছে শুধু source code-এর নির্ভরতা।
  1. OrderServicepolicyঅর্ডারের নিয়ম
  2. Repositorysave(order)policy-র package-এ
  3. MySQLRepositorydetaildatabase
  4. InMemoryRepositoryfaketest-এর জন্য
কে কাকে import করে, আর তার ফল
orders → mysql import?
mysql → orders import?
চালানোর সময় call
Unit test-এ database লাগে?
  1. শুরুর অবস্থা। OrderService নিজের ভেতরেই MySQLRepository() বানায়। তীর policy থেকে detail-এ: orders package mysql-কে import করে।
  2. খরচটা টের পাওয়া। একজন developer “শূন্য পরিমাণের অর্ডার বাতিল হবে” নিয়মটির test লিখতে গেলেন; কিন্তু তাঁর ল্যাপটপে MySQL চলছে না, তাই test ব্যবসার নিয়ম যাচাইয়ের আগেই ConnectionError-এ থেমে গেল।
  3. Policy-র ভাষায় চুক্তি। প্রশ্ন: OrderService আসলে কী চায়? শুধু “এই অর্ডারটি জমা রাখুন।” তাই orders package-এর ভেতরেই Repository protocol, একটি method save(order). OrderService এখন শুধু এটিকে চেনে; mysql-এর import উঠে গেল।
  4. তীর উল্টে গেল। MySQLRepository এখন Repository-র চুক্তি মানে — source code-এ তীর detail থেকে policy-র abstraction-এর দিকে। চালানোর সময় call কিন্তু আগের মতো Service থেকে MySQL-এ যায়।
  5. যুক্ত করার জায়গা। কে কোন বাস্তবায়ন পাবে, তা ঠিক হয় program-এর শুরুর কাছে একটি জায়গায় (composition root): production-এ OrderService(MySQLRepository()), test-এ OrderService(InMemoryRepository())। Database ছাড়াই নিয়মের test এখন চলে।

এবার stepper-এর গল্পটি চালানো যায় এমন কোডে। MySQLRepository এখানে ইচ্ছা করে ConnectionError ছোড়ে — যেন test চালানোর যন্ত্রে database নেই। Test-গুলো চালাতে একটি ছোট runner লেখা হয়েছে, যা ব্যর্থতা ধরে ছাপে কিন্তু প্রোগ্রাম থামায় না।

from typing import Protocol
class MySQLRepository:                     # low-level detail
    def save(self, order):
        raise ConnectionError("no MySQL server on this machine")

class OrderServiceV1:                      # policy creates its own detail
    def place(self, item, qty):
        MySQLRepository().save({"item": item, "qty": qty}); return "placed"

class Repository(Protocol):                # owned by the policy side
    def save(self, order: dict) -> None: ...

class OrderService:                        # depends only on Repository
    def __init__(self, repo: Repository): self.repo = repo
    def place(self, item, qty):
        if qty <= 0: raise ValueError("qty must be positive")
        self.repo.save({"item": item, "qty": qty}); return "placed"

class InMemoryRepository:                  # a fake, for tests
    def __init__(self): self.rows = []
    def save(self, order): self.rows.append(order)
def run(name, fn):
    try: fn(); print("PASS", name)
    except Exception as e: print("FAIL", name, "->", type(e).__name__, e)

def test_v1(): OrderServiceV1().place("tea", 2)
def test_saves_order():
    repo = InMemoryRepository()
    assert OrderService(repo).place("tea", 2) == "placed"
    assert repo.rows == [{"item": "tea", "qty": 2}]
def test_rejects_zero():
    repo = InMemoryRepository()
    try: OrderService(repo).place("tea", 0); assert False
    except ValueError: assert repo.rows == []
for t in (test_v1, test_saves_order, test_rejects_zero): run(t.__name__, t)

আউটপুট:

FAIL test_v1 -> ConnectionError no MySQL server on this machine
PASS test_saves_order
PASS test_rejects_zero

test_v1 ব্যর্থ — কিন্তু অর্ডারের নিয়মে কোনো ভুলের জন্য নয়, database না থাকায়। এমন test পরিবেশের ওপর নির্ভর করে, ধীর হয়, আর মাঝে মাঝে অকারণে ব্যর্থ হয় (flaky test, পর্ব ৫১)। OrderService constructor-এ যেকোনো Repository নেয়; test-এ দেওয়া হয়েছে InMemoryRepository — একটি fake, যা সত্যিই জমা রাখে, শুধু একটি list-এ। test_saves_order দেখল অর্ডার জমা হয়েছে, আর test_rejects_zero দেখল শূন্য পরিমাণে ValueError এসেছে এবং কিছু জমা হয়নি। লক্ষ করুন, InMemoryRepository Repository-কে inherit করেনি — Protocol আকার মিলিয়েই খুশি। এখানে repository-টি সরল রাখা হয়েছে; port আর adapter নামে এই ধারণার বড় রূপ পর্ব ৪০-এ আসবে।

৭ · Inversion, injection আর IoC — তিনটি আলাদা জিনিস

ইন্টারভিউতে সবচেয়ে বেশি গুলিয়ে যায় তিনটি নাম, কারণ তিনটিই “D” আর “I” দিয়ে, আর প্রায়ই একসাথে আসে। কিন্তু এরা ভিন্ন স্তরের কথা:

Dependency inversion (DIP)Dependency injection (DI)Inversion of control (IoC)
কীনকশার নীতিএকটি কৌশলএকটি বড় ধারণা
প্রশ্নSource code-এর নির্ভরতার তীর কোন দিকে, abstraction-এর মালিক কে?Object তার dependency কোথা থেকে পায় — নিজে বানায়, না বাইরে থেকে পায়?প্রোগ্রামের প্রবাহ কে চালায় — আপনার কোড, না একটি framework?
উদাহরণorders package-এ Repository, MySQL তা মানেOrderService(repo) — constructor-এ দেওয়াWeb framework আপনার handler ডাকে; GUI-র event loop; template method
একটি ছাড়া অন্যটি?হ্যাঁ — factory বা plugin registry দিয়েও বাস্তবায়ন বেছে নেওয়া যায়হ্যাঁ — concrete MySQLRepository inject করলে DI আছে, DIP নেইDI হলো IoC-এর একটি রূপ: dependency জোগাড়ের নিয়ন্ত্রণ বাইরে গেল

Dependency injection নামটি Martin Fowler-এর ২০০৪ সালের একটি প্রবন্ধ (“Inversion of Control Containers and the Dependency Injection pattern”) থেকে প্রচলিত; সেখানে তিনি বলেন, “inversion of control” কথাটি এত সাধারণ যে container-গুলোর কাজ বোঝাতে আরও নির্দিষ্ট নাম দরকার। Inversion of control-এর ধারণা পুরোনো — framework নিয়ে ১৯৮০-র দশকের লেখায় (যেমন Johnson ও Foote, ১৯৮৮) পাওয়া যায় বলে সাধারণত উল্লেখ করা হয়, আর এর জনপ্রিয় ডাকনাম “Hollywood principle”: আমাদের ডাকবেন না, আমরাই আপনাকে ডাকব। DI container (যেমন Java-র Spring) কীভাবে কাজ করে, constructor বনাম setter injection, আর inheritance-এর বদলে composition — এসব পর্ব ৩৭-এর বিষয়।

DIP আর testability

DIP-এর সবচেয়ে দৈনন্দিন লাভ test-এ। Policy যখন একটি ছোট, নিজের ভাষার abstraction-এর ওপর নির্ভর করে, তখন test-এ সেই abstraction-এর একটি সরল বাস্তবায়ন (fake, stub) বসানো যায় — database, network, ঘড়ি বা random সংখ্যা ছাড়াই। ঘড়িও একটি dependency: “রাত ১২টার পর ছাড় বন্ধ” নিয়মের test-এ আসল ঘড়ি ব্যবহার করলে test শুধু রাতে পাস করবে! Clock abstraction নিলে test নিজে সময় ঠিক করে দিতে পারে। Python-এর testing পর্বে এর ব্যবহারিক রূপ দেখতে পারেন; কোন test double কখন উপযুক্ত, পর্ব ৪৯-এ।

কখন DIP বাড়াবাড়ি

প্রতিটি class-এর জন্য একটি interface — IPriceCalculator আর তার একমাত্র PriceCalculatorImpl — অনেক “enterprise” কোডবেসের পরিচিত দৃশ্য। এতে file দ্বিগুণ হয়, কোড পড়তে লাফাতে হয়, কিন্তু লাভ প্রায় নেই। Martin নিজেও বলেন, স্থির concrete জিনিসের (যেমন language-এর string class) ওপর সরাসরি নির্ভর করা সমস্যা নয়; DIP-এর লক্ষ্য অস্থির (volatile) detail. নিজেকে তিনটি প্রশ্ন করুন:

  • এটি কি boundary? Database, network, file, ঘড়ি, তৃতীয় পক্ষের API, হার্ডওয়্যার — এদের পেছনে abstraction প্রায় সবসময় লাভজনক।
  • এর কি দ্বিতীয় বাস্তবায়ন আছে বা আসবে? Test-এর fake-ও একটি বাস্তবায়ন। কোনো দিন না এলে পর্ব ২৬-এর YAGNI মনে করুন।
  • এটি কি স্থির? Python-এর datetime.date, Decimal, নিজের value object (যেমন Money) — এদের interface-এর পেছনে লুকানো অপ্রয়োজনীয়।

একটি ছোট script বা দুই দিনের prototype-এ সরাসরি sqlite3 ডাকা সম্পূর্ণ যুক্তিসঙ্গত। পরে দরকার হলে abstraction বের করা (refactoring, পর্ব ৪২) সাধারণত সস্তা — যদি policy আর detail একই function-এ জট পাকিয়ে না থাকে। অর্থাৎ শুরু থেকে interface না লিখলেও, policy আর I/O আলাদা function-এ রাখার অভ্যাস প্রায় বিনামূল্যে ভবিষ্যতের পথ খোলা রাখে।

আরও গভীরে: Liskov ও Wing-এর ১৯৯৪ সালের গবেষণাপত্র (ACM TOPLAS) subtype-কে invariant আর history-র বৈশিষ্ট্য দিয়ে আনুষ্ঠানিকভাবে সংজ্ঞায়িত করে, আর দেখায় কেন শুধু method-ভিত্তিক নিয়ম যথেষ্ট নয়। Joshua Bloch-এর Effective Java-য় “inheritance-এর বদলে composition” প্রসঙ্গে Java-র নিজের ভুল উদাহরণ আছে — Stack হলো Vector-এর subclass, Properties হলো Hashtable-এর। Martin তাঁর Clean Architecture (২০১৭) বইয়ে DIP-কে architecture-এর “dependency rule”-এর ভিত্তি করেছেন — পর্ব ৪০-এ দেখবেন।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • LSP নিজের ভাষায় বলুন, আর Rectangle/Square কেন লঙ্ঘন? — subtype base-এর জায়গায় বসে আচরণ না ভাঙবে; Square-এর setter Rectangle-এর “অন্য দিক অপরিবর্তিত” postcondition ভাঙে।
  • Subclass precondition আর postcondition নিয়ে কী করতে পারে? — precondition দুর্বল বা সমান, postcondition শক্ত বা সমান; invariant রক্ষা, নতুন exception নয়।
  • ISP না মানলে কী সমস্যা? — অপ্রয়োজনীয় নির্ভরতা: অব্যবহৃত method বদলালেও recompile/retest, stub আর বড় test double.
  • Dependency inversion আর dependency injection কি একই? — না; DIP নির্ভরতার দিক ও মালিকানার নীতি, DI dependency বাইরে থেকে দেওয়ার কৌশল; একটি অন্যটি ছাড়া থাকতে পারে।
  • DIP-এ abstraction কোন package-এ থাকে? — high-level policy-র দিকে, তার ভাষায়; detail সেটি মানে।

৮ · AI-এর যুগে SOLID

AI-কে “SOLID মেনে refactor করো” বললে সে আনন্দের সাথে কোড তিনগুণ লম্বা করে দেয়। কিছু ভুল প্রায়ই দেখা যায়:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
প্রতিটি class-এর জন্য একটি interface আর একটি DI container — একটিমাত্র বাস্তবায়ন থাকলেওপ্রতিটি abstraction-এর জন্য জিজ্ঞেস করুন: boundary কি? দ্বিতীয় বাস্তবায়ন (test fake-সহ) কোথায়?
Square সারাতে setter-এ exception বসানো, বা isinstance দিয়ে বিশেষ ক্ষেত্রBase-এর contract test সব subclass-এ চালান; নতুন exception বা type পরীক্ষা দেখলে প্রত্যাখ্যান
Interface রাখে detail-এর package-এ (db/IRepository), SQL-এর ভাষায় methodImport-এর দিক দেখুন: policy-র package কি এখনো detail-এর package import করে?
এমন test, যা mock-এর ভেতরে implementation-এর call-ক্রম হুবহু নকল করে — কিছুই যাচাই করে নাকোডে ইচ্ছা করে ভুল ঢুকিয়ে test চালান (mutation testing, পর্ব ৫০); ফেল না করলে test অকেজো
ভুল ইতিহাস: “Liskov SOLID বানিয়েছেন”, “DIP আর DI একই”, ভুল বছরমূল উৎস মিলিয়ে নিন: Liskov ১৯৮৭/১৯৯৪, Martin ১৯৯৬/২০০২, Fowler ২০০৪
ভুল UML: realization-এর বদলে association, তীর উল্টো দিকেUML-এর নিয়মের সাথে মেলান (পর্ব ২৯) — ফাঁপা ত্রিভুজ interface-এর দিকে

নকশা review-তে AI-কে সমালোচক হিসেবে ব্যবহার করুন, লেখক হিসেবে নয়। কোডের আগে কোম্পানির গোপন নাম, key আর গ্রাহকের তথ্য মুছে নিন:

নিচে একটি কাল্পনিক অনলাইন বাজার app-এর payment অংশের Python কোড দিলাম (গোপন তথ্য মুছে দেওয়া)। ১. কোনো class hierarchy-তে Liskov substitution ভাঙছে কি না দেখো। প্রতিটি সন্দেহের জন্য base-এর কোন precondition, postcondition, invariant বা history-র প্রতিশ্রুতি ভাঙছে, তা নির্দিষ্ট করে বলো, আর একটি ছোট test দাও যা লঙ্ঘনটি দেখাবে। ২. কোনো interface অনেক ভিন্ন client-এর method এক জায়গায় জড়ো করেছে কি না দেখো; client ধরে ধরে কে কোন method ব্যবহার করে, তার টেবিল দাও। ৩. Business নিয়ম কোথায় সরাসরি database বা network-এর class import করে, তা চিহ্নিত করো। কিন্তু প্রতিটি নতুন interface প্রস্তাবের পাশে কারণ লেখো — boundary কি না, দ্বিতীয় বাস্তবায়ন কী। একটিমাত্র বাস্তবায়নের স্থির জিনিসের জন্য interface প্রস্তাব করবে না। ৪. নিজে কোড পুরো নতুন করে লিখবে না; ছোট ছোট ধাপের refactoring পরিকল্পনা দাও, প্রতিটি ধাপে test চলবে এমনভাবে।

AI-এর প্রস্তাব হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১গণিতে বর্গ তো আয়তই — তাহলে কোডে Square কেন Rectangle-এর subtype হতে পারে না? কোনো অবস্থায় কি পারে?

উত্তরের চাবি হলো: subtype সম্পর্ক জিনিসের মধ্যে নয়, তাদের আচরণের প্রতিশ্রুতির মধ্যে। গণিতের আয়ত একটি অপরিবর্তনীয় ধারণা — “২ × ৩-এর আয়ত” চিরকাল ২ × ৩। সেখানে “বর্গ” মানে এমন আয়ত যার দুই বাহু সমান; প্রতিটি বর্গ আয়তের সব বৈশিষ্ট্য মানে। কিন্তু কোডের Rectangle একটি বদলানো যায় এমন object, আর তার set_width একটি নতুন প্রতিশ্রুতি আনে: “শুধু width বদলাবে।” এই প্রতিশ্রুতি গণিতের আয়তে ছিলই না। বর্গ এই প্রতিশ্রুতি রাখতে গেলে বর্গ থাকে না, আর বর্গ থাকতে গেলে প্রতিশ্রুতি ভাঙে।

তাই উত্তর — হ্যাঁ, কিছু অবস্থায় পারে। যদি Rectangle immutable হয় (শুধু width, height, area() পড়া যায়, বদলানো যায় না), তবে Square সেই সব প্রতিশ্রুতি রাখে; একটি Square(3) যেকোনো “শুধু পড়ে” এমন Rectangle-এর কোডে নিরাপদে বসে। Liskov ও Wing-এর history constraint ঠিক এই পার্থক্য ধরে: mutable আচরণ যত বেশি, subtype হওয়ার শর্ত তত কড়া। অনেক functional ভাষা আর আধুনিক নকশা-রীতি immutable value object পছন্দ করে, তার একটি কারণ এটিই।

দ্বিতীয় শিক্ষা: “is-a” পরীক্ষা ভাষার ব্যাকরণের প্রশ্ন, নকশার প্রশ্ন নয়। “পেঙ্গুইন একটি পাখি” বাংলায় সত্য; কিন্তু আপনার Bird যদি “উড়তে পারে” প্রতিশ্রুতি দেয়, তবে কোডে পেঙ্গুইন সেই Bird নয়। ভালো প্রশ্ন হলো “behaves-like-a”: যে client এই type-এর জন্য লেখা, সে কি পার্থক্য টের পাবে? Martin তাঁর লেখায় এটিকেই মূল কথা বলেন — একটি নকশার বৈধতা তার client-দের প্রত্যাশা দিয়ে মাপা হয়, আলাদাভাবে নয়।

মূল উপলব্ধি: Subtype হওয়া নির্ভর করে কোন প্রতিশ্রুতি দেওয়া হয়েছে তার ওপর; mutable setter-এর প্রতিশ্রুতি সরালে Square আবার Rectangle-এর জায়গায় নিরাপদে বসতে পারে।

প্র ০২Java-র Collections.unmodifiableList যে list দেয়, তাতে add() ডাকলে UnsupportedOperationException আসে। এটি কি LSP লঙ্ঘন? ভাষা-নির্মাতারা এই সমস্যা কীভাবে এড়াতে চেয়েছেন?

প্রশ্নটি নিয়ে অনেক বছর ধরে তর্ক চলছে, আর উত্তরটি নির্ভর করে চুক্তি কোথায় লেখা তার ওপর। Java-র List interface-এর documentation add, remove-এর মতো method-কে “optional operation” বলে, আর স্পষ্ট লেখে যে বাস্তবায়ন চাইলে UnsupportedOperationException ছুড়তে পারে। অর্থাৎ base-এর চুক্তিতেই ব্যতিক্রমটি ঘোষিত — আক্ষরিক অর্থে unmodifiable list চুক্তি ভাঙছে না, কারণ চুক্তিটি আগেই দুর্বল করে রাখা হয়েছে।

কিন্তু এর মূল্য দেয় caller: List হাতে পেয়ে সে জানে না add কাজ করবে কি না। চুক্তি এত দুর্বল যে “list-এ যোগ করা যায়” — এই সবচেয়ে স্বাভাবিক প্রত্যাশাও নিশ্চিত নয়। LSP-র আত্মা অনুযায়ী এটি একটি আপস: Java-র নকশাকারীরা interface-এর সংখ্যা কম রাখতে (ISP-র উল্টো দিকে) চুক্তি দুর্বল করেছেন। Collections framework-এর নকশা নিয়ে প্রকাশিত আলোচনায় এই আপসের কারণ হিসেবে interface-এর সংখ্যা বিস্ফোরণ এড়ানোর কথা বলা হয়েছে বলে জানা যায়।

অন্য ভাষাগুলো ভিন্ন পথ নিয়েছে। Kotlin-এ List (শুধু পড়া) আর MutableList (যোগ-বিয়োগ) আলাদা interface; MutableList হলো List-এর subtype, উল্টো নয় — কারণ পরিবর্তনের ক্ষমতা একটি বাড়তি প্রতিশ্রুতি। Python-এর collections.abc-তেও Sequence আর MutableSequence আলাদা। এটি ISP আর LSP-র মিলিত প্রয়োগ: role interface দিয়ে প্রতিটি client ঠিক যা চায় তাই চায়, আর কোনো subtype-কে কাজ “ফিরিয়ে দিতে” হয় না।

আপনার নিজের কোডে শিক্ষা: যদি কিছু বাস্তবায়ন একটি কাজ না পারে, সবচেয়ে ভালো হলো সেই কাজকে আলাদা ভূমিকায় সরানো। তা সম্ভব না হলে অন্তত base-এর চুক্তিতে লিখে রাখুন এবং জিজ্ঞেস করার উপায় দিন — অঘোষিত exception সবচেয়ে খারাপ বিকল্প।

মূল উপলব্ধি: চুক্তি দুর্বল করে LSP-র অক্ষর রক্ষা করা যায়, কিন্তু তাতে caller-এর নিশ্চয়তা হারায়; read-only আর mutable ভূমিকা আলাদা interface-এ রাখা পরিষ্কার সমাধান।

প্র ০৩Type checker তো method-এর নাম আর type যাচাই করে। Liskov কেন বললেন, শুধু signature মেলা যথেষ্ট নয়? এই ধারণা তাঁর আগের কাজের সাথে কীভাবে জড়িত?

Liskov-এর ১৯৭০-এর দশকের প্রধান কাজ ছিল data abstraction — MIT-এ তাঁর দল CLU ভাষা বানায়, যেখানে “cluster” নামে একটি type তার data লুকিয়ে শুধু operation দেখাত। চিন্তাটি ছিল: একটি type-কে চেনা যায় তার আচরণ দিয়ে, ভেতরের গঠন দিয়ে নয়। একটি stack-এর ভেতরে array না linked list, client-এর জানার দরকার নেই — জানার দরকার push-এর পর pop করলে কী পাওয়া যায়। ২০০৮ সালের Turing Award-এর উল্লেখেও data abstraction-এর এই অবদানের কথা বলা হয়।

এই দৃষ্টিতে subtype-এর প্রশ্ন স্বাভাবিকভাবেই আচরণের প্রশ্ন। ১৯৮০-র দশকে object-oriented ভাষা জনপ্রিয় হচ্ছিল, আর অনেকে inheritance-কে কোড পুনর্ব্যবহারের হাতিয়ার হিসেবে ব্যবহার করছিলেন — “এই class-এর অর্ধেক কোড কাজে লাগবে, তাই subclass বানাই।” Liskov তাঁর ১৯৮৭ সালের বক্তৃতায় এই দুটি ব্যবহারকে আলাদা করেন: বাস্তবায়নের ভাগাভাগি আর type hierarchy এক জিনিস নয়। দ্বিতীয়টির জন্য substitution-এর শর্ত লাগে।

Signature যথেষ্ট নয় কেন, তার একটি সহজ উদাহরণ: Stack আর Queue — দুটিরই put(x) আর get() থাকতে পারে, একই type. কিন্তু একটি শেষে-ঢোকা-আগে-বেরোয়, অন্যটি আগে-ঢোকা-আগে-বেরোয়। কোনো type checker এদের আলাদা করবে না, অথচ একটির জায়গায় অন্যটি বসালে প্রোগ্রাম ভুল ফল দেবে। Liskov ও Wing ১৯৯৪ সালে তাই subtype-কে সংজ্ঞায়িত করেন বৈশিষ্ট্য-প্রমাণের ভাষায়: T সম্পর্কে যা প্রমাণ করা যায়, S সম্পর্কেও তা সত্য হবে — এর মধ্যে invariant আর ইতিহাসের বৈশিষ্ট্যও পড়ে।

ব্যবহারিক ফল: আচরণের চুক্তি যাচাইয়ের দায়িত্ব মানুষের আর test-এর। Eiffel-এর মতো কিছু ভাষা precondition আর postcondition কোডেই লেখার সুযোগ দেয়, আর subclass-এ সেগুলো ঠিক নিয়মে মেলায়; বেশিরভাগ ভাষায় আমরা documentation আর contract test-এর ওপর ভরসা করি।

মূল উপলব্ধি: LSP data abstraction-এরই সম্প্রসারণ — type মানে তার আচরণ; তাই subtype মানে আচরণের প্রতিশ্রুতি রক্ষা, যা কোনো compiler একা যাচাই করতে পারে না।

প্র ০৪Python বা JavaScript-এর মতো dynamic ভাষায় recompile-এর সমস্যা কম। তাহলে কি সেখানে interface segregation অপ্রাসঙ্গিক?

Martin-এর Xerox গল্পে ISP-র চালিকা শক্তি ছিল build-এর সময়: C++-এ একটি header file বদলালে যত file সেটি include করে, সবাই আবার compile হয়। Python-এ এমন বাধ্যতামূলক recompile নেই, আর duck typing-এর কারণে client যে method ডাকে না, তার অস্তিত্ব নিয়েও মাথা ঘামায় না। তাই খরচের একটি উৎস এখানে সত্যিই কম।

কিন্তু বাকি খরচগুলো ভাষা-নিরপেক্ষ। প্রথমত, পরিবর্তনের প্রভাব: একটি বড় abstract base class-এ একটি method যোগ হলে প্রতিটি subclass-কে সেটি লিখতে হয়, নইলে object-ই তৈরি হবে না — Python-এর abc এটি জোর করে। দ্বিতীয়ত, test double: একটি fat interface-এর fake বানাতে সব method লিখতে হয়, নইলে test-এ কোন অংশ ব্যবহার হলো তা পরিষ্কার নয়। Python-এর unittest.mock.Mock যেকোনো method ডাকতে দেয় বলে আরও বিপজ্জনক — ভুল নামের method ডাকলেও test পাস করতে পারে (এজন্যই spec বা autospec রাখা হয়)। তৃতীয়ত, বোঝার খরচ: একটি function-এর parameter-এ Machine লেখা থাকলে পাঠক জানেন না সে আসলে চার method-এর কোনটি ব্যবহার করে; Printer লেখা থাকলে সাথে সাথে জানেন।

আধুনিক Python-এ typing.Protocol ISP-কে প্রায় বিনামূল্যে করে দিয়েছে: client নিজের প্রয়োজনের ছোট protocol ঘোষণা করে, আর বাস্তবায়নকে সেটি জানতেও হয় না। Go ভাষার প্রচলিত রীতিও কাছাকাছি — interface ছোট রাখা আর ব্যবহারকারীর দিকে ঘোষণা করা। এই রীতি DIP-এর “abstraction-এর মালিক client” কথার সাথেও মেলে।

আরেকটি ক্ষেত্র যেখানে ISP খুব জীবন্ত: network API. একটি বড় REST endpoint যা সব client-এর জন্য সব field পাঠায়, তার একটি field বদলালে সব mobile app-এর version নিয়ে চিন্তা করতে হয়। Client-নির্দিষ্ট API বা field বেছে নেওয়ার ব্যবস্থা একই নীতির network-রূপ।

মূল উপলব্ধি: ISP-র মূল কথা compile নয়, নির্ভরতার পরিধি; dynamic ভাষায়ও ছোট, client-কেন্দ্রিক চুক্তি পরিবর্তন, test আর বোঝাকে সস্তা করে।

প্র ০৫“প্রতিটি class-এর পেছনে একটি interface” — অনেক কোডবেসে এটি নিয়ম। DIP কি এটাই চায়? না হলে সীমারেখা কোথায় টানবেন?

না — এবং Martin-এর নিজের লেখাতেই এর উত্তর আছে। DIP-এর লক্ষ্য অস্থির খুঁটিনাটি থেকে স্থির নীতিকে রক্ষা করা। যে জিনিস প্রায় বদলায় না — language-এর string, তারিখের class, নিজের Money value object — তার ওপর সরাসরি নির্ভরতা কোনো ঝুঁকি আনে না; তাদের পেছনে interface শুধু অতিরিক্ত file আর পরোক্ষতা যোগ করে। অনেকে এই প্রবণতাকে ঠাট্টা করে “interface-এর জন্য interface” বলেন; open-source জগতে একটি ব্যঙ্গাত্মক project আছে যেখানে FizzBuzz-এর মতো সরল প্রোগ্রামকে factory, strategy আর interface-এর স্তূপে লেখা হয়েছে — বাড়াবাড়ি দেখাতে।

“প্রতিটির পেছনে interface” রীতির পেছনে কিছু ঐতিহাসিক কারণও আছে। পুরোনো Java mocking tool-গুলো অনেক সময় শুধু interface-এর নকল বানাতে পারত, তাই test করতে interface লাগত। কিছু framework proxy বানাতে interface চাইত। আজ অনেক tool concrete class-এরও নকল বানাতে পারে, তবু অভ্যাসটি রয়ে গেছে।

সীমারেখা টানার একটি ব্যবহারিক উপায়: boundary আর volatility. যেখানে আপনার কোড বাইরের জগতের সাথে কথা বলে — database, network, file, ঘড়ি, payment gateway, হার্ডওয়্যার — সেখানে abstraction প্রায় সবসময় লাভজনক, কারণ test-এ এদের বদলাতে হয় আর এরা সত্যিই বদলায় (vendor বদল, version বদল)। Business নিয়মের ভেতরের ছোট class-গুলোর মধ্যে সাধারণত সরাসরি নির্ভরতাই যথেষ্ট। আরেকটি সংকেত: যদি কোনো interface-এর নাম তার একমাত্র বাস্তবায়নের নামের সাথে “I” বা “Impl” যোগ করে বানানো হয়, তবে সম্ভবত সেটি কোনো ভূমিকা প্রকাশ করছে না।

শেষে মনে রাখুন, abstraction পরে বের করাও সম্ভব। Policy আর I/O আলাদা function-এ রাখলে, দরকারের দিন একটি protocol বের করে আনা কয়েক মিনিটের refactoring. কিন্তু অপ্রয়োজনীয় abstraction একবার ঢুকলে সবাই তার চারপাশে কোড লেখে, আর সরানো কঠিন হয় — technical debt-এর একটি উল্টো রূপ।

মূল উপলব্ধি: DIP প্রয়োগ করুন অস্থির boundary-তে, যেখানে দ্বিতীয় বাস্তবায়ন (test fake-সহ) সত্যিই আছে; স্থির জিনিসের পেছনে interface নকশাকে ভালো নয়, শুধু লম্বা করে।

অনুশীলন

আউটপুট অনুমান, হিসাব, hierarchy নতুন করে সাজানো আর সমালোচনা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।

  1. আউটপুট অনুমান করুন (LSP test): একটি কাল্পনিক মোবাইল ওয়ালেটের Account আর তার দুটি subclass. Test-টি base-এর চুক্তি দিয়ে লেখা: ৬০০০ টাকা তুললে balance ঠিক ৬০০০ কমবে। চারটি লাইনের আউটপুট লিখুন, আর প্রতিটি subclass LSP-র কোন নিয়ম ভাঙছে বলুন।
    class Account:
        def __init__(self, bal): self.bal = bal
        def withdraw(self, amt):          # pre: 0 < amt <= bal
            if amt > self.bal: raise ValueError("insufficient")
            self.bal -= amt               # post: bal decreases by exactly amt
            return self.bal
    
    class SavingsAccount(Account):
        def withdraw(self, amt):
            if amt > 5000: raise ValueError("limit 5000")
            return super().withdraw(amt)
    
    class FeeAccount(Account):
        def withdraw(self, amt):
            self.bal -= amt + 10
            return self.bal
    
    def test(acc):
        before = acc.bal
        try:
            after = acc.withdraw(6000)
            print(type(acc).__name__, "OK" if after == before - 6000 else f"post broken: {after}")
        except ValueError as e:
            print(type(acc).__name__, "rejected:", e)
    
    for a in (Account(10000), SavingsAccount(10000), FeeAccount(10000), FeeAccount(100)):
        test(a)

    আসল আউটপুট (চালিয়ে মেলানো):

    Account OK
    SavingsAccount rejected: limit 5000
    FeeAccount post broken: 3990
    FeeAccount post broken: -5910
    • Account(10000): ৬০০০ ≤ ১০০০০, balance ৪০০০ = ১০০০০ − ৬০০০ → OK.
    • SavingsAccount(10000): base-এর precondition (amt ≤ bal) পূরণ হয়েছে, তবু নতুন শর্ত “৫০০০-এর বেশি নয়” চাপানো হলো — precondition কড়া করা। Exception-এর type একই (ValueError) হলেও কারণ নতুন; যে caller ধরে নিয়েছিল “balance থাকলে তোলা যাবে”, সে ঠকল।
    • FeeAccount(10000): ১০ টাকা ফি কেটে balance ৩৯৯০, ৪০০০ নয় — postcondition দুর্বল করা (“ঠিক amt কমবে” আর সত্য নয়)।
    • FeeAccount(100): base এখানে “insufficient” বলে থামত; FeeAccount থামেনি, balance −৫৯১০ — test “post broken” ছাপলেও আসল ক্ষতি invariant-এর: balance কখনো ঋণাত্মক নয়। এই লাইন দেখায়, test-এর বার্তা সব সময় মূল নিয়মের নাম বলে না; কারণ খুঁজতে হয়।

    সমাধানের দিক: ফি যদি ব্যবসার বাস্তবতা হয়, base-এর চুক্তিতেই লিখুন (“balance কমবে amt + fee(amt), fee ≥ ০”) আর fee() method দিন; সীমা থাকলে max_withdraw() দিয়ে চুক্তিতে প্রকাশ করুন, যাতে caller আগেই জানতে পারে।

  2. হিসাব (ISP): একটি কাল্পনিক সুপারশপের software-এ StoreDevice interface-এ ৬টি method: print_receipt, scan_barcode, open_drawer, weigh, show_price, send_sms. চারটি যন্ত্র-class এটি বাস্তবায়ন করে: ReceiptPrinter (শুধু print_receipt পারে), BarcodeScanner (শুধু scan_barcode), Scale (weigh আর show_price), CashDrawer (শুধু open_drawer). তিনটি client: Checkout ব্যবহার করে print_receipt, scan_barcode, open_drawer, show_price; Inventory শুধু scan_barcode; WeighStation weigh আর show_price. (ক) মোট কতটি stub লিখতে হয়? (খ) Client-দের মোট অপ্রয়োজনীয় নির্ভরতা কত? (গ) send_sms-এর signature বদলালে কতগুলো module (client + যন্ত্র) প্রভাবিত? (ঘ) Role interface-এ ভাঙলে এই তিনটি সংখ্যা কত হয়?

    (ক) Stub: প্রতিটি যন্ত্রকে ৬টি method লিখতে হয়, যা পারে না সেগুলো stub: ReceiptPrinter ৬ − ১ = ৫, BarcodeScanner ৫, Scale ৬ − ২ = ৪, CashDrawer ৫. মোট ৫ + ৫ + ৪ + ৫ = ১৯টি stub — আর প্রতিটি একটি সম্ভাব্য LSP লঙ্ঘন।

    (খ) অপ্রয়োজনীয় নির্ভরতা: Checkout ৬ − ৪ = ২, Inventory ৬ − ১ = ৫, WeighStation ৬ − ২ = ৪. মোট ১১. লক্ষ করুন, send_sms কেউ ব্যবহারই করে না।

    (গ) send_sms বদলালে: তিনটি client আর চারটি যন্ত্র — সবাই interface-এর সাথে বাঁধা, তাই ৭টি module আবার build/review/test করতে হয়, যদিও কেউ SMS পাঠায় না।

    (ঘ) Role interface (Receipt, Scanner, Drawer, Weigher, PriceDisplay, Sms): stub ০; অপ্রয়োজনীয় নির্ভরতা ০ (Checkout চারটি ছোট interface, Inventory একটি, WeighStation দুটি); send_sms বদলালে প্রভাবিত শুধু Sms interface-এর বাস্তবায়ন আর ব্যবহারকারী — এখানে কেউ নেই, তাই ০টি বিদ্যমান module (আর বাস্তবে প্রশ্ন জাগে, send_sms এই interface-এ ছিল কেন — হয়তো YAGNI). Weigher আর PriceDisplay আলাদা রাখা না একসাথে, তা নির্ভর করে তারা একসাথে বদলায় কি না; Checkout শুধু show_price চায় বলে এখানে আলাদা রাখা যুক্তিসঙ্গত।

  3. এই hierarchy নতুন করে সাজান: একটি কাল্পনিক অনলাইন বাজারে PaymentMethod-এর তিনটি method: pay(amount), refund(amount), save_for_later(). Subclass: Card (তিনটিই পারে), MobileWallet (pay আর refund পারে; save_for_later-এ NotImplementedError), CashOnDelivery (শুধু pay; বাকি দুটিতে NotImplementedError). Checkout-এর কোডে আছে if isinstance(p, CashOnDelivery): show("refund নগদে দোকান থেকে"). কী কী সমস্যা, আর নতুন নকশা কেমন হবে?

    সমস্যা: (১) NotImplementedError override — refused bequest, LSP লঙ্ঘন: PaymentMethod ধরে লেখা refund-এর কোড COD পেলে crash করবে। (২) Checkout-এর isinstance — caller subclass চিনতে বাধ্য; নতুন একটি “বাকিতে কেনা” পদ্ধতি এলে আরেকটি if (open-closed ভাঙল)। (৩) Fat interface — যে কোড শুধু টাকা নেয়, সেও refund আর সংরক্ষণের সাথে বাঁধা (ISP).

    নতুন নকশা (একটি সম্ভাব্য রূপ):

    • Payment — শুধু pay(amount); তিনটিই এটি মানে।
    • Refundable — refund(amount); Card আর MobileWallet মানে।
    • Saveable — save_for_later(); শুধু Card.
    • Refund-এর ব্যবসায়িক নিয়ম আলাদা: RefundPolicy একটি Payment দেখে refund-এর পথ ফেরত দেয় — Refundable হলে online refund, না হলে “দোকান থেকে নগদ”-এর মতো একটি নির্দেশনা object. এই একটি জায়গায় সিদ্ধান্ত হয়, Checkout জুড়ে ছড়িয়ে নয়। Python-এ @runtime_checkable Protocol দিয়ে ক্ষমতা জিজ্ঞেস করা যায়, অথবা প্রতিটি পদ্ধতি নিজেই refund_route() দিতে পারে (তাহলে base-এর চুক্তিতেই refund-এর পথ আছে, আর COD সৎভাবে “নগদ” বলে)।

    মূল কথা: “সব পেমেন্ট refund করা যায়” — এই মিথ্যা প্রতিশ্রুতি base থেকে সরানো হলো। ক্ষমতাগুলো ছোট ভূমিকায় ভাগ হলো (ISP), কোনো subclass কাজ ফিরিয়ে দেয় না (LSP), আর বিশেষ ক্ষেত্র একটি জায়গায় সিদ্ধান্ত হয়।

  4. এই pull request-এর সমালোচনা করুন: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালে একজন developer লিখেছেন: “SOLID মেনে refactor: (১) প্রতিটি class-এর জন্য interface — IGradeCalculator/GradeCalculatorImpl, IStudent/StudentImpl, IResultRepository; (২) IResultRepository রাখা হয়েছে db/mysql/ folder-এ, method run_query(sql); (৩) একটি DI container যোগ করেছি, যা ১২টি class জোড়ে; (৪) GPA-র test-এ IStudent-এর mock, যা ঠিক করা উত্তর দেয়।” Review-তে কী বলবেন?
    • DIP আসলে হয়নি (সবচেয়ে বড় সমস্যা): IResultRepository detail-এর folder-এ আর SQL-এর ভাষায় (run_query(sql)). Policy (ফলাফল প্রকাশের নিয়ম) এখনো mysql package import করে আর SQL লেখে — তীর উল্টায়নি, শুধু একটি নাম যোগ হয়েছে। প্রস্তাব: interface-টি results/domain package-এ সরান, policy-র ভাষায় method দিন — save_result(student_id, term, grades), results_for(student_id); SQL থাকুক MySQL বাস্তবায়নের ভেতরে।
    • অপ্রয়োজনীয় abstraction: IGradeCalculator আর IStudent-এর একটিমাত্র বাস্তবায়ন, আর এরা boundary নয় — বিশুদ্ধ হিসাব আর data. এদের interface শুধু file বাড়ায়। Student একটি সাধারণ dataclass হলেই চলে।
    • Value object-এর mock: GPA-র test-এ আসল Student বানানো সহজ আর দ্রুত; mock করলে test এমন উত্তরের ওপর চলে যা আসল class হয়তো কখনো দেবে না। Mock রাখুন boundary-র জন্য (পর্ব ৪৯)।
    • DI container-এর দরকার আছে কি? ১২টি class জোড়া একটি main() function-এ ১৫–২০ লাইনের হাতে লেখা composition root-এ হতে পারে। Container আনলে নতুন dependency, শেখার খরচ আর “জাদু”-র মতো জোড়া আসে; বড় হলে পরে বিবেচনা করা যায় (পর্ব ৩৭)।
    • প্রক্রিয়া: “SOLID মেনে refactor” একটি PR-এ অনেক কিছু বদলাচ্ছে; review সহজ করতে ছোট PR-এ ভাগ করতে বলুন — প্রথমে repository-র DIP, যার লাভ সবচেয়ে স্পষ্ট (database ছাড়া ফলাফল-নিয়মের test).

    মন্তব্যের সুর: নীতির নাম না তুলে প্রতিটি বদলের লাভ জিজ্ঞেস করুন — “এই interface-এর দ্বিতীয় বাস্তবায়ন কোথায়?”, “এই বদলে কোন test সহজ হলো?”

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি business service-কে database থেকে মুক্ত করতে dependency inversion-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔍 Policy কোথায় সরাসরি detail-এর class বানায় বা import করে, তা খুঁজে বের করা
  2. 🗣️ Policy আসলে কী চায়, তা তার নিজের ভাষায় লেখা — যেমন শুধু save(order)
  3. 🔌 সেই abstraction policy-র package-এ ঘোষণা করা, আর policy-কে শুধু এর ওপর নির্ভর করানো
  4. 🗄️ Detail-এর class-কে abstraction-এর চুক্তি মানানো (adapter)
  5. 🧷 Composition root-এ আসল বাস্তবায়ন জোড়া দেওয়া
  6. 🧪 In-memory fake দিয়ে database ছাড়াই policy-র test চালানো

একটি base class-এর method ship(order) যেকোনো ওজনের অর্ডার নেয় আর প্রতিশ্রুতি দেয় ৫ দিনের মধ্যে পৌঁছাবে। কোন subclass Liskov substitution principle মানে?

  • শুধু ২ কেজির কম ওজনের অর্ডার নেয়, বাকিতে error, আর ৩ দিনে পৌঁছায়
  • যেকোনো ওজন নেয়, আর ৩ দিনের মধ্যে পৌঁছায়
  • যেকোনো ওজন নেয়, আর ৭ দিনের মধ্যে পৌঁছায়
  • যেকোনো ওজন নেয়, ৫ দিনে পৌঁছায়, কিন্তু বৃষ্টির দিনে একটি নতুন WeatherError ছোড়ে

class Rectangle-এ set_width আর set_height আছে; class Square(Rectangle) দুটিতেই দুই বাহু সমান করে। একটি test করে: set_width(4), set_height(5), তারপর area() == 20 যাচাই। Square(3)-এ এই test-এর ফল কী, আর কেন?

  • পাস করে, কারণ শেষে ক্ষেত্রফল ২০
  • TypeError, কারণ Square-কে Rectangle-এর জায়গায় দেওয়া যায় না
  • ফেল করে, ক্ষেত্রফল ১৬
  • ফেল করে, ক্ষেত্রফল ২৫ — Rectangle-এর setter-এর postcondition ভেঙেছে

একটি counter class-এর একমাত্র method next() মান এক বাড়ায়, তাই মান কখনো কমে না। একটি subclass reset() যোগ করল, যা মান ০ করে; next()-এ কোনো বদল নেই। Liskov ও Wing-এর কোন নিয়ম ভাঙল?

  • History constraint
  • Precondition কড়া করা
  • next()-এর postcondition দুর্বল করা
  • নতুন exception ছোড়া

একটি interface-এ ৫টি method. দুটি client যথাক্রমে ১টি আর ২টি method ব্যবহার করে, আর একটি class শুধু ১টি method অর্থপূর্ণভাবে পারে। Client-দের মোট অপ্রয়োজনীয় নির্ভরতা আর ওই class-এর stub-এর সংখ্যা কত?

  • ৩ আর ১
  • ১০ আর ৫
  • ৭ আর ৪
  • ৭ আর ৫

OrderService-এর constructor একটি concrete MySQLRepository object নেয় (OrderService(MySQLRepository())), আর OrderService-এর file mysql module import করে। কোনটি সঠিক?

  • Dependency injection আর dependency inversion দুটিই আছে
  • Dependency injection আছে, কিন্তু dependency inversion নেই
  • কোনোটিই নেই, কারণ object নিজেই dependency বানায়
  • Dependency inversion আছে, কারণ constructor parameter ব্যবহার হয়েছে

Dependency inversion principle অনুযায়ী, business নিয়ম (policy) আর database adapter (detail)-এর মাঝের Repository interface কোথায় থাকা উচিত, আর কোন ভাষায় লেখা?

  • Database adapter-এর package-এ, SQL query-র ভাষায়
  • Policy-র package-এ, কিন্তু run_query(sql)-এর মতো SQL-এর ভাষায়
  • একটি আলাদা “interfaces” package-এ, যেকোনো ভাষায়
  • Policy-র package-এ, policy-র ভাষায় — যেমন save(order)

কোন পরিস্থিতিতে একটি নতুন interface বানিয়ে dependency inversion প্রয়োগ করা সবচেয়ে কম যুক্তিসঙ্গত?

  • নিজের তৈরি স্থির Money value object, যার একটিই বাস্তবায়ন আর test-এ সহজে বানানো যায়
  • তৃতীয় পক্ষের payment gateway, যার vendor ভবিষ্যতে বদলাতে পারে
  • “রাত ১২টার পর ছাড় বন্ধ” নিয়মের জন্য বর্তমান সময় (ঘড়ি)
  • অর্ডার জমা রাখার database

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. LSP (Liskov ১৯৮৭; Liskov ও Wing ১৯৯৪): subtype base-এর জায়গায় বসে আচরণ ভাঙবে না — precondition কড়া নয়, postcondition দুর্বল নয়, invariant ও history রক্ষা, নতুন exception নয়। “বেশি চাইবেন না, কম দেবেন না।”
  2. লক্ষণ: client-এ isinstance, concrete subclass-এ NotImplementedError বা খালি override, subclass-এর জন্য test skip. প্রমাণ: base-এর contract test সব subclass-এ, সীমার input-সহ। সমাধান: ছোট প্রতিশ্রুতির hierarchy, composition, বা চুক্তি প্রকাশ্যে।
  3. ISP: client যা ব্যবহার করে না তার ওপর নির্ভর করবে না। Fat interface → role interface; লাভ: কম recompile/retest, শূন্য stub, ছোট test double. ভাগ হয় client-এর প্রয়োজন ধরে, এক-method-এর নিয়মে নয়।
  4. DIP: policy আর detail দুটিই abstraction-এর ওপর; abstraction policy-র package-এ, policy-র ভাষায়। উল্টায় source code-এর নির্ভরতা, চালানোর সময়ের call নয়।
  5. DIP ≠ DI ≠ IoC: নীতি, কৌশল, বড় ধারণা। DIP প্রয়োগ করুন অস্থির boundary-তে (database, network, ঘড়ি); স্থির, একক বাস্তবায়নের জিনিসে এটি বাড়াবাড়ি।

পরের পর্বে: এই পর্বের নকশাগুলো আঁকার ভাষা — UML class diagram: class, association, aggregation, composition আর inheritance, আর interface-এর realization-এর সেই ফাঁপা ত্রিভুজ।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান