Python-এ SOLID নীতি ও design pattern: test করা যায় এমন code-সহ

SOLID পাঁচটি অভ্যাস যা code বদলানো ও test করা সহজ করে। ছোট একটি Python checkout উদাহরণে দেখুন, আর কখন pattern বেশি হয়ে যায়।

socket-সহ code ব্লক ও একটি নতুন ব্লক অন্যদের না ছুঁয়ে লাগছে

SOLID পাঁচটি design অভ্যাসের সেট যা code বদলানো ও test করা সহজ করে। এগুলো নির্দেশিকা, আইন নয়। হালকাভাবে ব্যবহার করলে অনেক সাহায্য হয়, আর যান্ত্রিকভাবে করলে ছোট ছোট class-এর গোলকধাঁধা তৈরি হয়। নিচের code ও output প্রকাশের আগে Python 3-এ চালানো।

পাঁচ অক্ষর, সহজ ভাষায়

  • S, Single responsibility। একটি code-এর বদলানোর কারণ একটিই। মূল্য হিসাব ও email পাঠানো আলাদা কারণ।
  • O, Open for extension, closed for modification। আচরণ যোগ করুন নতুন code দিয়ে (নতুন discount class), পরীক্ষিত function-এ আরও if যোগ করে নয়।
  • L, Liskov substitution। subclass-কে parent যেখানে চলে সেখানে অবাক না করে চলতে হবে। PercentOff যেখানে সংখ্যা ফেরায় সেখানে StudentDiscount error ছুড়লে এই নিয়ম ভাঙে।
  • I, Interface segregation। interface ছোট রাখুন। যে method কোনো class ব্যবহার করে না তা বাস্তবায়নে বাধ্য করবেন না।
  • D, Dependency inversion। abstraction-এর ওপর নির্ভর করুন, আর dependency ভেতরে বানানোর বদলে বাইরে থেকে দিন। এতেই test সহজ হয়।

একটি ছোট উদাহরণ

from typing import Protocol

# D: Checkout depends on abstractions (Notifier, Discount), not on concrete classes.
class Notifier(Protocol):
    def send(self, to: str, text: str) -> None: ...

class EmailNotifier:
    def __init__(self): self.sent = []
    def send(self, to, text): self.sent.append(("email", to, text))

# O: new discount rules are new classes; the pricing code does not change.
class Discount(Protocol):
    def apply(self, total: float) -> float: ...

class NoDiscount:
    def apply(self, total): return total

class PercentOff:
    def __init__(self, pct): self.pct = pct
    def apply(self, total): return total * (1 - self.pct / 100)

class StudentDiscount(PercentOff):
    def __init__(self): super().__init__(15)

# S: pricing only prices; sending a message happens elsewhere.
def price(items, discount: Discount) -> float:
    return round(discount.apply(sum(p * q for p, q in items)), 2)

class Checkout:
    def __init__(self, notifier: Notifier, discount: Discount):
        self.notifier, self.discount = notifier, discount
    def pay(self, user, items):
        total = price(items, self.discount)
        self.notifier.send(user, f"Paid {total}")
        return total

Checkout জানে না সে email পাঠায় না SMS, বা discount ছাত্রের না মৌসুমি। এটি S, O ও D একসঙ্গে। Python-এ Protocol inheritance ছাড়াই বলে দেয় একটি class-এর কী গড়ন থাকতে হবে।

test-সহ চালানো

items = [(20.0, 2), (10.0, 1)]
mail = EmailNotifier()
print(Checkout(mail, NoDiscount()).pay("a@example.com", items))
print(Checkout(mail, StudentDiscount()).pay("s@example.com", items))
print(mail.sent)

class FakeNotifier:                      # a test double, no email system needed
    def __init__(self): self.calls = []
    def send(self, to, text): self.calls.append((to, text))
fake = FakeNotifier()
assert Checkout(fake, PercentOff(50)).pay("x", items) == 25.0 and fake.calls == [("x", "Paid 25.0")]
print("test with a fake notifier passed, no email system needed")
50.0
42.5
[('email', 'a@example.com', 'Paid 50.0'), ('email', 's@example.com', 'Paid 42.5')]
test with a fake notifier passed, no email system needed

দুজন ক্রেতা 50.0 ও 42.5 দিয়েছেন, মানে ছাত্রের ১৫% ছাড়। test-এ fake notifier ও ৫০% ছাড় ব্যবহার হয়েছে। email সিস্টেম বা নেটওয়ার্ক লাগেনি, কারণ Checkout তার notifier বাইরে থেকে পেয়েছে।

যে pattern-এর সঙ্গে দেখা হবে

  • Strategy। run time-এ algorithm বদল। ওপরের Discount class গুলো একটি strategy।
  • Factory। একটি function ঠিক করে কোন class বানাতে হবে।
  • Adapter। library মুড়ে আপনার code-কে পছন্দের interface দেখানো।
  • Observer। object ঘটনায় subscribe করে (callback, signal)।
  • Decorator। function মুড়ে আচরণ যোগ (caching, সময় মাপা, logging)।

সাধারণ ভুল

  • "যদি লাগে" ভেবে প্রতিটি class-এর জন্য abstraction বানানো।
  • composition (অন্য object ধরে রাখা) সহজ হলেও গভীর inheritance গাছ।
  • dependency hard-code করা (business logic-এর ভেতরে smtplib), ফলে test ধীর ও ভঙ্গুর।
  • সমস্যা আসার আগেই pattern-এর নাম বসানো।
  • সবকিছু করা একটি বিশাল class ("god object")।

মূল কথা: প্রতিটি অংশ কেন্দ্রীভূত রাখুন, নতুন code যোগ করে আচরণ বাড়ান, আর fake দিয়ে test করতে dependency বাইরে থেকে দিন। বাস্তব সমস্যা দূর করলে তবেই pattern ব্যবহার করুন।


Programming roadmaps সিরিজ, পর্ব ৩ (ভালো Python লেখা): 5 নম্বর (মোট 5)। আগের লেখা: Python-এ error handling ও logging: পরিষ্কারভাবে ব্যর্থ হোন.

আরও পড়ুন