Part 01 · Low Level Design

Class Relationships and UML

AssociationAggregationCompositionComposition over inheritanceUML

চেনে, রাখে আর মালিক: 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 এর sessionSession ও যাবে: আলাদা করে এদের কোনো মানে নেই
Batch এর teacherTeacher থাকবে: তার নিজের অস্তিত্ব আছে, অন্য batch ও পড়ান
Batch এর studentStudent থাকবে, শুধু তার batch এর list update করতে হবে
Report printer এর হাতে batchকিছুই না: printer শুধু একবারের জন্য ধার নিয়েছিল

Code যদি চারটাকে একই ভাবে treat করে, তাহলে হয় এমন কিছু মুছে যাবে যেটা মোছার কথা না (teacher), নয়তো কিছু জিনিস ঝুলে থাকবে (এমন enrolment যেটা একটা নেই হয়ে যাওয়া batch কে point করছে)।

একটা ভাড়া বাসা আর তার আসবাবের কথা ভাবো

তুমি একটা flat ভাড়া নিলে। রুম গুলো flat এর: building ভাঙলে রুমও শেষ। এটা composition।

তোমার আসবাব flat এর ভেতরে আছে, কিন্তু ওগুলো তোমার: বাসা ছাড়লে sofa তোমার সাথে নতুন বাসায় যাবে। এটা aggregation।

তুমি আর তোমার প্রতিবেশী একে অপরকে চেনো, কিন্তু একজন বাসা ছাড়লে আরেকজন হারিয়ে যায় না। এটা association।

Plumber এক ঘণ্টার জন্য আসে, তোমার রান্নাঘর ব্যবহার করে, চলে যায়। সে এখানে থাকে না। এটা dependency।

Batch- name: string- teacher: *Teacher- sessions: []Session- students: []*Student+ Cancel()Teacher+ Name: stringSession+ Date: stringStudent+ Name: stringReportPrinter+ Print(b *Batch)teachersessionsstudentsusesaggregation ◇: the teacher outlives the batchcomposition ◆: sessions live inside the batchassociation →: a batch knows its studentsdependency ⇢: uses a batch for a moment, keeps nothingcancel(): the sessions go with the batchthe teacher and the students stay
Play চাপো। Batch এর চারপাশের চারটা সম্পর্ক একটা একটা করে জ্বলে। তারপর batch cancel হয়: session গুলো সাথে চলে যায়, teacher আর student রা থেকে যায়।

তবে একটা জায়গায় মেলে না। বাস্তবে কে কীসের মালিক, সেটা চোখেই দেখা যায়। Code এ এটা শুধু একটা field যেটা একটা reference ধরে আছে, আর syntax দেখে চার রকম সম্পর্কই একরকম লাগে। আসল পার্থক্য হলো object টা কে বানায়, কে মোছে, আর আর কে কে ওটা ধরে রাখতে পারে। এটা ইচ্ছা করে ঠিক করতে হয়, আর লিখে রাখতে হয়।

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

একটা Batch cancel হলো। কোন কোন object এর সাথে চলে যাওয়া উচিত?

চার রকম সম্পর্ক, আর has-a কেন সাধারণত is-a এর চেয়ে ভালো

  1. Association ("চেনে"): দুইটা object জোড়া, কিন্তু একজন ছাড়াও আরেকজন থাকতে পারে। Student আর batch: many-to-many, আর দুই দিক থেকেই একে অপরকে খুঁজতে হতে পারে। আঁকা হয় একটা সাধারণ লাইন দিয়ে, প্রায়ই * থেকে * এর মতো সংখ্যা সহ।
  2. Aggregation ("রাখে, কিন্তু share করে"): একটা পুরো জিনিস কিছু অংশ রাখে, যেগুলোর নিজের অস্তিত্ব আছে আর অন্যের সাথে share হতে পারে। Batch এর একজন teacher আছে; সেই teacher এর অন্য batch ও আছে। আঁকা হয় পুরো জিনিসটার দিকে একটা ফাঁকা diamond দিয়ে।
  3. Composition ("মালিক"): পুরো জিনিসটা নিজেই তার অংশ বানায়, আর সে গেলে অংশগুলোও যায়। আর কেউ ওগুলো ধরে রাখে না। Batch তার session গুলোর মালিক। আঁকা হয় ভরাট diamond দিয়ে। Code এ মালিক সাধারণত অংশটা নিজেই বানায়, আর এমন কোনো reference বাইরে দেয় না যেটা তার চেয়ে বেশিদিন বাঁচবে।
  4. Dependency ("একটু সময়ের জন্য ব্যবহার করে"): একটা class আরেকটাকে parameter বা local variable হিসেবে ব্যবহার করে, কোনো reference রেখে দেয় না। PrintSummary(batch) Batch এর উপর depend করে। আঁকা হয় dashed তীর দিয়ে। এটা সবচেয়ে হালকা সম্পর্ক, আর সবচেয়ে সস্তা।
  5. Inheritance ("এটা ওটারই একটা রকম"): TeachingAssistant is a Teacher। এটা সবচেয়ে শক্ত বাঁধন: child parent এর সব public method পেয়ে যায়, সারাজীবনের জন্য। এই কারণেই default পরামর্শ হলো composition over inheritance: যে আচরণ লাগবে সেটা একটা field এ রাখো, subclass হয়ে সেটা নিতে যেও না। একজন মানুষ একসাথে student আর teaching assistant, composition দিয়ে এটা সহজ (একজন মানুষ, কয়েকটা role), inheritance দিয়ে ঝামেলার (কোন class extend করবে?)।
  6. UML class diagram আসলে শুধু কয়েকটা box (নাম, জরুরি field, জরুরি method) আর এই লাইন গুলো। Interview তে ঠিক লাইন দিয়ে দ্রুত একটা sketch করলে code লেখার আগেই "কে কীসের মালিক" প্রশ্নের উত্তর হয়ে যায়।

একটা batch cancel করা, ধাপে ধাপে

ধাপে ধাপে দেখো

school.Cancel(HSC-Physics)

SchoolBatchSession গুলোTeacher আর student

ধাপ 1/4 School cancel করার সিদ্ধান্ত নেয়

Batch খোলা আর বন্ধ করা হয় School এ, তাই পরিষ্কার করার rule গুলোও এখানে থাকে।

open
HSC-Physics, HSC-Math

প্রতিটা লাইনের জন্য একটা প্রশ্ন

দুইটা class এর মাঝে যেকোনো লাইন আঁকার আগে জিজ্ঞেস করো: এইটা না থাকলে ওইটার কী হবে?

  • ওটাও যাবে → composition। মালিক ওটা বানায়, আর কেউ ধরে রাখে না।
  • থাকবে, আর অন্যরাও share করে → aggregation।
  • থাকবে, আর দুই দিকই একে অপরের খবর রাখে → association।
  • কিছুই না, কারণ কোনো reference রাখা হয়নি → dependency।

আর inheritance এর তীর আঁকার আগে বাক্যটা "সবসময়" দিয়ে জোরে বলে দেখো: "একজন teaching assistant সবসময়, সব দিক থেকে একজন teacher।" একটু দ্বিধা লাগলে composition নাও।

নিজে বলো: এটা কোন সম্পর্ক?

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

সম্পর্কটার নাম বলো

order.lines = [line1, line2]
কাজ করে: Composition। Order line গুলো এই order এর জন্যই বানানো, order ছাড়া এদের কোনো মানে নেই। Order মুছলে line ও যায়।

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

ছয়টাই হুবহু এটা 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 রাখো। Enroll batch আর 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 করে

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

ভাঙা সম্পর্কটা বাছো

DELETE batch → also DELETE its teacher
ভাঙে: Teacher aggregated, কারো মালিকানায় না। তাকে মুছলে সে যত batch পড়ান, সব ভেঙে যায়।

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

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 পাও →