OOP: Class, Inheritance, Dataclass

Object দিয়ে বাস্তব জিনিস modeling করা

রোডম্যাপের মাস ২পড়তে প্রায় ২২ মিনিট

Flutter এ সবকিছুই class: StatelessWidget, ChangeNotifier, model class। তাই Object-Oriented Programming (OOP) তোমার কাছে নতুন না। এই অধ্যায়ে দেখবো Python এ class কীভাবে লেখে, Dart থেকে কোথায় আলাদা, আর dataclass দিয়ে কীভাবে অনেক কম লিখে একই কাজ করা যায়। শেষে দেখবো কখন inheritance আর কখন composition।

#প্রথম class

Python
class BankAccount:
    """একটা সাধারণ bank account।"""

    bank_name = "Demo Bank"            # class attribute: সব account এর জন্য এক

    def __init__(self, owner: str, balance: int = 0) -> None:
        self.owner = owner             # instance attribute: প্রতিটা account এর আলাদা
        self.balance = balance

    def deposit(self, amount: int) -> None:
        if amount <= 0:
            raise ValueError("টাকার পরিমাণ শূন্যের বেশি হতে হবে")
        self.balance += amount

    def withdraw(self, amount: int) -> None:
        if amount > self.balance:
            raise ValueError("পর্যাপ্ত টাকা নেই")
        self.balance -= amount


acc = BankAccount("Rahim", 1000)
acc.deposit(500)
acc.withdraw(200)
print(acc.owner, acc.balance)      # Rahim 1300
print(acc.bank_name)               # Demo Bank

Dart এর সাথে পার্থক্য:

Dart Python
Constructor: BankAccount(this.owner, this.balance); def __init__(self, owner, balance):
this লিখতে হয় না প্রায়ই self সবসময় লিখতে হয়, প্রতিটা method এর প্রথম parameter
new বা সরাসরি BankAccount(...) সরাসরি BankAccount(...)
_balance = library-private _balance = "ভেতরের জিনিস, হাত দিও না" (শুধু রীতি, ভাষা আটকায় না)

#Property: হিসাব করা attribute

Python
class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    @property
    def area(self) -> float:          # method, কিন্তু attribute এর মতো পড়া যায়
        return self.width * self.height

r = Rectangle(4, 5)
print(r.area)       # 20  ← r.area() না

Property দিয়ে যাচাইও করা যায়, যাতে ভুল value set না হয়:

Python
class Product:
    def __init__(self, name: str, price: int) -> None:
        self.name = name
        self.price = price            # এটাও নিচের setter দিয়ে যাবে

    @property
    def price(self) -> int:
        return self._price

    @price.setter
    def price(self, value: int) -> None:
        if value < 0:
            raise ValueError("দাম ঋণাত্মক হতে পারে না")
        self._price = value

p = Product("Mouse", 650)
p.price = -10       # ValueError

#Special (dunder) method: Python এর সাথে মিশে যাওয়া

দুই পাশে দুইটা underscore (__init__) ওয়ালা method কে বলে dunder method। এগুলো দিয়ে তোমার class কে Python এর সাধারণ জিনিসের মতো আচরণ করানো যায়:

Python
class Money:
    def __init__(self, paisa: int) -> None:
        self.paisa = paisa

    def __repr__(self) -> str:                 # debug এ কীভাবে দেখাবে
        return f"Money({self.paisa})"

    def __str__(self) -> str:                  # print() এ কীভাবে দেখাবে
        return f"৳{self.paisa / 100:,.2f}"

    def __add__(self, other: "Money") -> "Money":   # a + b
        return Money(self.paisa + other.paisa)

    def __eq__(self, other: object) -> bool:   # a == b
        return isinstance(other, Money) and self.paisa == other.paisa

    def __lt__(self, other: "Money") -> bool:  # a < b (sort এর জন্য)
        return self.paisa < other.paisa


a = Money(12550)
b = Money(4500)
print(a + b)               # ৳170.50
print(repr(a))             # Money(12550)
print(sorted([a, b]))      # [Money(4500), Money(12550)]
Method কখন চলে
__init__ Object তৈরির সময়
__repr__ / __str__ Debug দেখা / print()
__eq__, __lt__ ==, < (আর sort)
__len__ len(obj)
__getitem__ obj[key]
__iter__ for x in obj
__enter__ / __exit__ with obj: (Context manager)

#dataclass: কম লিখে বেশি কাজ

উপরের মতো __init__, __repr__, __eq__ প্রতিটা class এ হাতে লেখা ক্লান্তিকর। Dart এ যেমন freezed package বা record, Python এ আছে dataclass:

Python
from dataclasses import dataclass, field
from datetime import datetime


@dataclass
class OrderItem:
    name: str
    price: int          # পয়সায়
    qty: int = 1

    @property
    def subtotal(self) -> int:
        return self.price * self.qty


@dataclass
class Order:
    id: int
    customer: str
    items: list[OrderItem] = field(default_factory=list)   # mutable default এর সঠিক উপায়
    created_at: datetime = field(default_factory=datetime.now)

    def add(self, item: OrderItem) -> None:
        self.items.append(item)

    @property
    def total(self) -> int:
        return sum(i.subtotal for i in self.items)


order = Order(id=1, customer="Nila")
order.add(OrderItem("Burger", 25000, 2))
order.add(OrderItem("Coke", 4000))
print(order)            # সুন্দর repr নিজে থেকেই
print(order.total)      # 54000

@dataclass নিজে থেকে __init__, __repr__, __eq__ বানিয়ে দেয়। আরও কিছু option:

Python
@dataclass(frozen=True)          # বদলানো যাবে না (immutable), hashable হয়
class Location:
    lat: float
    lng: float

@dataclass(order=True)           # <, > দিয়ে তুলনা করা যাবে
class Version:
    major: int
    minor: int

@dataclass(slots=True)           # কম memory, attribute এর বানান ভুল করলে error
class Point:
    x: int
    y: int

#Enum: নির্দিষ্ট কয়েকটা value

Python
from enum import StrEnum

class OrderStatus(StrEnum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    ON_THE_WAY = "on_the_way"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

status = OrderStatus.CONFIRMED
print(status == "confirmed")          # True, StrEnum string এর সাথে তুলনা করা যায়
print([s.value for s in OrderStatus])

Status কে "confirmd" এর মতো ভুল বানানের string হিসেবে না রেখে enum রাখলে ভুল সাথে সাথে ধরা পড়ে।

#Inheritance: "একটা ধরনের"

Python
class PaymentMethod:
    def __init__(self, amount: int) -> None:
        self.amount = amount

    def pay(self) -> str:
        raise NotImplementedError("প্রতিটা payment method নিজের pay() লিখবে")

    def receipt(self) -> str:
        return f"{self.amount / 100:.2f} টাকা পরিশোধ: {self.pay()}"


class MobileWallet(PaymentMethod):
    def __init__(self, amount: int, phone: str) -> None:
        super().__init__(amount)          # parent এর __init__
        self.phone = phone

    def pay(self) -> str:
        return f"wallet {self.phone} থেকে"


class Card(PaymentMethod):
    def __init__(self, amount: int, last4: str) -> None:
        super().__init__(amount)
        self.last4 = last4

    def pay(self) -> str:
        return f"card ****{self.last4} থেকে"


class CashOnDelivery(PaymentMethod):
    def pay(self) -> str:
        return "ডেলিভারির সময় নগদ"


payments: list[PaymentMethod] = [
    MobileWallet(55000, "017xxxxxxxx"),
    Card(120000, "4242"),
    CashOnDelivery(30000),
]
for p in payments:
    print(p.receipt())     # প্রতিটা নিজের pay() চালায়: polymorphism
PaymentMethod amount, pay(), receipt() MobileWallet phone, pay() Card last4, pay() CashOnDelivery pay()
প্রতিটা subclass "একটা ধরনের" PaymentMethod। Code শুধু p.receipt() ডাকে, কোন ধরনের payment সেটা জানার দরকার নেই।

নতুন payment method (যেমন bank transfer) যোগ করতে শুধু নতুন একটা class লিখতে হবে, পুরনো code ছুঁতে হবে না। এটাই polymorphism এর শক্তি।

#Abstract class: subclass কে বাধ্য করা

উপরে raise NotImplementedError দিয়েছি, কিন্তু কেউ pay() লিখতে ভুলে গেলে error আসবে কেবল চালানোর সময়। ABC দিয়ে আরও আগে ধরা যায়:

Python
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self) -> str: ...

class Broken(PaymentMethod):
    pass

Broken()   # TypeError: Can't instantiate abstract class Broken with abstract method pay

#Composition: "এর আছে"

Inheritance হলো "is-a" (Card একটা PaymentMethod)। Composition হলো "has-a" (Order এর আছে অনেকগুলো OrderItem)। উপরের Order class এ items: list[OrderItem] হলো composition।

নিয়ম: composition প্রথমে ভাবো, inheritance পরে। Inheritance এর গভীর শিকল (A → B → C → D) বোঝা আর বদলানো কঠিন, parent বদলালে সব child এ অপ্রত্যাশিত প্রভাব পড়ে। Flutter এও তুমি এটা দেখেছো: widget বানাও অন্য widget কে জুড়ে (composition), Container থেকে inherit করে না।

Python
# খারাপ: notification পাঠানোর ক্ষমতার জন্য inheritance
class OrderWithEmail(Order, EmailSender): ...

# ভালো: notification পাঠানোর জিনিসটা বাইরে থেকে দেওয়া
class OrderService:
    def __init__(self, notifier: "Notifier") -> None:
        self.notifier = notifier

    def place(self, order: Order) -> None:
        # ... order save
        self.notifier.send(order.customer, "আপনার order নেওয়া হয়েছে")

এখানে OrderService জানে না notification SMS এ যাবে নাকি email এ নাকি push এ। যেটা দেওয়া হবে সেটাই ব্যবহার করবে। এই কৌশলের নাম dependency injection, আর FastAPI এর পুরো গঠন এর উপর দাঁড়ানো। Test এ আসল SMS না পাঠিয়ে একটা নকল notifier দেওয়া যায়।

#@classmethod আর @staticmethod

Python
from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str

    @classmethod
    def from_dict(cls, data: dict) -> "User":      # বিকল্প constructor
        return cls(name=data["full_name"], email=data["email"].lower())

    @staticmethod
    def is_valid_email(email: str) -> bool:        # class এর সাথে সম্পর্কিত, কিন্তু object লাগে না
        return "@" in email and "." in email.split("@")[-1]

u = User.from_dict({"full_name": "Sara", "email": "SARA@X.COM"})
print(User.is_valid_email("abc@test.com"))

Dart এর factory User.fromJson(...) এর মতো কাজ করে @classmethod।

#অনুশীলন

#নিজেকে যাচাই করো

`self` আসলে কী?

যে object এর উপর method ডাকা হয়েছে সেটা। acc.deposit(500) আসলে BankAccount.deposit(acc, 500), তাই self হলো acc।

Class attribute আর instance attribute এর পার্থক্য কী?

Class attribute class এর ভেতরে সরাসরি লেখা হয় আর সব object শেয়ার করে (bank_name)। Instance attribute self.x = ... দিয়ে __init__ এ বানানো হয়, প্রতিটা object এর আলাদা (balance)।

dataclass এ list field এর default কীভাবে দেবে, আর কেন?

field(default_factory=list) দিয়ে। সরাসরি = [] দিলে সব object একই list শেয়ার করতো (mutable default ফাঁদ), dataclass এটা error দিয়েই আটকায়। default_factory প্রতিটা নতুন object এর জন্য নতুন list বানায়।

Inheritance আর composition এর পার্থক্য উদাহরণসহ বলো। কোনটা আগে ভাববে?

Inheritance "is-a": Card একটা PaymentMethod। Composition "has-a": Order এর আছে অনেকগুলো OrderItem, বা OrderService এর আছে একটা Notifier। সাধারণত composition আগে ভাবো, কারণ এটা নমনীয়, test করা সহজ, আর parent বদলের অপ্রত্যাশিত প্রভাব থাকে না।

Order এর status string হিসেবে না রেখে Enum কেন রাখবে?

String এ বানান ভুল ("confirmd") চুপচাপ চলে যায় আর পরে bug হয়। Enum এ শুধু নির্দিষ্ট value গুলোই সম্ভব, ভুল নাম লিখলে সাথে সাথে error আর editor এ auto-complete পাওয়া যায়।

চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।