OOP: Class, Inheritance, Dataclass
Object দিয়ে বাস্তব জিনিস modeling করা
Flutter এ সবকিছুই class: StatelessWidget, ChangeNotifier, model class। তাই Object-Oriented Programming (OOP) তোমার কাছে নতুন না। এই অধ্যায়ে দেখবো Python এ class কীভাবে লেখে, Dart থেকে কোথায় আলাদা, আর dataclass দিয়ে কীভাবে অনেক কম লিখে একই কাজ করা যায়। শেষে দেখবো কখন inheritance আর কখন composition।
#প্রথম class
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 BankDart এর সাথে পার্থক্য:
| 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
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 না হয়:
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 এর সাধারণ জিনিসের মতো আচরণ করানো যায়:
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:
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:
@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
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: "একটা ধরনের"
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() চালায়: polymorphismp.receipt() ডাকে, কোন ধরনের payment সেটা জানার দরকার নেই।নতুন payment method (যেমন bank transfer) যোগ করতে শুধু নতুন একটা class লিখতে হবে, পুরনো code ছুঁতে হবে না। এটাই polymorphism এর শক্তি।
#Abstract class: subclass কে বাধ্য করা
উপরে raise NotImplementedError দিয়েছি, কিন্তু কেউ pay() লিখতে ভুলে গেলে error আসবে কেবল চালানোর সময়। ABC দিয়ে আরও আগে ধরা যায়:
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 করে না।
# খারাপ: 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
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 ভরে যাবে।