Part 01 · Low Level Design

OOP Basics: Classes, Objects and the Four Pillars

EncapsulationAbstractionPolymorphismInheritanceValue vs Entity

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 হয়।

encapsulationpolymorphismreport scriptWalletbalance (private)Withdraw()checks balancebalance = 1e6blocked: field is privateWithdraw(305)the rule runs before the balance changescheckout()Cardfee 1.5%Bkashfee 1.85%Cashno feepay(amount)same call, three behaviours
Play চাপো। বাম দিকে: একটা script সরাসরি balance বদলাতে গিয়ে আটকে যায়; একই কাজ Withdraw দিয়ে করলে আগে check হয়। ডান দিকে: একটাই pay() call card, bKash আর cash এর কাছে যায়, আর প্রত্যেকে নিজের মতো উত্তর দেয়।

তবে একটা জায়গায় মেলে না। আসল teller নিজের বুদ্ধি খাটাতে পারে। Object পারে না: তুমি method এ যে rule লিখেছো, শুধু সেটাই মানে। কোনো method এ check লিখতে ভুলে গেলে object খুব ভদ্রভাবে ভুল কাজটাই করে ফেলবে।

আগে অনুমান করো

Wallet এর balance private, আর Withdraw method balance কম হলে টাকা দেয় না। এখন refund script কে একটা wallet থেকে ২০ টাকা fee কাটতে হবে। ঠিক উপায় কোনটা?

চারটা pillar আর দুই রকম object

  1. Class আর object। Class হলো একটা ছাঁচ: কোন data আর কোন কাজ একসাথে থাকবে। Object হলো সেই ছাঁচ থেকে বানানো একটা আসল জিনিস, তার নিজের data সহ। Wallet হলো class; রহিমের ৫০০ টাকার wallet টা একটা object।
  2. Encapsulation মানে data লুকিয়ে রাখা, আর বাইরে শুধু এমন method দেওয়া যেগুলো data কে ঠিক রাখে। Caller object কে কোনো অসম্ভব অবস্থায় নিতে পারে না, কারণ ঢোকার প্রতিটা দরজায় rule check হয়। Go তে এটা ছোট হাতের field, Java/C++ এ private, TypeScript এ #field, Rust এ pub ছাড়া field, Python এ _name এর একটা চল।
  3. Abstraction মানে caller নির্ভর করে জিনিসটা কী করে তার উপর, কীভাবে করে তার উপর না। Checkout জানে "payment method টাকা দিতে পারে, আর তার একটা নাম আছে"। Card network বা bKash এর API নিয়ে সে কিছুই জানে না। Code এ এটা interface (Go, Java, TS), trait (Rust), abstract class (C++), বা protocol (Python)।
  4. Polymorphism মানে এক call, অনেক রকম কাজ: pm.Pay(amount) চালাবে card এর code, নাকি bKash এর, নাকি cash এর, সেটা নির্ভর করে pm এর ভেতরে কোন object আছে তার উপর। Caller এ type দেখে কোনো if নেই, তাই নতুন payment method আনলে caller এ হাত দিতে হয় না।
  5. Inheritance মানে একটা class আরেকটার একটা বিশেষ রূপ হয়ে তার code reuse করে (SavingsAccount is an Account)। জিনিসটা শক্তিশালী, কিন্তু দুইটা class কে শক্ত করে বেঁধে দেয়, আর ভুল ব্যবহার করা খুব সহজ। Bkash extends Wallet শুনতে ঠিক লাগে, কিন্তু bKash payment কোনো wallet না; সে একটা wallet ব্যবহার করে। Interface আর composition বেশি ভালো; কেন, সেটা পরের chapter এ।
  6. 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, ৩০০ টাকা)

checkoutPaymentMethodBkashWallet

ধাপ 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?

প্রতিটা case চালিয়ে দেখো

Value object না entity?

Money{paisa: 50000}
কাজ করে: Value। যেকোনো ৫০০ টাকা আরেকটা ৫০০ টাকার মতোই। Immutable রাখো; যোগ করলে নতুন একটা Money ফেরত আসে।

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)
}

ছয়টাই হুবহু এটা 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) সমান। দুইটা Wallet object একই wallet শুধু এই কারণে যে দুটোর ID W-1।
  • Language ভেদে কী আলাদা: TypeScript এ #balance, যেটা সত্যিকারের private field। Python শুধু _balance দিয়ে বোঝাতে পারে যে এটা private, তাই property আর method গুলোই আসল নিয়ম। Rust এ Bkash wallet টা 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 যদি নিজে বদলাতে পারতো, একই Money share করা দুইটা wallet একসাথে বদলে যেতো। নতুন value ফেরত দিলে এই ধরনের bug আর হয়ই না।
  • প্রথম দিন থেকেই entity কে একটা স্থায়ী ID দাও, আর ID দিয়েই মেলাও। Database থেকে একই order দুইবার load করলে দুইটা object পাবে, কিন্তু order তো একটাই।
  • Interface আনো যেখানে কাজটা সত্যিই আলাদা (payment method, notification channel), সব class এর জন্য না। একটাই implementation এর পেছনে interface বেশিরভাগ সময় শুধু বাড়তি ঝামেলা।

যে design গুলো পরে problem create করে

প্রতিটা case চালিয়ে দেখো

ভাঙা design টা বাছো

wallet.Balance -= fee  // in the refund script
ভাঙে: যে কেউ rule এড়িয়ে যেতে পারে। Field private করো, আর সব পরিবর্তন Withdraw দিয়ে পাঠাও।

একটু থেমে ভাবো

একজন 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 পাও →