Part 01 · Low Level Design
OOP Basics: Classes, Objects and the Four Pillars
Data আর behaviour একসাথে রাখা: object এর state পাহারা দেওয়া, interface বনাম abstract class, এক call এ অনেক রকম কাজ, inheritance এর ফাঁদ, আর value object বনাম entity।
⏱ 2-3 ঘণ্টা · Station 2 / 80
Balance যখন minus এ চলে গেল
একটা ছোট wallet app। প্রতিটা user এর balance একটা struct এ রাখা, field টা public। Withdraw screen টাকা কাটার আগে check করে "যথেষ্ট টাকা আছে তো?"। সব ঠিকঠাক চলছে।
তারপর কেউ একজন refund এর একটা script লিখলো। Field টা হাতের কাছেই, তাই সে সরাসরি লিখে দিলো wallet.balance = wallet.balance - fee। কোনো check নেই। এক সপ্তাহ পর finance দেখে কিছু wallet এ balance −৩৪০ টাকা।
"Balance minus হতে পারবে" এমন কথা কেউ কোথাও লেখেনি। Code শুধু যে কাউকে যেকোনো জায়গা থেকে সংখ্যাটা বদলাতে দিয়েছে। Rule ছিল একটা screen এ, কিন্তু data খোলা ছিল সবার জন্য।
Object-oriented programming এর বড় কাজটাই হলো এটা আটকানো: data আর তাকে রক্ষা করার rule একসাথে রাখা, আর কে কী ছুঁতে পারবে সেটা ঠিক করে দেওয়া।
সমস্যা: data এক জায়গায়, rule আরেক জায়গায়
Procedural code এ data থাকে সাধারণ structure এ, আর program এর সব জায়গার function সেটা বদলায়। ছোট script এ এটা চলে। কিন্তু system বড় হলে তিনটা ঝামেলা হয়:
| যা দেখা যায় | আসল কারণ |
|---|---|
| "শুধু এবারের মতো" একটা rule বাদ পড়ে যায় | Rule এর ভেতর দিয়ে না গিয়েও data বদলানো যায় |
| নতুন একটা payment method আনতে দশটা file ধরতে হয় | সব জায়গায় code জিজ্ঞেস করছে "card না bKash না cash?" |
| একই wallet এর দুইটা copy দুই রকম কথা বলে | দুইটা wallet কখন "একই", সেটা কেউ ঠিক করেনি |
OOP এর জন্য চারটা tool দেয়, যাদের সবাই four pillar বলে। সাথে আরেকটা জিনিস যেটা interview তে খুব আসে: value আর entity এর পার্থক্য।
ব্যাংকের counter এর কথা ভাবো
ব্যাংকে গিয়ে তুমি counter এর পেছনে ঢুকে নিজের হাতে খাতায় balance বদলাও না। Counter এ গিয়ে বলো "৫০০ টাকা তুলবো"। Teller তোমার balance আর পরিচয় দেখে, তারপর খাতায় লেখে। খাতাটা private; counter টা হলো public interface।
Bill দেওয়ার সময়ও counter মাথা ঘামায় না তুমি কীভাবে দিচ্ছো। Card, mobile wallet, cash সব যায় একই "pay" desk এ, আর পেছনে প্রতিটা নিজের মতো করে handle হয়।
তবে একটা জায়গায় মেলে না। আসল teller নিজের বুদ্ধি খাটাতে পারে। Object পারে না: তুমি method এ যে rule লিখেছো, শুধু সেটাই মানে। কোনো method এ check লিখতে ভুলে গেলে object খুব ভদ্রভাবে ভুল কাজটাই করে ফেলবে।
Wallet এর balance private, আর Withdraw method balance কম হলে টাকা দেয় না। এখন refund script কে একটা wallet থেকে ২০ টাকা fee কাটতে হবে। ঠিক উপায় কোনটা?
চারটা pillar আর দুই রকম object
- Class আর object। Class হলো একটা ছাঁচ: কোন data আর কোন কাজ একসাথে থাকবে। Object হলো সেই ছাঁচ থেকে বানানো একটা আসল জিনিস, তার নিজের data সহ।
Walletহলো class; রহিমের ৫০০ টাকার wallet টা একটা object। - Encapsulation মানে data লুকিয়ে রাখা, আর বাইরে শুধু এমন method দেওয়া যেগুলো data কে ঠিক রাখে। Caller object কে কোনো অসম্ভব অবস্থায় নিতে পারে না, কারণ ঢোকার প্রতিটা দরজায় rule check হয়। Go তে এটা ছোট হাতের field, Java/C++ এ
private, TypeScript এ#field, Rust এpubছাড়া field, Python এ_nameএর একটা চল। - Abstraction মানে caller নির্ভর করে জিনিসটা কী করে তার উপর, কীভাবে করে তার উপর না। Checkout জানে "payment method টাকা দিতে পারে, আর তার একটা নাম আছে"। Card network বা bKash এর API নিয়ে সে কিছুই জানে না। Code এ এটা interface (Go, Java, TS), trait (Rust), abstract class (C++), বা protocol (Python)।
- Polymorphism মানে এক call, অনেক রকম কাজ:
pm.Pay(amount)চালাবে card এর code, নাকি bKash এর, নাকি cash এর, সেটা নির্ভর করেpmএর ভেতরে কোন object আছে তার উপর। Caller এ type দেখে কোনোifনেই, তাই নতুন payment method আনলে caller এ হাত দিতে হয় না। - Inheritance মানে একটা class আরেকটার একটা বিশেষ রূপ হয়ে তার code reuse করে (
SavingsAccount is an Account)। জিনিসটা শক্তিশালী, কিন্তু দুইটা class কে শক্ত করে বেঁধে দেয়, আর ভুল ব্যবহার করা খুব সহজ।Bkash extends Walletশুনতে ঠিক লাগে, কিন্তু bKash payment কোনো wallet না; সে একটা wallet ব্যবহার করে। Interface আর composition বেশি ভালো; কেন, সেটা পরের chapter এ। - Value বনাম entity। Value object (Money, Address, DateRange) এর কোনো পরিচয় নেই: দুইটা ৫ টাকা একই জিনিস, আর value কখনো বদলায় না; বদলাতে হলে নতুন একটা বানাও। Entity (Wallet, Order, User) এর একটা পরিচয় আছে, data বদলালেও সেটা একই থাকে: অনেকবার টাকা তোলার পরেও wallet W-1 সেই W-1 ই। Value মেলাও ভেতরের data দিয়ে; entity মেলাও ID দিয়ে।
একটা payment এর ভেতরে: কে কী ঠিক করে
checkout(Bkash, ৩০০ টাকা)
ধাপ 1/5 Checkout একটা call দেয়
checkout() ডাকে pm.Pay(300)। এটা কোন payment method, সে জানেও না, জিজ্ঞেসও করে না।
- call
- pm.Pay(300 tk)
প্রতিটা class কে যে দুইটা প্রশ্ন করবে
- এই object এর ব্যাপারে কোন কথাটা সবসময় সত্যি থাকতে হবে? (Wallet এর balance কখনো minus না। Loan এর সবসময় একটা due date থাকে।) Field গুলো private করো, আর শুধু এমন method কে বদলাতে দাও যেগুলো এই কথাগুলো ঠিক রাখে।
- এটা value না entity? একই data র দুইটা copy যদি একটা আরেকটার জায়গায় চলে, তাহলে value: immutable বানাও, data দিয়ে মেলাও। আর যদি এর নিজের একটা জীবন থাকে, সময়ের সাথে বদলায়, তাহলে entity: একটা ID দাও, ID দিয়ে মেলাও।
নিজে বলো: value না entity?
Value object না entity?
Money{paisa: 50000}Code এ wallet আর payment
Money একটা value object, Wallet একটা entity যার balance private, আর তিনটা payment method একটা interface এর পেছনে। Program টা প্রতিটা method দিয়ে payment করে, একবার balance এ টাকা কম পড়ে, তারপর একটা value আর একটা entity মিলিয়ে দেখে।
package main
import (
"errors"
"fmt"
)
// Money is a value object: no identity, never changes, equal when the amounts are equal.
type Money struct{ paisa int64 }
func Taka(t int64) Money { return Money{t * 100} }
func (m Money) Add(o Money) Money { return Money{m.paisa + o.paisa} }
func (m Money) Sub(o Money) Money { return Money{m.paisa - o.paisa} }
func (m Money) Less(o Money) bool { return m.paisa < o.paisa }
func (m Money) Percent(bp int64) Money { return Money{m.paisa * bp / 10000} } // bp = basis points
func (m Money) String() string { return fmt.Sprintf("%d.%02d tk", m.paisa/100, m.paisa%100) }
var ErrInsufficient = errors.New("insufficient balance")
// Wallet is an entity: it has an ID and its balance changes over time.
// Encapsulation: balance is private, so the only way to change it is a method that checks the rules.
type Wallet struct {
ID string
balance Money
}
func (w *Wallet) Deposit(m Money) { w.balance = w.balance.Add(m) }
func (w *Wallet) Balance() Money { return w.balance }
func (w *Wallet) Withdraw(m Money) error {
if w.balance.Less(m) {
return ErrInsufficient
}
w.balance = w.balance.Sub(m)
return nil
}
// Abstraction: checkout only knows "something that can pay". It never asks which kind.
type PaymentMethod interface {
Name() string
Pay(amount Money) (fee Money, err error)
}
type Card struct{}
func (Card) Name() string { return "card" }
func (Card) Pay(a Money) (Money, error) { return a.Percent(150), nil } // 1.5%
type Bkash struct{ wallet *Wallet }
func (Bkash) Name() string { return "bkash" }
func (b Bkash) Pay(a Money) (Money, error) {
fee := a.Percent(185) // 1.85%
if err := b.wallet.Withdraw(a.Add(fee)); err != nil {
return Money{}, err
}
return fee, nil
}
type Cash struct{}
func (Cash) Name() string { return "cash" }
func (Cash) Pay(Money) (Money, error) { return Money{}, nil }
// Polymorphism: one call, and each payment method does its own thing.
func checkout(pm PaymentMethod, amount Money) {
fee, err := pm.Pay(amount)
if err != nil {
fmt.Printf("%-6s pays %s: %v\n", pm.Name(), amount, err)
return
}
fmt.Printf("%-6s pays %s, fee %s\n", pm.Name(), amount, fee)
}
func main() {
w := &Wallet{ID: "W-1"}
w.Deposit(Taka(500))
fmt.Printf("wallet %s balance %s\n", w.ID, w.Balance())
checkout(Card{}, Taka(1200))
checkout(Bkash{w}, Taka(300))
fmt.Printf("wallet %s balance %s\n", w.ID, w.Balance())
checkout(Bkash{w}, Taka(300))
checkout(Cash{}, Taka(80))
fmt.Println("Taka(5) == Taka(5):", Taka(5) == Taka(5))
// Entities are the same when their IDs match, even if other fields differ.
loaded := &Wallet{ID: "W-1"} // e.g. the same wallet loaded again from the database
fmt.Println("same wallet (by ID):", w.ID == loaded.ID)
}from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class Money:
"""A value object: no identity, never changes, equal when the amounts are equal."""
paisa: int
@staticmethod
def taka(t: int) -> Money:
return Money(t * 100)
def add(self, o: Money) -> Money:
return Money(self.paisa + o.paisa)
def sub(self, o: Money) -> Money:
return Money(self.paisa - o.paisa)
def percent(self, bp: int) -> Money: # bp = basis points
return Money(self.paisa * bp // 10000)
def __str__(self) -> str:
return f"{self.paisa // 100}.{self.paisa % 100:02d} tk"
class InsufficientBalance(Exception):
def __str__(self) -> str:
return "insufficient balance"
class Wallet:
"""An entity: it has an ID and its balance changes over time.
Encapsulation: _balance is private by convention; change it only through methods."""
def __init__(self, id: str):
self.id = id
self._balance = Money(0)
def deposit(self, m: Money) -> None:
self._balance = self._balance.add(m)
@property
def balance(self) -> Money:
return self._balance
def withdraw(self, m: Money) -> None:
if self._balance.paisa < m.paisa:
raise InsufficientBalance()
self._balance = self._balance.sub(m)
class PaymentMethod(Protocol):
"""Abstraction: checkout only knows 'something that can pay'."""
name: str
def pay(self, amount: Money) -> Money: ... # returns the fee
class Card:
name = "card"
def pay(self, amount: Money) -> Money:
return amount.percent(150) # 1.5%
class Bkash:
name = "bkash"
def __init__(self, wallet: Wallet):
self.wallet = wallet
def pay(self, amount: Money) -> Money:
fee = amount.percent(185) # 1.85%
self.wallet.withdraw(amount.add(fee))
return fee
class Cash:
name = "cash"
def pay(self, amount: Money) -> Money:
return Money(0)
def checkout(pm: PaymentMethod, amount: Money) -> None:
"""Polymorphism: one call, and each payment method does its own thing."""
try:
fee = pm.pay(amount)
print(f"{pm.name:<6} pays {amount}, fee {fee}")
except InsufficientBalance as e:
print(f"{pm.name:<6} pays {amount}: {e}")
w = Wallet("W-1")
w.deposit(Money.taka(500))
print(f"wallet {w.id} balance {w.balance}")
checkout(Card(), Money.taka(1200))
checkout(Bkash(w), Money.taka(300))
print(f"wallet {w.id} balance {w.balance}")
checkout(Bkash(w), Money.taka(300))
checkout(Cash(), Money.taka(80))
print("Taka(5) == Taka(5):", str(Money.taka(5) == Money.taka(5)).lower())
# Entities are the same when their IDs match, even if other fields differ.
loaded = Wallet("W-1") # e.g. the same wallet loaded again from the database
print("same wallet (by ID):", str(w.id == loaded.id).lower())// A value object: no identity, never changes, equal when the amounts are equal.
class Money {
readonly paisa: number;
constructor(paisa: number) {
this.paisa = paisa;
}
static taka(t: number): Money {
return new Money(t * 100);
}
add(o: Money): Money {
return new Money(this.paisa + o.paisa);
}
sub(o: Money): Money {
return new Money(this.paisa - o.paisa);
}
percent(bp: number): Money {
return new Money(Math.floor((this.paisa * bp) / 10000)); // bp = basis points
}
equals(o: Money): boolean {
return this.paisa === o.paisa;
}
toString(): string {
return `${Math.floor(this.paisa / 100)}.${String(this.paisa % 100).padStart(2, "0")} tk`;
}
}
class InsufficientBalance extends Error {
constructor() {
super("insufficient balance");
}
}
// An entity: it has an ID and its balance changes over time.
// Encapsulation: #balance is truly private; change it only through methods.
class Wallet {
readonly id: string;
#balance = new Money(0);
constructor(id: string) {
this.id = id;
}
deposit(m: Money): void {
this.#balance = this.#balance.add(m);
}
get balance(): Money {
return this.#balance;
}
withdraw(m: Money): void {
if (this.#balance.paisa < m.paisa) throw new InsufficientBalance();
this.#balance = this.#balance.sub(m);
}
}
// Abstraction: checkout only knows "something that can pay".
interface PaymentMethod {
readonly name: string;
pay(amount: Money): Money; // returns the fee
}
class Card implements PaymentMethod {
readonly name = "card";
pay(amount: Money): Money {
return amount.percent(150); // 1.5%
}
}
class Bkash implements PaymentMethod {
readonly name = "bkash";
readonly wallet: Wallet;
constructor(wallet: Wallet) {
this.wallet = wallet;
}
pay(amount: Money): Money {
const fee = amount.percent(185); // 1.85%
this.wallet.withdraw(amount.add(fee));
return fee;
}
}
class Cash implements PaymentMethod {
readonly name = "cash";
pay(): Money {
return new Money(0);
}
}
// Polymorphism: one call, and each payment method does its own thing.
function checkout(pm: PaymentMethod, amount: Money): void {
try {
const fee = pm.pay(amount);
console.log(`${pm.name.padEnd(6)} pays ${amount}, fee ${fee}`);
} catch (e) {
console.log(`${pm.name.padEnd(6)} pays ${amount}: ${(e as Error).message}`);
}
}
const w = new Wallet("W-1");
w.deposit(Money.taka(500));
console.log(`wallet ${w.id} balance ${w.balance}`);
checkout(new Card(), Money.taka(1200));
checkout(new Bkash(w), Money.taka(300));
console.log(`wallet ${w.id} balance ${w.balance}`);
checkout(new Bkash(w), Money.taka(300));
checkout(new Cash(), Money.taka(80));
console.log("Taka(5) == Taka(5):", Money.taka(5).equals(Money.taka(5)));
// Entities are the same when their IDs match, even if other fields differ.
const loaded = new Wallet("W-1"); // e.g. the same wallet loaded again from the database
console.log("same wallet (by ID):", w.id === loaded.id);use std::fmt;
// A value object: Copy + PartialEq. No identity, never changes, equal when amounts are equal.
#[derive(Clone, Copy, PartialEq, Debug)]
struct Money { paisa: i64 }
impl Money {
fn taka(t: i64) -> Money { Money { paisa: t * 100 } }
fn add(self, o: Money) -> Money { Money { paisa: self.paisa + o.paisa } }
fn sub(self, o: Money) -> Money { Money { paisa: self.paisa - o.paisa } }
fn percent(self, bp: i64) -> Money { Money { paisa: self.paisa * bp / 10000 } } // bp = basis points
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}.{:02} tk", self.paisa / 100, self.paisa % 100)
}
}
// An entity: it has an ID and its balance changes over time.
// Encapsulation: `balance` is private to this module; change it only through methods.
struct Wallet { id: String, balance: Money }
impl Wallet {
fn new(id: &str) -> Wallet { Wallet { id: id.to_string(), balance: Money { paisa: 0 } } }
fn deposit(&mut self, m: Money) { self.balance = self.balance.add(m); }
fn balance(&self) -> Money { self.balance }
fn withdraw(&mut self, m: Money) -> Result<(), String> {
if self.balance.paisa < m.paisa {
return Err("insufficient balance".into());
}
self.balance = self.balance.sub(m);
Ok(())
}
}
// Abstraction: checkout only knows "something that can pay".
trait PaymentMethod {
fn name(&self) -> &str;
fn pay(&mut self, amount: Money) -> Result<Money, String>; // returns the fee
}
struct Card;
impl PaymentMethod for Card {
fn name(&self) -> &str { "card" }
fn pay(&mut self, a: Money) -> Result<Money, String> { Ok(a.percent(150)) } // 1.5%
}
struct Bkash<'a> { wallet: &'a mut Wallet }
impl PaymentMethod for Bkash<'_> {
fn name(&self) -> &str { "bkash" }
fn pay(&mut self, a: Money) -> Result<Money, String> {
let fee = a.percent(185); // 1.85%
self.wallet.withdraw(a.add(fee))?;
Ok(fee)
}
}
struct Cash;
impl PaymentMethod for Cash {
fn name(&self) -> &str { "cash" }
fn pay(&mut self, _: Money) -> Result<Money, String> { Ok(Money { paisa: 0 }) }
}
// Polymorphism: one call (through a trait object), and each method does its own thing.
fn checkout(pm: &mut dyn PaymentMethod, amount: Money) {
match pm.pay(amount) {
Ok(fee) => println!("{:<6} pays {amount}, fee {fee}", pm.name()),
Err(e) => println!("{:<6} pays {amount}: {e}", pm.name()),
}
}
fn main() {
let mut w = Wallet::new("W-1");
w.deposit(Money::taka(500));
println!("wallet {} balance {}", w.id, w.balance());
checkout(&mut Card, Money::taka(1200));
checkout(&mut Bkash { wallet: &mut w }, Money::taka(300));
println!("wallet {} balance {}", w.id, w.balance());
checkout(&mut Bkash { wallet: &mut w }, Money::taka(300));
checkout(&mut Cash, Money::taka(80));
println!("Taka(5) == Taka(5): {}", Money::taka(5) == Money::taka(5));
// Entities are the same when their IDs match, even if other fields differ.
let loaded = Wallet::new("W-1"); // e.g. the same wallet loaded again from the database
println!("same wallet (by ID): {}", w.id == loaded.id);
}public class Main {
// A value object: a record is immutable and compares by its fields.
record Money(long paisa) {
static Money taka(long t) { return new Money(t * 100); }
Money add(Money o) { return new Money(paisa + o.paisa); }
Money sub(Money o) { return new Money(paisa - o.paisa); }
Money percent(long bp) { return new Money(paisa * bp / 10000); } // bp = basis points
@Override public String toString() { return String.format("%d.%02d tk", paisa / 100, paisa % 100); }
}
static final class InsufficientBalance extends Exception {
private static final long serialVersionUID = 1L;
InsufficientBalance() { super("insufficient balance"); }
}
// An entity: it has an ID and its balance changes over time.
// Encapsulation: balance is private; change it only through methods.
static final class Wallet {
final String id;
private Money balance = new Money(0);
Wallet(String id) { this.id = id; }
void deposit(Money m) { balance = balance.add(m); }
Money balance() { return balance; }
void withdraw(Money m) throws InsufficientBalance {
if (balance.paisa() < m.paisa()) throw new InsufficientBalance();
balance = balance.sub(m);
}
}
// Abstraction: checkout only knows "something that can pay".
interface PaymentMethod {
String name();
Money pay(Money amount) throws InsufficientBalance; // returns the fee
}
static final class Card implements PaymentMethod {
public String name() { return "card"; }
public Money pay(Money a) { return a.percent(150); } // 1.5%
}
static final class Bkash implements PaymentMethod {
private final Wallet wallet;
Bkash(Wallet wallet) { this.wallet = wallet; }
public String name() { return "bkash"; }
public Money pay(Money a) throws InsufficientBalance {
Money fee = a.percent(185); // 1.85%
wallet.withdraw(a.add(fee));
return fee;
}
}
static final class Cash implements PaymentMethod {
public String name() { return "cash"; }
public Money pay(Money a) { return new Money(0); }
}
// Polymorphism: one call, and each payment method does its own thing.
static void checkout(PaymentMethod pm, Money amount) {
try {
Money fee = pm.pay(amount);
System.out.printf("%-6s pays %s, fee %s%n", pm.name(), amount, fee);
} catch (InsufficientBalance e) {
System.out.printf("%-6s pays %s: %s%n", pm.name(), amount, e.getMessage());
}
}
public static void main(String[] args) {
Wallet w = new Wallet("W-1");
w.deposit(Money.taka(500));
System.out.printf("wallet %s balance %s%n", w.id, w.balance());
checkout(new Card(), Money.taka(1200));
checkout(new Bkash(w), Money.taka(300));
System.out.printf("wallet %s balance %s%n", w.id, w.balance());
checkout(new Bkash(w), Money.taka(300));
checkout(new Cash(), Money.taka(80));
System.out.println("Taka(5) == Taka(5): " + Money.taka(5).equals(Money.taka(5)));
// Entities are the same when their IDs match, even if other fields differ.
Wallet loaded = new Wallet("W-1"); // e.g. the same wallet loaded again from the database
System.out.println("same wallet (by ID): " + w.id.equals(loaded.id));
}
}#include <cstdio>
#include <stdexcept>
#include <string>
// A value object: no identity, never changes, equal when the amounts are equal.
struct Money {
long long paisa = 0;
static Money taka(long long t) { return {t * 100}; }
Money add(Money o) const { return {paisa + o.paisa}; }
Money sub(Money o) const { return {paisa - o.paisa}; }
Money percent(long long bp) const { return {paisa * bp / 10000}; } // bp = basis points
bool operator==(const Money&) const = default;
std::string str() const {
char buf[32];
std::snprintf(buf, sizeof buf, "%lld.%02lld tk", paisa / 100, paisa % 100);
return buf;
}
};
struct InsufficientBalance : std::runtime_error {
InsufficientBalance() : std::runtime_error("insufficient balance") {}
};
// An entity: it has an ID and its balance changes over time.
// Encapsulation: balance_ is private; change it only through methods.
class Wallet {
public:
explicit Wallet(std::string id) : id(std::move(id)) {}
const std::string id;
void deposit(Money m) { balance_ = balance_.add(m); }
Money balance() const { return balance_; }
void withdraw(Money m) {
if (balance_.paisa < m.paisa) throw InsufficientBalance();
balance_ = balance_.sub(m);
}
private:
Money balance_;
};
// Abstraction: checkout only knows "something that can pay".
class PaymentMethod {
public:
virtual ~PaymentMethod() = default;
virtual const char* name() const = 0;
virtual Money pay(Money amount) = 0; // returns the fee
};
class Card : public PaymentMethod {
public:
const char* name() const override { return "card"; }
Money pay(Money a) override { return a.percent(150); } // 1.5%
};
class Bkash : public PaymentMethod {
public:
explicit Bkash(Wallet& w) : wallet_(w) {}
const char* name() const override { return "bkash"; }
Money pay(Money a) override {
Money fee = a.percent(185); // 1.85%
wallet_.withdraw(a.add(fee));
return fee;
}
private:
Wallet& wallet_;
};
class Cash : public PaymentMethod {
public:
const char* name() const override { return "cash"; }
Money pay(Money) override { return {}; }
};
// Polymorphism: one call (a virtual function), and each method does its own thing.
void checkout(PaymentMethod& pm, Money amount) {
try {
Money fee = pm.pay(amount);
std::printf("%-6s pays %s, fee %s\n", pm.name(), amount.str().c_str(), fee.str().c_str());
} catch (const InsufficientBalance& e) {
std::printf("%-6s pays %s: %s\n", pm.name(), amount.str().c_str(), e.what());
}
}
int main() {
Wallet w("W-1");
w.deposit(Money::taka(500));
std::printf("wallet %s balance %s\n", w.id.c_str(), w.balance().str().c_str());
Card card;
Bkash bkash(w);
Cash cash;
checkout(card, Money::taka(1200));
checkout(bkash, Money::taka(300));
std::printf("wallet %s balance %s\n", w.id.c_str(), w.balance().str().c_str());
checkout(bkash, Money::taka(300));
checkout(cash, Money::taka(80));
std::printf("Taka(5) == Taka(5): %s\n", Money::taka(5) == Money::taka(5) ? "true" : "false");
// Entities are the same when their IDs match, even if other fields differ.
Wallet loaded("W-1"); // e.g. the same wallet loaded again from the database
std::printf("same wallet (by ID): %s\n", w.id == loaded.id ? "true" : "false");
}ছয়টাই হুবহু এটা print করে:
wallet W-1 balance 500.00 tk
card pays 1200.00 tk, fee 18.00 tk
bkash pays 300.00 tk, fee 5.55 tk
wallet W-1 balance 194.45 tk
bkash pays 300.00 tk: insufficient balance
cash pays 80.00 tk, fee 0.00 tk
Taka(5) == Taka(5): true
same wallet (by ID): true
Output টা পড়ি:
- card: ১২০০ এর 1.5% হলো ১৮ টাকা। Card wallet ছোঁয়ই না; ওটা তার নিজের ব্যাপার।
- bkash, প্রথমবার: fee 5.55, wallet থেকে যায় 305.55, balance 500.00 থেকে নেমে 194.45। টাকা রাখা হচ্ছে paisa তে, তাই float এর মতো ভগ্নাংশের গোলমাল নেই।
- bkash, দ্বিতীয়বার: 305.55 লাগবে, আছে 194.45, তাই
Withdrawমানা করে দেয়, আর error টাPayহয়ে checkout এ ফিরে আসে। Balance কখনো minus এ যায়নি, আর কেন হলো না সেটা checkout কে জানতেও হয়নি। - cash: কোনো fee নেই। তিন রকম কাজ, অথচ
checkoutএ type নিয়ে একটাওifনেই। - Value বনাম entity: আলাদা আলাদা বানানো দুইটা
Taka(5)সমান। দুইটাWalletobject একই wallet শুধু এই কারণে যে দুটোর IDW-1। - Language ভেদে কী আলাদা: TypeScript এ
#balance, যেটা সত্যিকারের private field। Python শুধু_balanceদিয়ে বোঝাতে পারে যে এটা private, তাই property আর method গুলোই আসল নিয়ম। Rust এBkashwallet টা mutable borrow করে (&'a mut Wallet), তাই একসাথে দুইটা payment wallet ছুঁতে চাইলে compiler আটকে দেয়। Java রrecordআর Rust এর#[derive(PartialEq)]value equality এমনিতেই দিয়ে দেয়; C++ এ চাইতে হয়operator== = defaultলিখে।
Real code এ OOP যেখানে ভুল হয়
- প্রতিটা field এর জন্য getter আর setter মানেই encapsulation না।
SetBalance(x)আসলে public field ই, শুধু একটু ঘুরিয়ে। Raw field না, এমন কাজ বাইরে দাও (Withdraw,Deposit) যেগুলো rule মেনে চলে। - টাকার জন্য integer paisa (বা decimal type) ব্যবহার করো। Floating point এ
0.1 + 0.2আসলে0.3না; payment code এ এটা খুব চেনা একটা bug। - Value object immutable রাখো।
Moneyযদি নিজে বদলাতে পারতো, একইMoneyshare করা দুইটা wallet একসাথে বদলে যেতো। নতুন value ফেরত দিলে এই ধরনের bug আর হয়ই না। - প্রথম দিন থেকেই entity কে একটা স্থায়ী ID দাও, আর ID দিয়েই মেলাও। Database থেকে একই order দুইবার load করলে দুইটা object পাবে, কিন্তু order তো একটাই।
- Interface আনো যেখানে কাজটা সত্যিই আলাদা (payment method, notification channel), সব class এর জন্য না। একটাই implementation এর পেছনে interface বেশিরভাগ সময় শুধু বাড়তি ঝামেলা।
যে design গুলো পরে problem create করে
ভাঙা design টা বাছো
wallet.Balance -= fee // in the refund script
একটু থেমে ভাবো
একজন teammate বললো: "Wallet এ GetBalance() আর SetBalance() দিয়ে দিই। Field তো private থাকলো, তাহলে encapsulation হয়ে গেল না?"
উত্তর দেখো
আসলে না। SetBalance দিয়ে যে কেউ যেকোনো সংখ্যা বসাতে পারে, minus ও। তাই "কখনো minus না" rule টা এখনো সুরক্ষিত না। Field private ঠিকই, কিন্তু যে কথাটা সবসময় সত্যি থাকার কথা, সেটা খোলা। Encapsulation মানে এমন কাজ বাইরে দেওয়া যেগুলো object কে ঠিক রাখে: Deposit, Withdraw, হয়তো ApplyFee। শুধু পড়ার জন্য Balance() ঠিক আছে; যা খুশি বসানোর setter মানে সেই পুরনো public field, শুধু ছদ্মবেশে।
ছোট্ট quiz
১. Encapsulation আর abstraction এর পার্থক্য কী?
উত্তর
Encapsulation object এর data লুকায় আর এমন method দিয়ে পাহারা দেয় যেগুলো data কে ঠিক রাখে। Abstraction একটা interface এর পেছনে লুকায় তুমি কোন implementation এর সাথে কথা বলছো, যাতে caller নির্ভর করে জিনিসটা কী করে তার উপর, কীভাবে করে তার উপর না।
২. Card না bKash, এটা দেখার জন্য checkout এ if লাগে না কেন?
উত্তর
Polymorphism। সে PaymentMethod interface এর Pay ডাকে, আর পেছনে যে আসল type আছে, language তার method টা চালায়।
৩. Delivery এর ঠিকানা সাধারণত value object না entity? কেন?
উত্তর
Value object: একই field এর দুইটা ঠিকানা একটা আরেকটার জায়গায় চলে, আর বদলালে edit না করে নতুন একটা বসানো হয়। (তোমার product যদি ঠিকানাকে নাম দেওয়া, ইতিহাস সহ save করা record হিসেবে রাখে, তাহলে সেটা entity হতে পারে; domain ই ঠিক করবে।)
৪. Bkash extends Wallet কেন খারাপ design?
উত্তর
bKash payment কোনো ধরনের wallet না; সে একটা wallet ব্যবহার করে। Inheritance দিলে payment এর হাতে Wallet এর সব public method চলে আসে, আর দুইটা class শক্ত করে বাঁধা পড়ে যায়। একটা wallet এর reference রাখলে (composition) আসল ব্যাপারটাই বোঝা যায়।
তোমার challenge: নতুন একটা payment method
একটা Nagad payment method যোগ করো, 1.5% fee, আর bKash এর মতোই wallet থেকে টাকা কাটবে।
- Type টা বানাও, আর এটা যেন
PaymentMethodমেনে চলে।checkoutএ হাত দেবে না। - গল্পের শেষে wallet এ ১০০০ টাকা জমা দাও, তারপর Nagad দিয়ে ২০০ টাকা pay করো।
চালানোর আগে আন্দাজ করো fee কত আসবে আর balance কত থাকবে।
হয়েছে বুঝবে যখন নতুন লাইন দুইটা হবে nagad pays 200.00 tk, fee 3.00 tk আর wallet W-1 balance 991.45 tk, আগের সব লাইন একই থাকবে, আর checkout এ একটা লাইনও নতুন লাগবে না।
এক কথায়
Data আর তার rule একসাথে আর private রাখো (encapsulation), জিনিস কী করে তার উপর নির্ভর করো, কীভাবে করে তার উপর না (abstraction), একটা call কে আলাদা type এর জন্য আলাদা কাজ করতে দাও (polymorphism), আর inheritance কম ব্যবহার করো। Value মেলে data দিয়ে আর কখনো বদলায় না; entity মেলে ID দিয়ে আর বদলাতে বদলাতেই বেঁচে থাকে।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →