Part 01 · Low Level Design
What Is Low Level Design?
আজকে চলে এমন code আর এক মাস পরেও বদলানো যায় এমন code: HLD এর পাশে LLD কোথায় বসে, ৪৫ মিনিটের একটা LLD round কেমন হয়, আর requirement থেকে class এ যাওয়ার রাস্তা।
⏱ 1-2 ঘণ্টা · Station 1 / 80
একই কাজ, দুইটা program
ঢাকার একটা কলেজ library ছোট একটা system চায়। Member রা বই নেবে। একজন একসাথে ৩টার বেশি বই রাখতে পারবে না। বই রাখা যাবে ১৪ দিন। দেরি হলে দিনে ৫ টাকা fine।
সাব্বির আর তানিয়া দুজনেই এক বিকেলে এটা বানিয়ে ফেললো। দুটোই চলে। হাতে-কলমে একই পাঁচটা check দিলে দুটোই pass।
এক মাস পর librarian এসে দুইটা জিনিস চাইলেন: premium member রা ৫টা বই নিতে পারবে, আর কারো fine বাকি থাকলে টাকা না দেওয়া পর্যন্ত সে বই নিতে পারবে না।
- সাব্বিরের code একটা long function, সাথে কয়েকটা global map।
3লেখা আছে চার জায়গায়। "এই member বই নিতে পারবে কিনা" সেটা check হচ্ছে borrow screen এ, renew screen এ, আবার রাতের report এও। এই change করতে তার দুই দিন লাগলো, আর renew এর feature টা ভেঙে গেল। - তানিয়ার code এ
Memberনিজের limit নিজে জানে। আর কে বই পাবে, সেটা ঠিক করে শুধুLibrary। তার change হলো দুইটা ছোট edit আর একটা নতুন test।
দুইটা program ই চলতো। কিন্তু design ছিল একটাতেই। এই পার্থক্যটার নামই low level design, আর এই course এ আমরা এটাই শিখবো।
সমস্যাটা কোথায়: code চললেই কাজ শেষ না
আজ যে code লিখলে, সেটা সামনের কয়েক বছর মানুষ পড়বে, বদলাবে, তার উপর নতুন জিনিস বানাবে। আর বেশিরভাগ সময় সেই মানুষটা তুমি না, অন্য কেউ। একটা ছোট system এর প্রথম বছরেই সাধারণত এগুলো হয়:
| যা হয় | তখন code এর কাছে যা লাগে |
|---|---|
| নতুন rule আসে ("premium পাবে ৫টা") | প্রতিটা rule বদলানোর একটা পরিষ্কার জায়গা |
| Team এ নতুন কেউ join করে | এমন নাম আর ভাগ, যেটা নতুন মানুষও ধরতে পারে |
| Fine এর হিসাবে bug ধরা পড়ে | ছোট ছোট অংশ, যেগুলো আলাদা করে test করা যায় |
| একসাথে দুইটা request আসে | Shared data কার হাতে, সেটা পরিষ্কার |
| নতুন feature লাগে | পুরনো code না ভেঙে নতুন code বসানোর জায়গা |
প্রথম দিন এর কিছুই চোখে পড়ে না। কিন্তু পরের change টা কতটা কষ্টের হবে, সেটা এগুলোই ঠিক করে দেয়। Low level design মানে code টা এমনভাবে সাজানো যাতে পরের change টা সহজ হয়, আবার কেউ চায়নি এমন জিনিসও বানাতে না হয়।
একটা building বানানোর কথা ভাবো
একটা housing project শুরুর আগে দুই রকম plan লাগে।
একটা হলো site plan। পুরো জায়গাটা উপর থেকে: কোন building কোথায় বসবে, মাঝখান দিয়ে রাস্তা কোথায়, পানি আর কারেন্টের লাইন কোন দিক দিয়ে যাবে। Software এ এটাই high level design (HLD): কোন কোন service আর database থাকবে, আর তারা কীভাবে একে অপরের সাথে কথা বলবে।
আরেকটা হলো floor plan। একটা building এর ভেতরটা: কয়টা রুম, কোন রুম থেকে কোন রুমে যাওয়া যায়, রান্নাঘর কোথায়, বাথরুম কেন ডাইনিং টেবিলের পাশে না। এটাই low level design (LLD): একটা service এর ভেতরে কোন কোন class, কার কী কাজ, আর তারা মিলে কীভাবে কাজটা করে।
তবে একটা জায়গায় মেলে না। বাস্তবের দেয়াল ভাঙা অনেক খরচের, তাই architect রা floor plan আগেই পাকা করে নেয়। Software এ একটা "দেয়াল" সরানো, মানে এক class এর কাজ আরেক class এ নিয়ে যাওয়া, সস্তা, যদি code টা সেভাবে লেখা থাকে। না থাকলে সেটাই সবচেয়ে কষ্টের। তাই LLD মানে প্রথমবারেই সব ঠিক করে ফেলা না। LLD মানে রুমগুলো এমনভাবে রাখা যাতে দরকার হলে সহজে আবার সাজানো যায়।
তানিয়ার code এ Member এর একটা limit() method আছে, আর Library ছাড়া আর কেউ এটা check করে না। এখন librarian বললেন premium member রা ৫টা বই নিতে পারবে। Code এর কয় জায়গায় হাত দিতে হবে?
LLD আসলে কী, আর HLD থেকে কোথায় আলাদা
-
Low level design হলো একটা program এর ভেতরের class (বা struct, module) গুলো ঠিক করা: কে কী data রাখে, কী কাজ করে, কোন সিদ্ধান্তটা তার, আর অন্যদের সাথে কীভাবে জোড়া আর কীভাবে মিলে কাজ করে। শেষে হাতে থাকে একটা class diagram, জরুরি কয়েকটা flow, আর code।
-
High level design ঠিক করে box গুলো: service, database, cache, queue, আর এদের মাঝের তীর। এখানে মাথাব্যথা হলো অনেক machine জুড়ে scale, failure আর latency।
HLD LLD কোথা থেকে দেখছো পুরো system একটা service বা module এর ভেতরে প্রধান অংশ Service, DB, cache, queue Class, interface, enum সাধারণ প্রশ্ন "এই data কোথায় থাকবে, আর ১০ লাখ user এটা কীভাবে পাবে?" "এই সিদ্ধান্ত কোন class নেবে, আর rule বদলালে কী কী বদলাতে হবে?" ভুল হলে যা হয় Site down, page ধীর, data হারায় Bug, change করতে ভয় লাগে, কেউ code টা ছুঁতে চায় না -
ভালো design মানে যে change গুলো আসার কথা, সেগুলো সহজে করা যায়। দুনিয়ার সব change না, business আসলে যেগুলো চাইবে সেগুলো। LLD এর আসল skill হলো এগুলো ঠিকঠাক ধরতে পারা, আর বাকি সব সহজ রাখা।
-
Requirement থেকে class এ যাওয়া: requirement এর noun গুলো দাগাও (book, member, loan, fine), এগুলো class হতে পারে। Verb গুলো (borrow, return, pay) হয় method। আর rule গুলো (৩টা বই, ১৪ দিন, দিনে ৫ টাকা) কোথায় বসবে? যে object এর data দেখে rule টা চলে, তার কাছে।
-
প্রতিটা সিদ্ধান্তের মালিক একজনই। "এই member এই বইটা পাবে কিনা" এটা ঠিক হবে এক জায়গায়। Screen, API, script সবাই ওই জায়গাকেই জিজ্ঞেস করবে, কেউ নিজে আবার rule টা লিখবে না।
Requirement এর একটা লাইন থেকে class বানানো
একটা requirement থেকে design
ধাপ 1/5 Requirement এর লাইনটা দিয়ে শুরু
"একজন member ১৪ দিনের জন্য সর্বোচ্চ ৩টা বই নিতে পারবে। দেরিতে ফেরত দিলে দিনে ৫ টাকা fine।"
- rule
- limit 3, 14 days, 5 taka/day
এক নজরে: একই system, দুই level
একই system, দুই জায়গা থেকে দেখা:
- দূর থেকে (HLD): কয়েকটা box আর তীর। "Library API কথা বলে Postgres আর একটা email service এর সাথে।"
- কাছে গিয়ে (LLD): একটা box এর ভেতরে অল্প কয়েকটা class। প্রত্যেকের কাজ এক লাইনে বলা যায়, আর প্রতিটা সিদ্ধান্তের মালিক একজন।
Design করার সময় নিজেকে দুইটা প্রশ্ন বারবার করো: এই সিদ্ধান্তটা কার? আর এরপর কী বদলাতে পারে? এই part এ সামনে যা আসছে, OOP, SOLID, design pattern, সবই এই দুইটা প্রশ্নের ভালো উত্তর দেওয়ার tool।
নিজে বলো: এটা LLD না HLD?
এই প্রশ্নটা কোন level এর?
database বাছাই, sharding, replica
Code এ তানিয়ার library
তানিয়ার library টা, ছয়টা language এ। Noun থেকে চারটা type, verb থেকে দুইটা method। আর প্রতিটা rule এক জায়গায়: limit আছে Member.limit() এ, loan এর মেয়াদ আর fine আছে Lend আর Return এর পাশে।
Program টা ছোট একটা গল্প চালায়: রহিম limit শেষ না হওয়া পর্যন্ত বই নেয়, নাদিয়া এমন একটা বই চায় যেটা আগেই কেউ নিয়ে গেছে, রহিম দেরিতে ফেরত দিয়ে fine দেয়, তারপর premium member নাদিয়া বই নেয়।
package main
import (
"errors"
"fmt"
)
// Each class has one job, found from the nouns in the requirements.
type Book struct {
Title string
onLoan bool
}
type Member struct {
Name string
Premium bool
loans int
}
// The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
func (m *Member) limit() int {
if m.Premium {
return 5
}
return 3
}
type Loan struct {
book *Book
member *Member
due int // day number
}
// Library is the only place that decides who may borrow what.
type Library struct {
loans []*Loan
}
const loanDays, finePerDay = 14, 5 // taka
func (l *Library) Lend(m *Member, b *Book, today int) error {
if b.onLoan {
return errors.New("already on loan")
}
if m.loans >= m.limit() {
return fmt.Errorf("limit reached (%d)", m.limit())
}
b.onLoan = true
m.loans++
l.loans = append(l.loans, &Loan{book: b, member: m, due: today + loanDays})
return nil
}
// Return closes the loan and says how much fine is due.
func (l *Library) Return(b *Book, today int) (int, error) {
for i, ln := range l.loans {
if ln.book == b {
l.loans = append(l.loans[:i], l.loans[i+1:]...)
b.onLoan = false
ln.member.loans--
return max(0, today-ln.due) * finePerDay, nil
}
}
return 0, errors.New("not on loan")
}
func main() {
lib := &Library{}
rahim := &Member{Name: "Rahim"}
nadia := &Member{Name: "Nadia", Premium: true}
books := []*Book{{Title: "Himu"}, {Title: "Feluda"}, {Title: "Byomkesh"}, {Title: "Tintin"}}
for _, b := range books {
if err := lib.Lend(rahim, b, 1); err != nil {
fmt.Printf("day 1 Rahim %-9s refused: %v\n", b.Title, err)
} else {
fmt.Printf("day 1 Rahim %-9s ok\n", b.Title)
}
}
if err := lib.Lend(nadia, books[0], 2); err != nil {
fmt.Printf("day 2 Nadia %-9s refused: %v\n", books[0].Title, err)
}
fine, _ := lib.Return(books[0], 19) // due on day 15
fmt.Printf("day 19 Rahim returns %s, %d days late, fine %d taka\n", books[0].Title, fine/finePerDay, fine)
if err := lib.Lend(nadia, books[3], 19); err == nil {
fmt.Printf("day 19 Nadia %-9s ok (premium limit %d)\n", books[3].Title, nadia.limit())
}
}from dataclasses import dataclass, field
LOAN_DAYS, FINE_PER_DAY = 14, 5 # taka
# Each class has one job, found from the nouns in the requirements.
@dataclass(eq=False)
class Book:
title: str
on_loan: bool = False
@dataclass(eq=False)
class Member:
name: str
premium: bool = False
loans: int = 0
# The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
def limit(self) -> int:
return 5 if self.premium else 3
@dataclass(eq=False)
class Loan:
book: Book
member: Member
due: int # day number
class LendError(Exception):
pass
@dataclass
class Library:
"""The only place that decides who may borrow what."""
loans: list[Loan] = field(default_factory=list)
def lend(self, m: Member, b: Book, today: int) -> None:
if b.on_loan:
raise LendError("already on loan")
if m.loans >= m.limit():
raise LendError(f"limit reached ({m.limit()})")
b.on_loan = True
m.loans += 1
self.loans.append(Loan(b, m, today + LOAN_DAYS))
def return_book(self, b: Book, today: int) -> int:
for ln in self.loans:
if ln.book is b:
self.loans.remove(ln)
b.on_loan = False
ln.member.loans -= 1
return max(0, today - ln.due) * FINE_PER_DAY
raise LendError("not on loan")
lib = Library()
rahim, nadia = Member("Rahim"), Member("Nadia", premium=True)
books = [Book(t) for t in ("Himu", "Feluda", "Byomkesh", "Tintin")]
for b in books:
try:
lib.lend(rahim, b, 1)
print(f"day 1 Rahim {b.title:<9} ok")
except LendError as e:
print(f"day 1 Rahim {b.title:<9} refused: {e}")
try:
lib.lend(nadia, books[0], 2)
except LendError as e:
print(f"day 2 Nadia {books[0].title:<9} refused: {e}")
fine = lib.return_book(books[0], 19) # due on day 15
print(f"day 19 Rahim returns {books[0].title}, {fine // FINE_PER_DAY} days late, fine {fine} taka")
lib.lend(nadia, books[3], 19)
print(f"day 19 Nadia {books[3].title:<9} ok (premium limit {nadia.limit()})")const LOAN_DAYS = 14, FINE_PER_DAY = 5; // taka
// Each class has one job, found from the nouns in the requirements.
class Book {
readonly title: string;
onLoan = false;
constructor(title: string) {
this.title = title;
}
}
class Member {
readonly name: string;
readonly premium: boolean;
loans = 0;
constructor(name: string, premium = false) {
this.name = name;
this.premium = premium;
}
// The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
limit(): number {
return this.premium ? 5 : 3;
}
}
interface Loan { book: Book; member: Member; due: number } // due = day number
// The only place that decides who may borrow what.
class Library {
private loans: Loan[] = [];
lend(m: Member, b: Book, today: number): void {
if (b.onLoan) throw new Error("already on loan");
if (m.loans >= m.limit()) throw new Error(`limit reached (${m.limit()})`);
b.onLoan = true;
m.loans++;
this.loans.push({ book: b, member: m, due: today + LOAN_DAYS });
}
returnBook(b: Book, today: number): number {
const i = this.loans.findIndex((ln) => ln.book === b);
if (i < 0) throw new Error("not on loan");
const [ln] = this.loans.splice(i, 1);
b.onLoan = false;
ln!.member.loans--;
return Math.max(0, today - ln!.due) * FINE_PER_DAY;
}
}
const lib = new Library();
const rahim = new Member("Rahim"), nadia = new Member("Nadia", true);
const books = ["Himu", "Feluda", "Byomkesh", "Tintin"].map((t) => new Book(t));
for (const b of books) {
try {
lib.lend(rahim, b, 1);
console.log(`day 1 Rahim ${b.title.padEnd(9)} ok`);
} catch (e) {
console.log(`day 1 Rahim ${b.title.padEnd(9)} refused: ${(e as Error).message}`);
}
}
try {
lib.lend(nadia, books[0]!, 2);
} catch (e) {
console.log(`day 2 Nadia ${books[0]!.title.padEnd(9)} refused: ${(e as Error).message}`);
}
const fine = lib.returnBook(books[0]!, 19); // due on day 15
console.log(`day 19 Rahim returns ${books[0]!.title}, ${fine / FINE_PER_DAY} days late, fine ${fine} taka`);
lib.lend(nadia, books[3]!, 19);
console.log(`day 19 Nadia ${books[3]!.title.padEnd(9)} ok (premium limit ${nadia.limit()})`);const LOAN_DAYS: u32 = 14;
const FINE_PER_DAY: u32 = 5; // taka
// Each struct has one job, found from the nouns in the requirements.
// Books and members are referred to by index, so the Library can own them all.
struct Book { title: String, on_loan: bool }
struct Member { premium: bool, loans: u32 }
impl Member {
// The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
fn limit(&self) -> u32 { if self.premium { 5 } else { 3 } }
}
struct Loan { book: usize, member: usize, due: u32 }
// The only place that decides who may borrow what.
struct Library { books: Vec<Book>, members: Vec<Member>, loans: Vec<Loan> }
impl Library {
fn lend(&mut self, m: usize, b: usize, today: u32) -> Result<(), String> {
if self.books[b].on_loan {
return Err("already on loan".into());
}
let member = &mut self.members[m];
if member.loans >= member.limit() {
return Err(format!("limit reached ({})", member.limit()));
}
member.loans += 1;
self.books[b].on_loan = true;
self.loans.push(Loan { book: b, member: m, due: today + LOAN_DAYS });
Ok(())
}
fn return_book(&mut self, b: usize, today: u32) -> Result<u32, String> {
let i = self.loans.iter().position(|l| l.book == b).ok_or("not on loan")?;
let loan = self.loans.remove(i);
self.books[b].on_loan = false;
self.members[loan.member].loans -= 1;
Ok(today.saturating_sub(loan.due) * FINE_PER_DAY)
}
}
fn main() {
let titles = ["Himu", "Feluda", "Byomkesh", "Tintin"];
let mut lib = Library {
books: titles.iter().map(|t| Book { title: t.to_string(), on_loan: false }).collect(),
members: vec![Member { premium: false, loans: 0 }, Member { premium: true, loans: 0 }],
loans: vec![],
};
let (rahim, nadia) = (0, 1);
for b in 0..titles.len() {
match lib.lend(rahim, b, 1) {
Ok(()) => println!("day 1 Rahim {:<9} ok", titles[b]),
Err(e) => println!("day 1 Rahim {:<9} refused: {e}", titles[b]),
}
}
if let Err(e) = lib.lend(nadia, 0, 2) {
println!("day 2 Nadia {:<9} refused: {e}", lib.books[0].title);
}
let fine = lib.return_book(0, 19).unwrap(); // due on day 15
println!("day 19 Rahim returns {}, {} days late, fine {fine} taka", lib.books[0].title, fine / FINE_PER_DAY);
lib.lend(nadia, 3, 19).unwrap();
println!("day 19 Nadia {:<9} ok (premium limit {})", lib.books[3].title, lib.members[nadia].limit());
}import java.util.ArrayList;
import java.util.List;
public class Library {
static final int LOAN_DAYS = 14, FINE_PER_DAY = 5; // taka
// Each class has one job, found from the nouns in the requirements.
static final class Book {
final String title;
boolean onLoan;
Book(String title) { this.title = title; }
}
static final class Member {
final String name;
final boolean premium;
int loans;
Member(String name, boolean premium) { this.name = name; this.premium = premium; }
// The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
int limit() { return premium ? 5 : 3; }
}
record Loan(Book book, Member member, int due) {}
static final class LendException extends Exception {
private static final long serialVersionUID = 1L;
LendException(String msg) { super(msg); }
}
// The only place that decides who may borrow what.
private final List<Loan> loans = new ArrayList<>();
void lend(Member m, Book b, int today) throws LendException {
if (b.onLoan) throw new LendException("already on loan");
if (m.loans >= m.limit()) throw new LendException("limit reached (" + m.limit() + ")");
b.onLoan = true;
m.loans++;
loans.add(new Loan(b, m, today + LOAN_DAYS));
}
int returnBook(Book b, int today) throws LendException {
for (Loan ln : loans) {
if (ln.book() == b) {
loans.remove(ln);
b.onLoan = false;
ln.member().loans--;
return Math.max(0, today - ln.due()) * FINE_PER_DAY;
}
}
throw new LendException("not on loan");
}
public static void main(String[] args) throws LendException {
Library lib = new Library();
Member rahim = new Member("Rahim", false), nadia = new Member("Nadia", true);
List<Book> books = List.of(new Book("Himu"), new Book("Feluda"), new Book("Byomkesh"), new Book("Tintin"));
for (Book b : books) {
try {
lib.lend(rahim, b, 1);
System.out.printf("day 1 Rahim %-9s ok%n", b.title);
} catch (LendException e) {
System.out.printf("day 1 Rahim %-9s refused: %s%n", b.title, e.getMessage());
}
}
try {
lib.lend(nadia, books.get(0), 2);
} catch (LendException e) {
System.out.printf("day 2 Nadia %-9s refused: %s%n", books.get(0).title, e.getMessage());
}
int fine = lib.returnBook(books.get(0), 19); // due on day 15
System.out.printf("day 19 Rahim returns %s, %d days late, fine %d taka%n", books.get(0).title, fine / FINE_PER_DAY, fine);
lib.lend(nadia, books.get(3), 19);
System.out.printf("day 19 Nadia %-9s ok (premium limit %d)%n", books.get(3).title, nadia.limit());
}
}#include <algorithm>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <vector>
constexpr int kLoanDays = 14, kFinePerDay = 5; // taka
// Each class has one job, found from the nouns in the requirements.
struct Book {
std::string title;
bool onLoan = false;
};
struct Member {
std::string name;
bool premium = false;
int loans = 0;
// The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
int limit() const { return premium ? 5 : 3; }
};
struct Loan {
Book* book;
Member* member;
int due; // day number
};
// The only place that decides who may borrow what.
class Library {
public:
void lend(Member& m, Book& b, int today) {
if (b.onLoan) throw std::runtime_error("already on loan");
if (m.loans >= m.limit()) throw std::runtime_error("limit reached (" + std::to_string(m.limit()) + ")");
b.onLoan = true;
++m.loans;
loans_.push_back({&b, &m, today + kLoanDays});
}
int returnBook(Book& b, int today) {
auto it = std::find_if(loans_.begin(), loans_.end(), [&](const Loan& l) { return l.book == &b; });
if (it == loans_.end()) throw std::runtime_error("not on loan");
Loan ln = *it;
loans_.erase(it);
b.onLoan = false;
--ln.member->loans;
return std::max(0, today - ln.due) * kFinePerDay;
}
private:
std::vector<Loan> loans_;
};
int main() {
Library lib;
Member rahim{"Rahim"}, nadia{"Nadia", true};
std::vector<Book> books{{"Himu"}, {"Feluda"}, {"Byomkesh"}, {"Tintin"}};
for (auto& b : books) {
try {
lib.lend(rahim, b, 1);
std::printf("day 1 Rahim %-9s ok\n", b.title.c_str());
} catch (const std::exception& e) {
std::printf("day 1 Rahim %-9s refused: %s\n", b.title.c_str(), e.what());
}
}
try {
lib.lend(nadia, books[0], 2);
} catch (const std::exception& e) {
std::printf("day 2 Nadia %-9s refused: %s\n", books[0].title.c_str(), e.what());
}
int fine = lib.returnBook(books[0], 19); // due on day 15
std::printf("day 19 Rahim returns %s, %d days late, fine %d taka\n", books[0].title.c_str(), fine / kFinePerDay, fine);
lib.lend(nadia, books[3], 19);
std::printf("day 19 Nadia %-9s ok (premium limit %d)\n", books[3].title.c_str(), nadia.limit());
}ছয়টাই হুবহু এটা print করে:
day 1 Rahim Himu ok
day 1 Rahim Feluda ok
day 1 Rahim Byomkesh ok
day 1 Rahim Tintin refused: limit reached (3)
day 2 Nadia Himu refused: already on loan
day 19 Rahim returns Himu, 4 days late, fine 20 taka
day 19 Nadia Tintin ok (premium limit 5)
Output টা পড়ি:
- রহিমের চার নম্বর বইটা দেওয়া হয়নি, message
limit reached (3)। সংখ্যাটা আসছেlimit()থেকে, তাই rule বদলালেও message ভুল হবে না। - নাদিয়া হিমু পায়নি, কারণ ওটা রহিমের কাছে। বইটা loan এ আছে কিনা
Bookনিজেই জানে, আর কিছু বদলানোর আগেইLibraryসেটা দেখে নেয়। - ১৯ তারিখ: বই ফেরতের শেষ দিন ছিল ১৫ (১ + ১৪)। রহিম ৪ দিন দেরি করেছে, তাই ৪ × ৫ = ২০ টাকা।
- নাদিয়া টিনটিন পেয়েছে, কারণ তার limit ৫। Premium শুধু
Memberএর একটা field, আর কোনো code কে এটা নিয়ে মাথা ঘামাতে হয়নি। - Language ভেদে কী আলাদা: Go আর Python এ সাধারণ struct আর method। Java তে
Loanএকটাrecord, কারণ এটা কখনো বদলায় না। Rust এ book আর member কে index দিয়ে ধরা হয়েছে, যাতে borrow checker এর সাথে যুদ্ধ না করেLibraryসব data নিজের কাছে রাখতে পারে। C++ এ vector এর ভেতরে pointer রাখা, আর loan চলাকালীন vector গুলো resize হয় না। কিন্তু কোন rule কার কাছে, সেই design ছয়টাতেই এক।
Real project আর interview এ যা কাজে লাগে
- যে change গুলো আসছে বলে বোঝা যাচ্ছে, সেগুলোর জন্য design করো। Product টা যিনি দেখেন, তাকে জিজ্ঞেস করো: কোন rule গুলো ঘনঘন বদলায়? ওগুলোর একটা পরিষ্কার জায়গা লাগবে। বাকিগুলো যতদিন না নড়ে, সাধারণই থাকুক।
- Code লেখার আগে class গুলোর list লিখে ফেলো। পাঁচ লাইন ("Library: বই দেয়, ফেরত নেয়, loan গুলো তার কাছে। Member: নিজের limit জানে…") লিখলেই বেশিরভাগ design এর ভুল আগেভাগে ধরা পড়ে।
- Business rule কে screen আর handler থেকে দূরে রাখো। Borrow API, librarian এর screen, রাতের script, সবাই
Library.Lendকে ডাকবে। (Backend First Principles এ এটাই আছে "business logic বনাম plumbing" নামে।) - কাল্পনিক ভবিষ্যতের জন্য কিছু বানিও না। একটাই implementation এর জন্য
IBookinterface না, দুই field এর object এর জন্য factory না। সত্যি দরকার পড়লে এগুলো এক বিকেলেই বসানো যায়।
৪৫ মিনিটের LLD interview, বাস্তবে যেমন যায়:
| মিনিট | কী করবে |
|---|---|
| ০–৫ | Requirement আর scope পরিষ্কার করো। কী কী এবার বানাতে হবে না, সেটাও জিজ্ঞেস করো। |
| ৫–১২ | Entity গুলোর list, প্রত্যেকের কাজ এক লাইনে; কে কার সাথে জোড়া, এঁকে ফেলো। |
| ১২–২০ | Class diagram আঁকো, মূল method গুলোর নাম দাও। |
| ২০–৩৮ | মূল flow টা code করো (এখানে Lend আর Return), rule গুলো তাদের জায়গায় রেখে। |
| ৩৮–৪৫ | "যদি এমন হয়…" ধরনের প্রশ্ন সামলাও: নতুন rule, নতুন type, একসাথে দুইটা thread। |
Interview টা আসলে জেতা হয় শেষের অংশে। "Premium member রা ৫টা পেলে?" প্রশ্নে যদি বলতে পারো "limit() এ এক লাইন", তাহলে তুমি ঠিক সেটাই দেখালে যেটা দেখানোর জন্য LLD round। এই part এর শেষ chapter এ এই পুরো approach টা ধরে ধরে আছে।
যে design গুলো পরে problem create করে
খারাপ design টা চেনো
func handleBorrow(req) { ... 300 lines ... }একটু থেমে ভাবো
একজন teammate বললো: "চলো Book কে এখনই abstract class বানিয়ে ফেলি, সাথে PaperBook আর EBook subclass। Library হয়তো কোনোদিন e-book দেবে।" অথচ library কখনো e-book এর কথা বলেনি। তুমি কী বলবে?
উত্তর দেখো
এখন না। আজ বই একরকমই, আলাদা কোনো আচরণও নেই। এখন subclass বানানো মানে আন্দাজে বানানো, আর আন্দাজ খুঁটিনাটিতে প্রায়ই ভুল হয় (e-book এর কি copy থাকে? limit আছে?)। Book যেমন আছে তেমন রাখো। E-book যেদিন সত্যি requirement হয়ে আসবে, সেদিন জানবে আসলে পার্থক্য কোথায়। তখন abstraction বসানো ছোট একটা কাজ, কারণ বাকি design (প্রতিটা rule এর মালিক একজন) পরিষ্কার। এটাকেই বলে YAGNI: "you aren't gonna need it", যতদিন না সত্যি লাগে।
ছোট্ট quiz
১. এক লাইনে বলো, HLD কী ঠিক করে আর LLD কী ঠিক করে?
উত্তর
HLD ঠিক করে কোন কোন service, database আর queue থাকবে, আর অনেক machine জুড়ে তারা কীভাবে কথা বলবে। LLD ঠিক করে একটা program এর ভেতরে কোন কোন class থাকবে, কার কী দায়িত্ব, আর তারা মিলে কীভাবে কাজ করবে।
২. সাব্বির আর তানিয়া দুজনের program ই চলতো। তানিয়ারটা ভালো কেন?
উত্তর
প্রতিটা rule এর জায়গা ছিল একটাই (limit আছে Member এ, বই দেওয়ার নিয়ম Library তে)। তাই নতুন rule মানে ছোট, নিরাপদ একটা change, copy খুঁজে খুঁজে বের করা না।
৩. একটা requirement এর লাইন থেকে class আর method এ যাও কীভাবে?
উত্তর
Noun গুলো class হতে পারে, verb গুলো method হয়, আর প্রতিটা rule যায় সেই object এর কাছে যার data দেখে rule টা চলে। তারপর যে class এর নিজের কোনো data বা কাজ নেই, সেটা বাদ দাও।
৪. ৪৫ মিনিটের LLD interview তে বেশিরভাগ সময় কোথায় দেবে, আর শেষের কয়েক মিনিট কী দিয়ে জেতা যায়?
উত্তর
বেশিরভাগ সময় যায় class design আর মূল flow code করতে। শেষ কয়েক মিনিট জেতা যায় "যদি এমন হয়…" প্রশ্নগুলো ছোট, এক জায়গার change দিয়ে সামলে। তাতে বোঝা যায় design টা সহজে বদলানো যায়।
তোমার challenge: fine এর rule
Librarian এর দ্বিতীয় দাবিটা যোগ করো: যার fine বাকি, টাকা না দেওয়া পর্যন্ত সে বই নিতে পারবে না।
Memberকে তার বাকি টাকার হিসাব রাখতে দাও।Returnfine টা ওখানে যোগ করবে।- Member এর কিছু বাকি থাকলে
Lendবই দেবে না, বলবেpay fine first (20 taka)। - গল্পের শেষে এক লাইন যোগ করো: ১৯ তারিখে রহিম আবার হিমু নিতে চায়।
চালানোর আগে আন্দাজ করো, নতুন শেষ লাইনটা কী আসবে।
হয়েছে বুঝবে যখন শেষ লাইনে আসবে day 19 Rahim Himu refused: pay fine first (20 taka), আগের সব লাইন হুবহু আগের মতো থাকবে, আর check টা থাকবে এক জায়গায় (Lend এ), গল্পের code এ না।
এক কথায়
HLD বাইরের box গুলো আঁকে; LLD একটা box এর ভেতরটা সাজায়। Noun থেকে class, verb থেকে method, প্রতিটা rule এর জন্য ঠিক একটা জায়গা, আর design কে ততটুকুই flexible রাখো যতটুকু change সত্যি আসতে দেখছো। Code চালানো প্রথম দিনের কাজ; ভালো design লাগে তার পরের প্রতিটা দিনে।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →