Part 01 · Low Level Design
Class Relationships and UML
চেনে, রাখে আর মালিক: association, aggregation বনাম composition, কেন is-a এর চেয়ে has-a বেশিরভাগ সময় ভালো, আর whiteboard এ তাড়াতাড়ি আঁকা যায় এমন UML class diagram।
⏱ 2-3 ঘণ্টা · Station 3 / 80
Batch বন্ধ হলো, সাথে teacher ও উধাও
একটা coaching center এর app এ batch, teacher আর student আছে। Admin যখন HSC-Physics batch টা cancel করে, code টা batch এর row মুছে দেয় "ভেতরের সবকিছু সহ": তার class session, enrolment, আর তার teacher ও।
করিম স্যার HSC-Math ও পড়ান। পরদিন সকালে Math batch এ দেখা যায় "teacher: unknown"।
Bug টা delete query তে না। Bug টা এমন একটা প্রশ্নে যেটা class design করার সময় কেউ করেনি: batch কি তার teacher এর মালিক, নাকি শুধু তাকে point করে? Session গুলো batch এর নিজের, তাই batch গেলে ওগুলোও যাবে। Teacher এর তো তা না।
এই chapter এ দেখবো class গুলো চার রকমভাবে জোড়া থাকতে পারে, কীভাবে এদের আলাদা করে চিনবে, আর কীভাবে আঁকবে যাতে পরের developer কে আন্দাজ করতে না হয়।
সমস্যা: "has a" এর মানে আসলে চার রকম
"Batch এর একজন teacher আছে", "batch এর session আছে", "batch এর student আছে", "report printer এর হাতে একটা batch আছে"। কথা একই, কিন্তু সম্পর্ক চার রকম:
| সম্পর্ক | Batch delete হলে কী হবে? |
|---|---|
| Batch এর session | Session ও যাবে: আলাদা করে এদের কোনো মানে নেই |
| Batch এর teacher | Teacher থাকবে: তার নিজের অস্তিত্ব আছে, অন্য batch ও পড়ান |
| Batch এর student | Student থাকবে, শুধু তার batch এর list update করতে হবে |
| Report printer এর হাতে batch | কিছুই না: printer শুধু একবারের জন্য ধার নিয়েছিল |
Code যদি চারটাকে একই ভাবে treat করে, তাহলে হয় এমন কিছু মুছে যাবে যেটা মোছার কথা না (teacher), নয়তো কিছু জিনিস ঝুলে থাকবে (এমন enrolment যেটা একটা নেই হয়ে যাওয়া batch কে point করছে)।
একটা ভাড়া বাসা আর তার আসবাবের কথা ভাবো
তুমি একটা flat ভাড়া নিলে। রুম গুলো flat এর: building ভাঙলে রুমও শেষ। এটা composition।
তোমার আসবাব flat এর ভেতরে আছে, কিন্তু ওগুলো তোমার: বাসা ছাড়লে sofa তোমার সাথে নতুন বাসায় যাবে। এটা aggregation।
তুমি আর তোমার প্রতিবেশী একে অপরকে চেনো, কিন্তু একজন বাসা ছাড়লে আরেকজন হারিয়ে যায় না। এটা association।
Plumber এক ঘণ্টার জন্য আসে, তোমার রান্নাঘর ব্যবহার করে, চলে যায়। সে এখানে থাকে না। এটা dependency।
তবে একটা জায়গায় মেলে না। বাস্তবে কে কীসের মালিক, সেটা চোখেই দেখা যায়। Code এ এটা শুধু একটা field যেটা একটা reference ধরে আছে, আর syntax দেখে চার রকম সম্পর্কই একরকম লাগে। আসল পার্থক্য হলো object টা কে বানায়, কে মোছে, আর আর কে কে ওটা ধরে রাখতে পারে। এটা ইচ্ছা করে ঠিক করতে হয়, আর লিখে রাখতে হয়।
একটা Batch cancel হলো। কোন কোন object এর সাথে চলে যাওয়া উচিত?
চার রকম সম্পর্ক, আর has-a কেন সাধারণত is-a এর চেয়ে ভালো
- Association ("চেনে"): দুইটা object জোড়া, কিন্তু একজন ছাড়াও আরেকজন থাকতে পারে। Student আর batch: many-to-many, আর দুই দিক থেকেই একে অপরকে খুঁজতে হতে পারে। আঁকা হয় একটা সাধারণ লাইন দিয়ে, প্রায়ই
*থেকে*এর মতো সংখ্যা সহ। - Aggregation ("রাখে, কিন্তু share করে"): একটা পুরো জিনিস কিছু অংশ রাখে, যেগুলোর নিজের অস্তিত্ব আছে আর অন্যের সাথে share হতে পারে। Batch এর একজন teacher আছে; সেই teacher এর অন্য batch ও আছে। আঁকা হয় পুরো জিনিসটার দিকে একটা ফাঁকা diamond দিয়ে।
- Composition ("মালিক"): পুরো জিনিসটা নিজেই তার অংশ বানায়, আর সে গেলে অংশগুলোও যায়। আর কেউ ওগুলো ধরে রাখে না। Batch তার session গুলোর মালিক। আঁকা হয় ভরাট diamond দিয়ে। Code এ মালিক সাধারণত অংশটা নিজেই বানায়, আর এমন কোনো reference বাইরে দেয় না যেটা তার চেয়ে বেশিদিন বাঁচবে।
- Dependency ("একটু সময়ের জন্য ব্যবহার করে"): একটা class আরেকটাকে parameter বা local variable হিসেবে ব্যবহার করে, কোনো reference রেখে দেয় না।
PrintSummary(batch)Batch এর উপর depend করে। আঁকা হয় dashed তীর দিয়ে। এটা সবচেয়ে হালকা সম্পর্ক, আর সবচেয়ে সস্তা। - Inheritance ("এটা ওটারই একটা রকম"):
TeachingAssistant is a Teacher। এটা সবচেয়ে শক্ত বাঁধন: child parent এর সব public method পেয়ে যায়, সারাজীবনের জন্য। এই কারণেই default পরামর্শ হলো composition over inheritance: যে আচরণ লাগবে সেটা একটা field এ রাখো, subclass হয়ে সেটা নিতে যেও না। একজন মানুষ একসাথে student আর teaching assistant, composition দিয়ে এটা সহজ (একজন মানুষ, কয়েকটা role), inheritance দিয়ে ঝামেলার (কোন class extend করবে?)। - UML class diagram আসলে শুধু কয়েকটা box (নাম, জরুরি field, জরুরি method) আর এই লাইন গুলো। Interview তে ঠিক লাইন দিয়ে দ্রুত একটা sketch করলে code লেখার আগেই "কে কীসের মালিক" প্রশ্নের উত্তর হয়ে যায়।
একটা batch cancel করা, ধাপে ধাপে
school.Cancel(HSC-Physics)
ধাপ 1/4 School cancel করার সিদ্ধান্ত নেয়
Batch খোলা আর বন্ধ করা হয় School এ, তাই পরিষ্কার করার rule গুলোও এখানে থাকে।
- open
- HSC-Physics, HSC-Math
প্রতিটা লাইনের জন্য একটা প্রশ্ন
দুইটা class এর মাঝে যেকোনো লাইন আঁকার আগে জিজ্ঞেস করো: এইটা না থাকলে ওইটার কী হবে?
- ওটাও যাবে → composition। মালিক ওটা বানায়, আর কেউ ধরে রাখে না।
- থাকবে, আর অন্যরাও share করে → aggregation।
- থাকবে, আর দুই দিকই একে অপরের খবর রাখে → association।
- কিছুই না, কারণ কোনো reference রাখা হয়নি → dependency।
আর inheritance এর তীর আঁকার আগে বাক্যটা "সবসময়" দিয়ে জোরে বলে দেখো: "একজন teaching assistant সবসময়, সব দিক থেকে একজন teacher।" একটু দ্বিধা লাগলে composition নাও।
নিজে বলো: এটা কোন সম্পর্ক?
সম্পর্কটার নাম বলো
order.lines = [line1, line2]
Code এ coaching center
একটা School batch খোলে। প্রতিটা Batch তার session গুলোর মালিক, একজন teacher কে share করে, আর অনেক student এর সাথে associated। PrintSummary শুধু একটা batch এর উপর depend করে। তারপর HSC-Physics cancel হয়।
package main
import (
"fmt"
"strings"
)
// Teacher and Student live on their own; batches only point at them.
type Teacher struct {
Name string
batches []*Batch
}
type Student struct {
Name string
batches []*Batch
}
// Session is owned by its batch (composition): it is created inside the batch and
// has no meaning outside it.
type Session struct{ Topic string }
type Batch struct {
Name string
teacher *Teacher // aggregation: shared, outlives the batch
sessions []Session // composition: owned, dies with the batch
students []*Student // association: many-to-many, both sides know
}
// School is where batches are created and cancelled.
type School struct{ batches []*Batch }
func (s *School) Open(name string, t *Teacher, topics ...string) *Batch {
b := &Batch{Name: name, teacher: t}
for _, topic := range topics {
b.sessions = append(b.sessions, Session{Topic: topic})
}
t.batches = append(t.batches, b)
s.batches = append(s.batches, b)
return b
}
func (b *Batch) Enroll(st *Student) {
b.students = append(b.students, st)
st.batches = append(st.batches, b)
}
// Cancel removes the batch everywhere it is referenced. Its sessions need no clean-up:
// nothing else holds them.
func (s *School) Cancel(b *Batch) {
s.batches = without(s.batches, b)
b.teacher.batches = without(b.teacher.batches, b)
for _, st := range b.students {
st.batches = without(st.batches, b)
}
}
func without(list []*Batch, b *Batch) []*Batch {
out := []*Batch{}
for _, x := range list {
if x != b {
out = append(out, x)
}
}
return out
}
func names(list []*Batch) string {
if len(list) == 0 {
return "(none)"
}
n := []string{}
for _, b := range list {
n = append(n, b.Name)
}
return strings.Join(n, ", ")
}
// PrintSummary only uses a batch for a moment (dependency): it keeps no reference.
func PrintSummary(b *Batch) {
st := []string{}
for _, s := range b.students {
st = append(st, s.Name)
}
fmt.Printf("%s: teacher %s, %d sessions, students %s\n",
b.Name, b.teacher.Name, len(b.sessions), strings.Join(st, ", "))
}
func main() {
school := &School{}
karim := &Teacher{Name: "Karim"}
rahim, nadia := &Student{Name: "Rahim"}, &Student{Name: "Nadia"}
physics := school.Open("HSC-Physics", karim, "Vectors", "Motion", "Waves")
math := school.Open("HSC-Math", karim, "Matrices", "Calculus")
physics.Enroll(rahim)
physics.Enroll(nadia)
math.Enroll(rahim)
PrintSummary(physics)
PrintSummary(math)
fmt.Println("Rahim's batches:", names(rahim.batches))
fmt.Printf("cancel %s: its %d sessions go with it\n", physics.Name, len(physics.sessions))
school.Cancel(physics)
fmt.Println("Karim still exists, teaches:", names(karim.batches))
fmt.Println("Rahim still exists, batches:", names(rahim.batches))
fmt.Println("Nadia still exists, batches:", names(nadia.batches))
fmt.Println("open batches:", names(school.batches))
}from __future__ import annotations
from dataclasses import dataclass, field
@dataclass(eq=False)
class Teacher:
"""Lives on its own; batches only point at it."""
name: str
batches: list[Batch] = field(default_factory=list)
@dataclass(eq=False)
class Student:
name: str
batches: list[Batch] = field(default_factory=list)
@dataclass(frozen=True)
class Session:
"""Owned by its batch (composition): no meaning outside it."""
topic: str
@dataclass(eq=False)
class Batch:
name: str
teacher: Teacher # aggregation: shared, outlives the batch
sessions: list[Session] = field(default_factory=list) # composition: owned, dies with the batch
students: list[Student] = field(default_factory=list) # association: many-to-many
def enroll(self, st: Student) -> None:
self.students.append(st)
st.batches.append(self)
class School:
def __init__(self) -> None:
self.batches: list[Batch] = []
def open(self, name: str, t: Teacher, *topics: str) -> Batch:
b = Batch(name, t, [Session(x) for x in topics])
t.batches.append(b)
self.batches.append(b)
return b
def cancel(self, b: Batch) -> None:
"""Remove the batch everywhere it is referenced. Its sessions need no clean-up."""
self.batches.remove(b)
b.teacher.batches.remove(b)
for st in b.students:
st.batches.remove(b)
def names(batches: list[Batch]) -> str:
return ", ".join(b.name for b in batches) or "(none)"
def print_summary(b: Batch) -> None:
"""Uses a batch only for a moment (dependency): keeps no reference."""
students = ", ".join(s.name for s in b.students)
print(f"{b.name}: teacher {b.teacher.name}, {len(b.sessions)} sessions, students {students}")
school = School()
karim = Teacher("Karim")
rahim, nadia = Student("Rahim"), Student("Nadia")
physics = school.open("HSC-Physics", karim, "Vectors", "Motion", "Waves")
math = school.open("HSC-Math", karim, "Matrices", "Calculus")
physics.enroll(rahim)
physics.enroll(nadia)
math.enroll(rahim)
print_summary(physics)
print_summary(math)
print("Rahim's batches:", names(rahim.batches))
print(f"cancel {physics.name}: its {len(physics.sessions)} sessions go with it")
school.cancel(physics)
print("Karim still exists, teaches:", names(karim.batches))
print("Rahim still exists, batches:", names(rahim.batches))
print("Nadia still exists, batches:", names(nadia.batches))
print("open batches:", names(school.batches))// Teacher and Student live on their own; batches only point at them.
class Teacher {
readonly name: string;
batches: Batch[] = [];
constructor(name: string) {
this.name = name;
}
}
class Student {
readonly name: string;
batches: Batch[] = [];
constructor(name: string) {
this.name = name;
}
}
// Owned by its batch (composition): no meaning outside it.
interface Session {
readonly topic: string;
}
class Batch {
readonly name: string;
readonly teacher: Teacher; // aggregation: shared, outlives the batch
readonly sessions: Session[]; // composition: owned, dies with the batch
readonly students: Student[] = []; // association: many-to-many
constructor(name: string, teacher: Teacher, topics: string[]) {
this.name = name;
this.teacher = teacher;
this.sessions = topics.map((topic) => ({ topic }));
}
enroll(st: Student): void {
this.students.push(st);
st.batches.push(this);
}
}
const without = (list: Batch[], b: Batch) => list.filter((x) => x !== b);
class School {
batches: Batch[] = [];
open(name: string, t: Teacher, ...topics: string[]): Batch {
const b = new Batch(name, t, topics);
t.batches.push(b);
this.batches.push(b);
return b;
}
// Remove the batch everywhere it is referenced. Its sessions need no clean-up.
cancel(b: Batch): void {
this.batches = without(this.batches, b);
b.teacher.batches = without(b.teacher.batches, b);
for (const st of b.students) st.batches = without(st.batches, b);
}
}
const names = (list: Batch[]) => list.map((b) => b.name).join(", ") || "(none)";
// Uses a batch only for a moment (dependency): keeps no reference.
function printSummary(b: Batch): void {
const students = b.students.map((s) => s.name).join(", ");
console.log(`${b.name}: teacher ${b.teacher.name}, ${b.sessions.length} sessions, students ${students}`);
}
const school = new School();
const karim = new Teacher("Karim");
const rahim = new Student("Rahim"), nadia = new Student("Nadia");
const physics = school.open("HSC-Physics", karim, "Vectors", "Motion", "Waves");
const math = school.open("HSC-Math", karim, "Matrices", "Calculus");
physics.enroll(rahim);
physics.enroll(nadia);
math.enroll(rahim);
printSummary(physics);
printSummary(math);
console.log("Rahim's batches:", names(rahim.batches));
console.log(`cancel ${physics.name}: its ${physics.sessions.length} sessions go with it`);
school.cancel(physics);
console.log("Karim still exists, teaches:", names(karim.batches));
console.log("Rahim still exists, batches:", names(rahim.batches));
console.log("Nadia still exists, batches:", names(nadia.batches));
console.log("open batches:", names(school.batches));// School owns everyone. Relationships are stored as IDs (indexes), which is how Rust
// models shared, many-to-many links without fighting the borrow checker.
type Id = usize;
struct Teacher { name: String }
struct Student { name: String }
// Owned by its batch (composition): a plain value inside the batch.
struct Session { _topic: String }
struct Batch {
name: String,
teacher: Id, // aggregation: shared, outlives the batch
sessions: Vec<Session>, // composition: owned, dropped with the batch
students: Vec<Id>, // association: many-to-many (Student side is looked up below)
}
struct School { teachers: Vec<Teacher>, students: Vec<Student>, batches: Vec<Option<Batch>> }
impl School {
fn open(&mut self, name: &str, t: Id, topics: &[&str]) -> Id {
let sessions = topics.iter().map(|x| Session { _topic: x.to_string() }).collect();
self.batches.push(Some(Batch { name: name.into(), teacher: t, sessions, students: vec![] }));
self.batches.len() - 1
}
fn enroll(&mut self, b: Id, st: Id) { self.batches[b].as_mut().unwrap().students.push(st); }
// Cancelling drops the Batch value, and its sessions are dropped with it.
fn cancel(&mut self, b: Id) { self.batches[b] = None; }
fn open_batches(&self) -> impl Iterator<Item = &Batch> { self.batches.iter().flatten() }
fn names<'a>(&self, it: impl Iterator<Item = &'a Batch>) -> String {
let v: Vec<&str> = it.map(|b| b.name.as_str()).collect();
if v.is_empty() { "(none)".into() } else { v.join(", ") }
}
fn of_teacher(&self, t: Id) -> String { self.names(self.open_batches().filter(|b| b.teacher == t)) }
fn of_student(&self, s: Id) -> String { self.names(self.open_batches().filter(|b| b.students.contains(&s))) }
}
// Uses a batch only for a moment (dependency): borrows it, keeps nothing.
fn print_summary(school: &School, b: &Batch) {
let st: Vec<&str> = b.students.iter().map(|&s| school.students[s].name.as_str()).collect();
println!("{}: teacher {}, {} sessions, students {}",
b.name, school.teachers[b.teacher].name, b.sessions.len(), st.join(", "));
}
fn main() {
let mut school = School {
teachers: vec![Teacher { name: "Karim".into() }],
students: vec![Student { name: "Rahim".into() }, Student { name: "Nadia".into() }],
batches: vec![],
};
let (karim, rahim, nadia) = (0, 0, 1);
let physics = school.open("HSC-Physics", karim, &["Vectors", "Motion", "Waves"]);
let math = school.open("HSC-Math", karim, &["Matrices", "Calculus"]);
school.enroll(physics, rahim);
school.enroll(physics, nadia);
school.enroll(math, rahim);
print_summary(&school, school.batches[physics].as_ref().unwrap());
print_summary(&school, school.batches[math].as_ref().unwrap());
println!("Rahim's batches: {}", school.of_student(rahim));
let p = school.batches[physics].as_ref().unwrap();
println!("cancel {}: its {} sessions go with it", p.name, p.sessions.len());
school.cancel(physics);
println!("Karim still exists, teaches: {}", school.of_teacher(karim));
println!("Rahim still exists, batches: {}", school.of_student(rahim));
println!("Nadia still exists, batches: {}", school.of_student(nadia));
println!("open batches: {}", school.names(school.open_batches()));
}import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
// Teacher and Student live on their own; batches only point at them.
static final class Teacher {
final String name;
final List<Batch> batches = new ArrayList<>();
Teacher(String name) { this.name = name; }
}
static final class Student {
final String name;
final List<Batch> batches = new ArrayList<>();
Student(String name) { this.name = name; }
}
// Owned by its batch (composition): no meaning outside it.
record Session(String topic) {}
static final class Batch {
final String name;
final Teacher teacher; // aggregation: shared, outlives the batch
final List<Session> sessions = new ArrayList<>(); // composition: owned, dies with the batch
final List<Student> students = new ArrayList<>(); // association: many-to-many
Batch(String name, Teacher teacher, String... topics) {
this.name = name;
this.teacher = teacher;
for (String t : topics) sessions.add(new Session(t));
}
void enroll(Student st) {
students.add(st);
st.batches.add(this);
}
}
static final class School {
final List<Batch> batches = new ArrayList<>();
Batch open(String name, Teacher t, String... topics) {
Batch b = new Batch(name, t, topics);
t.batches.add(b);
batches.add(b);
return b;
}
// Remove the batch everywhere it is referenced. Its sessions need no clean-up.
void cancel(Batch b) {
batches.remove(b);
b.teacher.batches.remove(b);
for (Student st : b.students) st.batches.remove(b);
}
}
static String names(List<Batch> list) {
return list.isEmpty() ? "(none)" : list.stream().map(b -> b.name).collect(Collectors.joining(", "));
}
// Uses a batch only for a moment (dependency): keeps no reference.
static void printSummary(Batch b) {
String st = b.students.stream().map(s -> s.name).collect(Collectors.joining(", "));
System.out.printf("%s: teacher %s, %d sessions, students %s%n", b.name, b.teacher.name, b.sessions.size(), st);
}
public static void main(String[] args) {
School school = new School();
Teacher karim = new Teacher("Karim");
Student rahim = new Student("Rahim"), nadia = new Student("Nadia");
Batch physics = school.open("HSC-Physics", karim, "Vectors", "Motion", "Waves");
Batch math = school.open("HSC-Math", karim, "Matrices", "Calculus");
physics.enroll(rahim);
physics.enroll(nadia);
math.enroll(rahim);
printSummary(physics);
printSummary(math);
System.out.println("Rahim's batches: " + names(rahim.batches));
System.out.printf("cancel %s: its %d sessions go with it%n", physics.name, physics.sessions.size());
school.cancel(physics);
System.out.println("Karim still exists, teaches: " + names(karim.batches));
System.out.println("Rahim still exists, batches: " + names(rahim.batches));
System.out.println("Nadia still exists, batches: " + names(nadia.batches));
System.out.println("open batches: " + names(school.batches));
}
}#include <algorithm>
#include <cstdio>
#include <memory>
#include <string>
#include <vector>
struct Batch;
// Teacher and Student live on their own; batches only point at them (raw, non-owning pointers).
struct Teacher {
std::string name;
std::vector<Batch*> batches;
};
struct Student {
std::string name;
std::vector<Batch*> batches;
};
// Owned by its batch (composition): stored by value, destroyed with the batch.
struct Session {
std::string topic;
};
struct Batch {
std::string name;
Teacher* teacher; // aggregation: shared, outlives the batch
std::vector<Session> sessions; // composition: owned, dies with the batch
std::vector<Student*> students; // association: many-to-many
void enroll(Student& st) {
students.push_back(&st);
st.batches.push_back(this);
}
};
void erase(std::vector<Batch*>& v, Batch* b) { v.erase(std::remove(v.begin(), v.end(), b), v.end()); }
// The school owns batches (unique_ptr): cancelling destroys the batch and its sessions.
struct School {
std::vector<std::unique_ptr<Batch>> batches;
Batch& open(const std::string& name, Teacher& t, std::vector<std::string> topics) {
auto b = std::make_unique<Batch>(Batch{name, &t, {}, {}});
for (auto& x : topics) b->sessions.push_back({x});
t.batches.push_back(b.get());
batches.push_back(std::move(b));
return *batches.back();
}
void cancel(Batch& b) {
erase(b.teacher->batches, &b);
for (Student* st : b.students) erase(st->batches, &b);
std::erase_if(batches, [&](const std::unique_ptr<Batch>& p) { return p.get() == &b; });
}
};
template <class List>
std::string names(const List& list) {
std::string out;
for (const auto& b : list) out += (out.empty() ? "" : ", ") + b->name;
return out.empty() ? "(none)" : out;
}
// Uses a batch only for a moment (dependency): keeps no reference.
void printSummary(const Batch& b) {
std::string st;
for (const Student* s : b.students) st += (st.empty() ? "" : ", ") + s->name;
std::printf("%s: teacher %s, %zu sessions, students %s\n", b.name.c_str(), b.teacher->name.c_str(),
b.sessions.size(), st.c_str());
}
int main() {
School school;
Teacher karim{"Karim", {}};
Student rahim{"Rahim", {}}, nadia{"Nadia", {}};
Batch& physics = school.open("HSC-Physics", karim, {"Vectors", "Motion", "Waves"});
Batch& math = school.open("HSC-Math", karim, {"Matrices", "Calculus"});
physics.enroll(rahim);
physics.enroll(nadia);
math.enroll(rahim);
printSummary(physics);
printSummary(math);
std::printf("Rahim's batches: %s\n", names(rahim.batches).c_str());
std::printf("cancel %s: its %zu sessions go with it\n", physics.name.c_str(), physics.sessions.size());
school.cancel(physics); // physics is destroyed here; don't touch it again
std::printf("Karim still exists, teaches: %s\n", names(karim.batches).c_str());
std::printf("Rahim still exists, batches: %s\n", names(rahim.batches).c_str());
std::printf("Nadia still exists, batches: %s\n", names(nadia.batches).c_str());
std::printf("open batches: %s\n", names(school.batches).c_str());
}ছয়টাই হুবহু এটা print করে:
HSC-Physics: teacher Karim, 3 sessions, students Rahim, Nadia
HSC-Math: teacher Karim, 2 sessions, students Rahim
Rahim's batches: HSC-Physics, HSC-Math
cancel HSC-Physics: its 3 sessions go with it
Karim still exists, teaches: HSC-Math
Rahim still exists, batches: HSC-Math
Nadia still exists, batches: (none)
open batches: HSC-Math
Output টা পড়ি:
- প্রথম তিন লাইন link গুলো দেখায়: করিম স্যার দুইটা batch ই পড়ান, রহিম দুইটাতেই আছে, নাদিয়া শুধু Physics এ।
- Cancel: Physics এর ৩টা session ছিল। Cancel এর পর কোথাও আর ওদের কেউ point করে না; এরা কখনো share ই হয়নি।
- করিম স্যার এখনো আছেন, আর Math পড়াচ্ছেন। শুধু Physics এর সাথে তার link টা সরেছে।
- রহিম আর নাদিয়া এখনো আছে। নাদিয়ার এখন কোনো batch নেই, আর একজন student এর জন্য এটা একদম ঠিক একটা অবস্থা।
- Language ভেদে কী আলাদা: Go, Python, TypeScript আর Java তে batch এ আর কেউ পৌঁছাতে না পারলে garbage collector session গুলো সরিয়ে দেয়। C++ এ মালিকানা খোলাখুলি লেখা: school batch গুলো রাখে
unique_ptrএ, session গুলো batch এর ভেতরে value হিসেবে, আর teacher/student রা হলো মালিকানাহীন raw pointer; তাই cancel করার লাইনেই batch আর তার session ধ্বংস হয়ে যায়। Rust এ link গুলো ID হিসেবে রাখা আর সবকিছুর মালিক school; batch এর জায়গায়Noneবসালে batch আর তার session drop হয়ে যায়, আর borrow checker কাউকে ওদের reference রেখে দিতে দেয় না।
Real code এ সম্পর্ক ঠিক রাখার উপায়
- যে বানায়, সে-ই মোছে। কোনো class যদি নিজের constructor বা factory method এ নিজের অংশ বানায়, সেটা composition এর বড় একটা লক্ষণ, আর ওগুলো সরানোর অধিকারও শুধু তার।
- Many-to-many সাধারণত নিজেই একটা object হয়ে যায়।
Enrollment{student, batch, joinedOn, fee}খুব তাড়াতাড়ি নিজের data চাইতে শুরু করে। Database এ এটা join table; code এ প্রায়ই একটা class। - দুই দিক একটা method এই sync রাখো।
Enrollbatch আর student দুটোই একসাথে update করে। দুইটা আলাদা call দিয়ে করলে কোনোদিন একটা ভুলে যাওয়া হবেই। - Database এর foreign key ও এই সিদ্ধান্ত গুলো মেনে চলে: composition এর জন্য
ON DELETE CASCADE(session), aggregation এর জন্যON DELETE RESTRICTবাSET NULL(batch যেন তার teacher কে মুছে না দেয়)। - Interview তে code এর আগে আঁকো। দুই মিনিটে পাঁচটা box আর ঠিক লাইন গুলো আঁকলে আলোচনা হয় design নিয়ে, syntax নিয়ে না।
যে design গুলো পরে problem create করে
ভাঙা সম্পর্কটা বাছো
DELETE batch → also DELETE its teacher
একটু থেমে ভাবো
Product team বললো: "Teacher রাও অন্য batch এ student হিসেবে ভর্তি হয়ে নতুন subject শিখতে পারবেন।" Teacher আর Student আলাদা class হলে করিম স্যারের English batch এ ভর্তি হওয়াটা কীভাবে model করবে?
উত্তর দেখো
Teacher extends Student বা উল্টোটা কোরো না। একটা Person বের করো (নাম, phone, ID), আর batch এর link গুলো দাও একজন person এর সাথে, একটা role সহ: একটা batch এ একজন পড়ানো person আর অনেকজন ভর্তি হওয়া person। অথবা Teacher আর Student কে role object হিসেবে রাখো, যারা দুজনেই একই Person কে point করে। যেভাবেই করো, করিম স্যার একজন মানুষ, দুইটা role, এটা composition, আর তার data দুই জায়গায় copy হয় না।
ছোট্ট quiz
১. Composition আর aggregation আলাদা করার এক লাইনের test কোনটা?
উত্তর
পুরো জিনিসটা মুছলে অংশটাও কি সাথে যায়? হ্যাঁ, আর আর কেউ ধরে নেই: composition। না, সে থাকে আর share হতে পারে: aggregation।
২. Many-to-many association প্রায়ই নিজেই একটা class হয়ে যায় কেন?
উত্তর
কারণ link টার খুব তাড়াতাড়ি নিজের data লাগে (ভর্তির তারিখ, fee, status)। একটা Enrollment object সেটা রাখে, আর দুই দিকই তাকে point করে।
৩. বাস্তবে "composition over inheritance" মানে কী?
উত্তর
যে আচরণ বা data লাগবে, সেটা subclass হয়ে না নিয়ে একটা field এ রাখো। এতে class গুলো ঢিলেভাবে জোড়া থাকে, চলার সময় আচরণ বদলানো যায়, আর জোর করে "is a" বলতে হয় না।
৪. UML এ ভরাট diamond আর dashed তীর মানে কী?
উত্তর
ভরাট diamond মানে composition (পুরো জিনিসটা অংশের মালিক)। Dashed তীর মানে dependency (একটা class আরেকটাকে অল্প সময়ের জন্য ব্যবহার করে)।
তোমার challenge: একজন teacher চলে যাচ্ছেন
School.Resign(teacher, replacement) যোগ করো: যে teacher চলে যাচ্ছেন, তার সব batch replacement এর হাতে যাবে।
- গল্পের শুরুতে সালমা নামে একজন teacher যোগ করো।
- শেষে করিম স্যার resign করবেন, সালমা দায়িত্ব নেবেন।
- এরপর
Salma teaches:আরKarim still exists, teaches:print করো।
চালানোর আগে আন্দাজ করো দুইটা লাইনে কী আসবে।
হয়েছে বুঝবে যখন নতুন লাইন দুইটা হবে Salma teaches: HSC-Math আর Karim still exists, teaches: (none), করিম স্যার object হিসেবে এখনো থাকবেন, আর batch এর session আর student গুলো একই থাকবে।
এক কথায়
প্রতিটা link এর জন্য জিজ্ঞেস করো, এক দিক চলে গেলে আরেক দিকের কী হয়: সাথে চলে গেলে composition, থেকে যায় আর share হয় তাহলে aggregation, থেকে যায় আর দুই দিকই একে অপরকে চেনে তাহলে association, আর কিছুই না কারণ কিছু রাখা হয়নি তাহলে dependency। Is-a এর চেয়ে has-a বেশি পছন্দ করো, আর অংশ বানানো আর মোছার অধিকার শুধু মালিককে দাও।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →