Composite Pattern: বাক্সের ভেতরে বাক্স — একটা জিনিস আর অনেক জিনিসকে একই চোখে দেখো
কুরিয়ারের পার্সেলে বাক্সের ভেতরে বাক্সের উদাহরণ দিয়ে Composite pattern শেখো। একটা আইটেম আর পুরো গ্রুপকে একই interface দিয়ে ট্রিট করো, আর সহজ recursion দিয়ে মোট হিসাব বের করো।
📦 ঈদের আগের সেই পার্সেল
ধরো ঈদের ঠিক আগের দিন। সুমাইয়ার মা ঢাকায় একটা বড় পার্সেল প্যাক করছেন — চট্টগ্রামে দাদির কাছে পাঠাবেন। সুমাইয়ার বয়স বারো, আর সে ছিল এই অপারেশনের অফিশিয়াল "টেপ ম্যানেজার"। তাই সে একদম জানে ভেতরে কী কী গেছে।
বড় কার্টনে গেছে: একটা জামদানি শাড়ি, একটা মিষ্টির বাক্স, আর একটা মাঝারি বাক্স। মাঝারি বাক্সের ভেতরে: দুটো আতরের প্যাকেট, একটা ছবির ফ্রেম, আর একটা ছোট বাক্স। আর ছোট বাক্সের ভেতরে: চকলেট আর দাদির জন্য একটা শুভেচ্ছা কার্ড। বাক্সের ভেতরে বাক্স, তার ভেতরে আবার বাক্স — রাশিয়ান পুতুলের মতো, কিন্তু বাদামি স্কচ টেপ আর সুমাইয়ার আঙুলের ছাপ সর্বত্র।
পরদিন সকালে সুমাইয়া আর মা কার্টনটা নিয়ে কুরিয়ার দোকানে গেলেন। কাউন্টারের পেছনে বসে আছেন জামাল ভাই, যিনি সারাজীবনে ঢাকার অর্ধেক পার্সেল ওজন করেছেন। তিনি বললেন, "ভাবি, চার্জ হবে মোট ওজনের উপর।"
এখন আমার তোমার কাছে একটা প্রশ্ন। মোট ওজন বের করতে জামাল ভাই কি প্রতিটা বাক্স খুলে, প্রতিটা জিনিস বের করে, কাউন্টারে আলাদা আলাদা ওজন করলেন? মাঝারি বাক্সের ভেতরের ছোট বাক্সটাও কি খুললেন?
অবশ্যই না! তিনি শুধু পুরো বড় কার্টনটা দাঁড়িপাল্লায় রাখলেন। একটাই রিডিং। শেষ। রশিদ প্রিন্ট করা সহ মাত্র চল্লিশ সেকেন্ড।
এটা কেন কাজ করে? কারণ ওজন একটাই সুন্দর নিয়ম মানে:
- একটা item-এর ওজন হলো শুধু তার নিজের ওজন।
- একটা বাক্সের ওজন হলো তার ভেতরের সব কিছুর ওজনের যোগফল — আর এটা কোনো ব্যাপার না যে "ভেতরের জিনিস" একটা item নাকি আরেকটা বাক্স। একই নিয়ম আরেক স্তর নিচেও চলে।
প্রতিটা nesting level-এ নিয়ম একই। তাই কাউকে জানতে হয় না বাক্সগুলো কত গভীরে গেছে। উপরের বাক্সকে জিজ্ঞেস করো "তোমার ওজন কত?", প্রশ্নটা চুপচাপ প্রতিটা বাক্স আর item-এর মধ্য দিয়ে নামে, আর উত্তরগুলো যোগ হতে হতে উপরে ফেরে।
তোমার স্কুলেও একই আকার দেখবে। একটা স্কুলে ক্লাস থাকে, একটা ক্লাসে সেকশন থাকে, একটা সেকশনে ছাত্রছাত্রী থাকে। প্রিন্সিপালকে জিজ্ঞেস করো "স্কুলে মোট কতজন ছাত্র?" — তিনি প্রতিটা class teacher-কে জিজ্ঞেস করেন, প্রতিটা class teacher সেকশন monitor-কে জিজ্ঞেস করেন, প্রতিটা monitor মাথা গণে। উপরের কারো জানতে হয় না নিচে আসলে কী হচ্ছে।
এই ধারণাটাই — একটা tree বানাও, তারপর যেকোনো node-কে একই প্রশ্ন করো — হলো Composite pattern।
সুমাইয়ার পার্সেলের দিনটা এখানে একটা যাত্রা হিসেবে দেখানো হলো। সবচেয়ে সেরা মুহূর্তটা খেয়াল করো: একটাই ওজন নেওয়া।
🌳 Composite pattern কী জিনিস?
সহজ ভাষায় definition টা দেখো।
Composite pattern হলো একটা structural design pattern যেটা তোমাকে object-গুলোকে একটা tree-তে সাজাতে দেয়, মানে পুরোর ভেতরে অংশ, বাক্সের ভেতরে বাক্স। আর তারপর client code একটা single object আর পুরো একটা group of objects-কে ঠিক একইভাবে ট্রিট করতে পারে, একটা shared interface দিয়ে।
এই pattern-এ তিনজন খেলোয়াড়:
| Pattern role | মানে | সুমাইয়ার গল্পে |
|---|---|---|
| Component | Shared interface যেটা simple আর grouped দুটো জিনিসই মানে; getWeight()-এর মতো common প্রশ্ন declare করে | "পার্সেলে যা কিছু দেওয়া যায় তাই" |
| Leaf | কোনো children নেই এমন simple node; নিজের data থেকে সরাসরি উত্তর দেয় | শাড়ি, আতরের প্যাকেট, শুভেচ্ছা কার্ড |
| Composite | Children-এর list রাখা container node; সব children-কে জিজ্ঞেস করে আর combine করে উত্তর দেয় | ছোট বাক্স, মাঝারি বাক্স, বড় কার্টন |
| Client | একটা Component reference রাখে আর একটা প্রশ্ন করে | জামাল ভাই আর তার দাঁড়িপাল্লা |
Leaf আর Composite দুটোই Component interface মানে। তাই client একটা Component type-এর reference রাখে আর একটাই method call করে। Recursion composite-এর ভেতরে লুকানো থাকে। এই কারণেই pattern-টাকে Object Treeও বলে।
Composite-এর মূল রহস্য একটাই লাইনে: Composite-এর children-গুলো Component হিসেবে typed, কখনো Leaf বা Box হিসেবে নয়। এই একটা সিদ্ধান্তই বাক্সের ভেতরে বাক্সের ভেতরে বাক্স সম্ভব করে, যেকোনো depth-এ, runtime-এ ঠিক হয়। বাক্স কখনো জিজ্ঞেস করে না "তুমি কি item নাকি বাক্স?" — সে প্রতিটা child-এ একই method call করে আর উত্তর বিশ্বাস করে।
পুরো pattern এক পাতায়:
💥 এই pattern কোন সমস্যা সমাধান করে
দেখো, এই pattern ছাড়া পার্সেল model করলে কী হয়। সহজ পদ্ধতিতে চেষ্টা করা যাক:
// BAD: two unrelated classes, and the client must tell them apart.
class Item {
constructor(public name: string, public weightKg: number) {}
}
class Box {
contents: (Item | Box)[] = [];
}
// The client must type-check at EVERY level. Ugly and fragile!
function totalWeight(thing: Item | Box): number {
if (thing instanceof Item) {
return thing.weightKg;
} else {
let sum = 0;
for (const inner of thing.contents) {
sum += totalWeight(inner); // and inside, the same branching again
}
return sum;
}
}এখন কাজ করছে। কিন্তু খরচটা দেখো। Client function-কে প্রতিটা ধরনের node জানতে হয় আর type-এ branch করতে হয়। কাল যদি কুরিয়ার Pouch, GiftWrap, বা Envelope চালু করে — তোমাকে totalWeight() খুলে নতুন branch যোগ করতে হবে। তারপর printContents() খুলতে হবে। তারপর countItems()। তোমার পুরো program-এর প্রতিটা walking function-কে প্রতিটা নতুন node type সম্পর্কে চিরকালের জন্য জানতে হবে। এটা একটা maintenance nightmare।
আসল সমস্যাটা হলো এটাই: client-কে tree-এর পুরো আকার বুঝতে হয় শুধু সেটা ব্যবহার করার জন্য। "একটা বাক্সের মোট কীভাবে বের করতে হয়" সেই জ্ঞান বাক্সের বাইরে, client code-এ বসে আছে, প্রতিটা function-এ copy করা। মানে যেন জামাল ভাইকে ঢাকার প্রতিটা পার্সেলের ভেতরে কী আছে জানতে হয় ওজন নেওয়ার আগে।
Composite দায়িত্বটা উল্টে দেয়। প্রতিটা node নিজের জন্য উত্তর দিতে জানে। Item নিজের ওজন দেয়। Box তার children-গুলো যোগ করে। Client শুধু একবার top node-কে জিজ্ঞেস করে — কোনো branching নেই, কোনো instanceof নেই, কখনোই না।
একটু ভাবো, কতটুকু naive code type checking-এ নষ্ট হয়। কয়েক ধরনের node আর কয়েকটা walking function থাকলে, branching নিজেই সবচেয়ে বড় অংশ হয়ে যায়:
Composite প্রথম দুটো অংশ মুছে ফেলে। প্রতিটা node শুধু তার নিজের ছোট্ট "actual useful work" রাখে, আর recursion plumbing একবার লেখা হয়, composite-এর ভেতরে।
🛠️ কীভাবে কাজ করে, ধাপে ধাপে
তোমার data যখন স্বাভাবিকভাবে tree তৈরি করে, তখন এই recipe follow করো।
- নিশ্চিত করো তোমার model সত্যিই একটা tree। দরকার parts আর wholes: বাক্সের ভেতরে item, folder-এর ভেতরে file, panel-এর ভেতরে widget। Nesting না থাকলে Composite সঠিক tool না।
- Component interface declare করো এমন operation দিয়ে যেটা simple আর grouped দুটো জিনিসের জন্যই মানে রাখে —
getWeight(),describe(),getPrice()। - Leaf class লেখো simple জিনিসগুলোর জন্য। প্রতিটা leaf নিজের data ব্যবহার করে সরাসরি operation implement করে।
- Composite class লেখো children list সহ। অনেক গুরুত্বপূর্ণ ব্যাপার: list-এর type হতে হবে
Component[]— interface — কখনো concrete class নয়। এটাই যেকোনো mixture আর যেকোনো depth সম্ভব করে। - Child management যোগ করো —
add(child)আরremove(child)— composite-এ। ঠিক কোথায় রাখবে সেটা নিয়ে নিচে trade-off section-এ আলোচনা করব। - Composite-এ shared operation implement করো children loop করে, প্রতিটায় একই operation call করে, result combine করে। ওজন যোগ করো, description জোড়া লাগাও, সব shape আঁকো।
- Client শুধু Component দিয়ে কাজ করুক। Tree বানাও, root-এর reference রাখো, আর root-কে তোমার প্রশ্ন করো। Tree বাকিটা নিজেই করবে।
সেই diagram-এর সবচেয়ে গুরুত্বপূর্ণ arrow হলো Box থেকে ParcelComponent-এ ফেরা loop-back। একটা box component রাখে, আর একটা box নিজেও একটা component — তাই box-এর ভেতরে box থাকতে পারে, যেকোনো depth-এ, কোনো extra code ছাড়া।
College corner: সেই loop-back arrow হলো একটা recursive data type, ঠিক যেমন linked list node অন্য node-এর pointer রাখে, বা directory entry আরেকটা directory-র দিকে point করে। Composite-এ প্রতিটা operation হলো একটা tree traversal। আমাদের getWeight() হলো classic depth-first, post-order traversal: একটা node-এর উত্তর তার সব children উত্তর দেওয়ার পরে হিসাব হয়, কারণ parent-এর children-দের result যোগ করতে দরকার। Recursion-এর base case হলো leaf, সে নিজের data থেকে উত্তর দেয়, আর কোনো call নেই। Recursive case হলো composite, সে প্রতিটা child-এ একই operation call করে। প্রতিটা node ঠিক একবার visit হয়, তাই n node-এর জন্য time complexity হলো O(n), আর maximum call-stack depth সমান tree-এর height — balanced tree-এর জন্য O(log n), একটার পর একটা বাক্সের chain হলে O(n)। কেউ যদি অনেক গভীর tree দেয়, stack overflow এড়াতে explicit stack দিয়ে recursion replace করা যায়। যখন তুমি bigCarton.getWeight() লেখো, তুমি তোমার data-structures course-এর DFS-এর মতোই কাজ করছো — pattern শুধু traversal-টা polymorphism-এর ভেতরে লুকিয়ে রাখে।
💻 Real-life code example
চলো সুমাইয়ার ঈদের পার্সেলটা TypeScript-এ pack করি আর code-কে দিয়ে ওজন করাই — ঠিক জামাল ভাইয়ের দাঁড়িপাল্লার মতো।
// ---------- COMPONENT: the shared interface ----------
interface ParcelComponent {
getWeight(): number; // in kilograms
describe(indent: string): void; // pretty-print the tree
}
// ---------- LEAF: a simple item, no children ----------
class Item implements ParcelComponent {
constructor(private name: string, private weightKg: number) {}
getWeight(): number {
return this.weightKg; // a leaf answers from its own data
}
describe(indent: string): void {
console.log(`${indent}- ${this.name} (${this.weightKg} kg)`);
}
}
// ---------- COMPOSITE: a box that can hold ANYTHING ----------
class Box implements ParcelComponent {
// Children are typed as the INTERFACE — items or boxes, we don't care.
private children: ParcelComponent[] = [];
constructor(private label: string, private ownWeightKg: number) {}
add(child: ParcelComponent): Box {
this.children.push(child);
return this; // returning `this` lets us chain add() calls
}
getWeight(): number {
// The box's weight = its own cardboard + sum of all children.
// child.getWeight() may recurse into deeper boxes. We never check!
let total = this.ownWeightKg;
for (const child of this.children) {
total += child.getWeight();
}
return total;
}
describe(indent: string): void {
console.log(`${indent}+ ${this.label} [box]`);
for (const child of this.children) {
child.describe(indent + " ");
}
}
}
// ---------- CLIENT: pack the Diwali parcel ----------
const smallBox = new Box("Small box", 0.1)
.add(new Item("Chocolates", 0.4))
.add(new Item("Greeting card", 0.05));
const mediumBox = new Box("Medium box", 0.2)
.add(new Item("Diya packet 1", 0.5))
.add(new Item("Diya packet 2", 0.5))
.add(new Item("Photo frame", 0.75))
.add(smallBox); // a box inside a box!
const bigCarton = new Box("Big carton", 0.5)
.add(new Item("Silk saree", 0.6))
.add(new Item("Kaju katli box", 1.0))
.add(mediumBox); // and that box goes inside the carton
// One call — like placing the carton on the weighing scale.
bigCarton.describe("");
console.log(`\nTotal weight: ${bigCarton.getWeight().toFixed(2)} kg`);
// Output:
// + Big carton [box]
// - Silk saree (0.6 kg)
// - Kaju katli box (1 kg)
// + Medium box [box]
// - Diya packet 1 (0.5 kg)
// - Diya packet 2 (0.5 kg)
// - Photo frame (0.75 kg)
// + Small box [box]
// - Chocolates (0.4 kg)
// - Greeting card (0.05 kg)
//
// Total weight: 4.60 kgbigCarton.getWeight() call করলে ঠিক কী হলো সেটা ধাপে ধাপে দেখো:
- বড় কার্টন তার নিজের cardboard (0.5) যোগ করল আর প্রতিটা child-কে তার ওজন জিজ্ঞেস করল।
- শাড়ি বলল 0.6, মিষ্টির বাক্স বলল 1.0 — leaf-রা তাৎক্ষণিক উত্তর দেয়।
- মাঝারি বাক্স একই কৌশল এক স্তর নিচে করল: 0.2 + 0.5 + 0.5 + 0.75, আর ছোট বাক্স যা বলে তাও।
- ছোট বাক্স আরেকবার করল: 0.1 + 0.4 + 0.05।
- উত্তরগুলো উপরে উঠে যোগ হয়ে 4.60 kg হলো।
আর এখানেই magic। কোথাও একটাও instanceof বা type check নেই। Client একটাই call করল। প্রতিটা node নিজের জন্য উত্তর দিল। কাল যদি কুরিয়ার BubbleWrapPouch আবিষ্কার করে, সে শুধু ParcelComponent implement করবে। আর সব existing function — ওজন, description, যাই হোক — সাথে সাথে কাজ করবে, একটুও না বদলে।
আমরা যে tree বানিয়েছি সেটা ছবিতে দেখো:
আর এখানে প্রশ্নটা নিচে যাচ্ছে আর উত্তরগুলো উপরে উঠছে, sequence হিসেবে দেখানো হলো। এটা চিত্র ৬-এর গতিময় রূপ:
প্রতিটা বাক্স শুধু তার সরাসরি children-এর সাথে কথা বলে। বড় কার্টন কখনো জানে না যে চকলেট আছে — সে শুধু মাঝারি বাক্স থেকে "2.50" শোনে আর বিশ্বাস করে। এই বিশ্বাস, প্রতিটা স্তরে বারবার — এটাই পুরো pattern।
একই সংখ্যাগুলো আরেকভাবে দেখো। nesting-এর স্তরগুলো নামো আর দেখো প্রতিটা স্তরে কতটুকু ওজন — উপরের scale কোনো স্তর নিজে না দেখেই সব দেখতে পায়:
Bar-গুলো সরাসরি প্রতিটা স্তরের ওজন দেখায়, মানে নিজের cardboard আর সরাসরি item। Line দেখায় cumulative উত্তর যেটা প্রতিটা বাক্স উপরে report করে — ঠিক চিত্র ৭-এর সংখ্যাগুলো। ছোট বাক্স বলে 0.55, মাঝারি বাক্স বলে 2.50, কার্টন বলে 4.60।
ওজন নেওয়ার পরে সুমাইয়া প্রতিদিন কুরিয়ার ওয়েবসাইটে পার্সেল track করত। একটা পার্সেলের জীবন নিজেই একটা সুন্দর ছোট state machine:
লক্ষ্য করো যে বাক্সের পুরো tree এই state-গুলোর মধ্য দিয়ে একটা unit হিসেবে যায়। এটা group-কে single জিনিস হিসেবে ট্রিট করার আরেকটা সুবিধা: কুরিয়ার একটাই পার্সেল track করে, নয়টা আলাদা item না।
🌐 C# আর Python-এ একই ধারণা
Pattern-টা সব জায়গায় কাজ করে সেটা প্রমাণ করতে, এখানে C#-এ school structure version। স্কুলে ক্লাস, ক্লাসে সেকশন, সেকশনে ছাত্র, আর একটাই call-এ মাথা গণা:
// Component
public interface ISchoolUnit
{
int CountStudents();
}
// Leaf
public class Student : ISchoolUnit
{
public string Name { get; }
public Student(string name) => Name = name;
public int CountStudents() => 1; // a student counts as one
}
// Composite — works for a Section, a Class, or the whole School
public class SchoolGroup : ISchoolUnit
{
private readonly List<ISchoolUnit> _members = new();
public string Label { get; }
public SchoolGroup(string label) => Label = label;
public SchoolGroup Add(ISchoolUnit unit)
{
_members.Add(unit);
return this;
}
public int CountStudents()
=> _members.Sum(m => m.CountStudents()); // recursion, hidden
}
// Client
var sectionA = new SchoolGroup("6-A")
.Add(new Student("Aarav")).Add(new Student("Meera"));
var sectionB = new SchoolGroup("6-B")
.Add(new Student("Riya")).Add(new Student("Kabir")).Add(new Student("Zoya"));
var class6 = new SchoolGroup("Class 6").Add(sectionA).Add(sectionB);
var school = new SchoolGroup("Sunrise School").Add(class6);
Console.WriteLine($"Total students: {school.CountStudents()}");
// Output: Total students: 5Principal (client) স্কুলকে একটাই প্রশ্ন করল। স্কুল তার ক্লাসগুলোকে জিজ্ঞেস করল, ক্লাস তাদের সেকশনকে, সেকশন তাদের ছাত্রদের গণল। একই pattern, ভিন্ন গল্প।
আর এখানে একই ধারণার তৃতীয় রূপ — Python-এ একটা ছোট্ট file system, যেখানে folder-এর size তার ভেতরের সব কিছুর যোগফল:
# Component: anything with a size — file or folder.
class FsNode:
def get_size_kb(self) -> int:
raise NotImplementedError
# Leaf: a file knows its own size.
class TextFile(FsNode):
def __init__(self, name: str, size_kb: int):
self.name, self.size_kb = name, size_kb
def get_size_kb(self) -> int:
return self.size_kb # base case of the recursion
# Composite: a folder asks its children and adds.
class Folder(FsNode):
def __init__(self, name: str):
self.name = name
self.children: list[FsNode] = [] # typed as the COMPONENT
def add(self, node: FsNode) -> "Folder":
self.children.append(node)
return self
def get_size_kb(self) -> int:
return sum(child.get_size_kb() for child in self.children)
# Client: one question at the root.
photos = Folder("photos").add(TextFile("diwali.jpg", 2048))
docs = Folder("docs").add(TextFile("notes.txt", 12)).add(photos)
root = Folder("riya-laptop").add(docs).add(TextFile("todo.txt", 1))
print(f"Total: {root.get_size_kb()} KB")
# Output: Total: 2061 KBপার্সেলের ওজন, ছাত্রের সংখ্যা, folder-এর size — তিনটা গল্প, একটাই আকার। যখনই একটা whole তার parts দিয়ে তৈরি, আর parts নিজেরাও whole হতে পারে, Composite তোমার জন্য অপেক্ষা করছে।
🌍 Real software-এ কোথায় দেখবে
Composite একবার জানলে, সর্বত্র tree দেখবে।
- HTML DOM। একটা web page হলো বিশাল Composite। একটা
<div>paragraph, list, আর আরো<div>রাখে। প্রতিটা element common Node/Element interface share করে। Browser page render করলে বা তুমিelement.remove()call করলে, একই operation single element আর পুরো subtree-র উপর সমানভাবে চলে। পৃথিবীর সবচেয়ে বেশি ব্যবহৃত Composite এটাই — তুমি এখন একটার মধ্যে বসে পড়ছো। - File system। Folder-এ file আর অন্য folder থাকে। "Folder-এর size" হলো তার entries-এর size-এর যোগফল — ঠিক আমাদের পার্সেলের ওজনের নিয়ম। File explorer-এর delete, copy, search সব operation একটা file বা পুরো folder tree-তে একটা uniform ধারণায় কাজ করে।
- UI component tree। WPF আর JavaFX button, label, আর panel-কে container-এ compose করে, container-কে window-এ। Layout আর drawing recursively tree walk করে। React-এর virtual DOM আর Flutter-এর widget tree একই আকার follow করে।
- Organisation chart আর menu। একজন manager-এর team size হলো তার সব report-এর team size-এর যোগফল। একটা menu-তে menu item আর submenu থাকে যেখানে আরো item থাকে। দুটোই textbook Composite।
- Expression tree। Calculator আর compiler
(2 + 3) * 4-কে tree হিসেবে store করে। সংখ্যাগুলো leaf, operator-গুলো composite। Expression evaluate করা হলো recursivegetValue()— পার্সেলের ওজনের কৌশল, গণিতের পোশাকে। - Graphics editor। যেকোনো drawing tool-এ তিনটা shape group করো আর এক সাথে drag করো — group হলো composite, shape-গুলো leaf, আর
move(dx, dy)পুরো subtree-র উপর চলে। - পড়ার মতো open-source example। iluwatar/java-design-patterns Composite example word আর letter দিয়ে sentence বানায়। Refactoring.Guru-র Composite page nested order box-এর price যোগ করে — সুমাইয়ার পার্সেল price tag সহ।
✅ কখন ব্যবহার করবে আর কখন না
জামাল ভাই একটা envelope ওজন নেওয়ার আগে তিনটা বাক্সে ঢোকান না। Pattern তখনই কাজের যখন সত্যিকারের nesting আছে। তোমার case check করো:
| পরিস্থিতি | Composite ব্যবহার করবে? | কেন |
|---|---|---|
| তোমার data স্বাভাবিকভাবে part–whole tree, যেমন folder, menu, UI, org chart | হ্যাঁ | এটাই pattern-এর উদ্দেশ্য |
| Client code বারবার জিজ্ঞেস করে "এটা একটা জিনিস নাকি group?" | হ্যাঁ | Composite সেই branch সম্পূর্ণ মুছে দেয় |
| Nesting depth অজানা আর runtime-এ ঠিক হয় | হ্যাঁ | Recursive children-as-Component কৌশল যেকোনো depth handle করে |
| পুরো structure-এ একটা operation দরকার, যেমন total, render, count | হ্যাঁ | Root-এ একটাই call, recursion বাকি সব করে |
| তোমার object-গুলো একে অপরকে ধারণ করে না | না | Tree নেই, Composite নেই — plain list যথেষ্ট |
| Leaf আর container-এর সম্পূর্ণ ভিন্ন, unrelated operation দরকার | না | একটা shared interface জোর করলে সেটায় অর্থহীন method ভরে যাবে |
| Structure সবসময় ঠিক এক level গভীর আর এমনই থাকবে | না | একটা simple collection loop pattern-এর চেয়ে অনেক পরিষ্কার |
একই সিদ্ধান্ত ছবিতে — গভীর nesting আর uniform operation হলো pattern-এর আদর্শ ক্ষেত্র:
⚠️ ছাত্ররা যে ভুল করে
Classic beginner bug হলো composite-এ অন্য composite থাকতে পারে সেটা ভুলে যাওয়া। ছাত্ররা বাক্সের total লেখে "children-দের নিজের ওজনের যোগফল" হিসেবে, "children-দের getWeight()-এর যোগফল"-এর বদলে। প্রথম version মুহূর্তেই ভেঙে পড়ে যখন একটা বাক্স অন্য বাক্সের ভেতরে থাকে। সবসময় child-এর method-এ delegate করো আর recursion-কে তার কাজ করতে দাও — কখনো বাইরে থেকে child-এর data-তে হাত দিও না।
আর classic design dilemma যেটা প্রতিটা ছাত্রকে জানতে হবে — transparency বনাম safety:
- Transparent design: Component interface-এ
add()আরremove()রাখো। এখন client প্রতিটা node-কে একইভাবে ট্রিট করতে পারে। কিন্তু একটাStudentleaf এখনadd()method রাখে যেটার কোনো মানে নেই। এটাকে exception throw করতে হবে বা চুপচাপ কিছু না করতে হবে। ভুলটা runtime-এ সম্ভব হয়। - Safe design: শুধু Composite class-এ
add()আরremove()রাখো, আমাদের উপরের code এটাই করে। Compiler কাউকে leaf-এ child যোগ করতে দেবে না। কিন্তু tree বানানো client-কে মাঝে মাঝে check বা cast করতে হয়।
কোনোটাই "সঠিক উত্তর" না। যদি তোমার client বেশিরভাগ tree traverse করে, transparent দিকে যাও। যদি বেশি tree বানায় আর modify করে, safe দিকে যাও। এই trade-off জানাই বোঝায় যে তুমি সত্যিই Composite বুঝেছ।
College corner: transparency-vs-safety dilemma আসলে Liskov Substitution Principle আর interface design সম্পর্কে প্রশ্ন। Transparent version দাবি করে প্রতিটা Component children নিতে পারে — একটা প্রতিশ্রুতি leaf রাখতে পারে না। তাই leaf-কে runtime-এ contract ভাঙতে হয়, সাধারণত UnsupportedOperationException দিয়ে, ঠিক Java-র read-only collection যা করে। Safe version contract সৎ রাখে কিন্তু type জ্ঞান client-এ ফিরিয়ে দেয়, pattern-এর motivating uniformity একটু দুর্বল করে। GoF book নিজেই transparency বেছে নিয়ে খোলাখুলি বলে এটা safety-র বিরুদ্ধে trade-off। Interview-এ দুটো option, তাদের cost, আর LSP angle বলতে পারলে শক্তিশালী উত্তর হয়।
আরো কিছু trap আছে:
- Children list concrete class দিয়ে type করা, যেমন
children: Box[]বাchildren: Item[]। এটা নীরবে mixing নিষিদ্ধ করে, আর পুরো pattern ভেঙে পড়ে। Children অবশ্যই Component interface হিসেবে typed হতে হবে। - Parent link যত্ন ছাড়া যোগ করা। মাঝে মাঝে children-দের তাদের parent-এর reference দরকার, যেমন tree-তে উপরে যেতে। যোগ করা ঠিক আছে, কিন্তু
add()আরremove()-এ update করতে মনে রেখো, নাহলে tree মিথ্যা বলবে। - Cycle। যদি box A-কে box B-তে আর B-কে A-তে রাখা হয়, recursion কখনো শেষ হবে না। Real tree-এ loop থাকে না —
add()-এ guard রাখো যদি users স্বাধীনভাবে structure বানাতে পারে। সুমাইয়া ছোট বাক্সের ভেতরে কার্টন রাখতে পারে না, তোমার code-ও পারা উচিত না। - কাজ দুবার করা। একটা node যদি দুটো parent-এর নিচে যোগ করা যায়, তাহলে একটা
getWeight()call তাকে দুবার গণবে। Tree মানে প্রতিটা node-এর সর্বোচ্চ একটা parent — এটা enforce করো।
👪 সমগোত্রীয়দের সাথে তুলনা
Composite-এর সাথে সবচেয়ে বেশি গুলিয়ে ফেলা হয় Decorator-কে, কারণ দুটোই shared interface দিয়ে component wrap করে আর recursive composition ব্যবহার করে। পার্থক্য বুঝতে:
| প্রশ্ন | Composite | Decorator |
|---|---|---|
| কতজন children বা wrapped object? | অনেক — children-এর list | ঠিক একটা — একটা wrapped object |
| মূল উদ্দেশ্য | Group থেকে result combine করা, যেমন যোগ বা সব render করা | একটা object-এ extra behaviour যোগ করা, যেমন logging, border, caching |
| যে আকার তৈরি করে | প্রশস্ত, গভীর tree | সরল chain of wrapper |
| সাধারণত যে প্রশ্নের উত্তর দেয় | "তোমাদের সবার মোট কত?" | "একই জিনিস, কিন্তু extra সুবিধা সহ?" |
| মনে রাখার কৌশল | অনেক কিছু ভরা পার্সেল | একটা উপহার অনেক স্তরের কাগজে মোড়া |
এক লাইনের test: children গণো। অনেক children মানে Composite। একটা wrapped object মানে Decorator। আর লক্ষ্য করো — এরা একসাথে সুন্দর কাজ করে। একটা UI tree মানে Composite, যেখানে একটা widget scroll decorator-এ wrap করা মানে Decorator — GUI framework-এ এটা প্রতিদিনের code।
Composite-এর আরো সহায়ক বন্ধু আছে। Iterator composite tree walk করে children list expose না করে। Visitor পুরো tree-তে নতুন operation যোগ করে node class edit না করে। Builder বড় tree ধাপে ধাপে বানাতে সাহায্য করে। আর Flyweight অনেক identical leaf-কে একটা object share করতে দেয় memory বাঁচাতে — ভাবো দশ হাজার identical আতরের প্যাকেট সহ একটা পার্সেল।
📦 Quick revision box
+=====================================================================+
| COMPOSITE PATTERN — REVISION CARD |
+=====================================================================+
| Type : Structural pattern |
| Nickname : Object Tree |
| Story : Diwali parcel — boxes inside boxes, one weighing |
| |
| Players : Component -> shared interface (getWeight) |
| Leaf -> simple item, answers from own data |
| Composite -> box; children: Component[] ; combines |
| |
| Heart : children are typed as COMPONENT, never concrete |
| Client rule : ask the root ONE question; recursion does the rest |
| No instanceof, no type checks, ever. |
| |
| CS view : post-order DFS, leaf = base case, O(n) visit |
| Trade-off : add()/remove() on interface = transparent, unsafe |
| add()/remove() on composite = safe, less transparent |
| vs Decorator: many children = Composite; one wrapped = Decorator |
+=====================================================================+🏋️ Practice exercise
তোমার editor খোলো — এই কাজগুলো pattern-টা মাথায় গেঁথে দেবে।
-
পার্সেলের দাম। ঈদের পার্সেল code-এ দ্বিতীয় operation যোগ করো,
getPrice(): number। Item-এর নিজের দাম আছে, বাক্সের দাম হলো তার contents-এর যোগফল, cardboard বিনামূল্যে। একটাই call দিয়ে বড় কার্টনের মোট বিল print করো। Bonus:countItems()operation যোগ করো আর নিশ্চিত করো সুমাইয়ার পার্সেলে 7 ফেরে। -
School head-count, upgraded। C# school example নাও আর
Teacherleaf যোগ করো যারCountStudents()0 দেয়। তারপর নতুন operationCountPeople()যোগ করো যেটা সবাই গণে। শেষে তোমার পছন্দমতো দুটো সেকশন সহClass 7যোগ করো আর দেখো total ঠিকঠাক আসছে কিনা — client-এর একটাইschool.CountStudents()call না ছুঁয়ে। -
Mini file explorer। এই article-এর Python file system extend করো:
print_tree()method যোগ করো যেটা indent সহ structure দেখায়, terminal-এরtreecommand-এর মতো। কমপক্ষে তিন স্তর গভীর structure বানাও, তারপর সৎভাবে উত্তর দাও — একটাওisinstanceলাগল কি? যদি উত্তর হ্যাঁ হয়, সেটা খুঁজে সরিয়ে ফেলো। -
Recursion trace করো। কাগজে, exercise 1-এর solution-এ
getPrice()-এর জন্য sequence diagram আঁকো, চিত্র ৭-এর মতো। প্রতিটা return arrow-এ সংখ্যা লেখো, আর base case-গুলো বৃত্ত দিয়ে চিহ্নিত করো। যদি প্রতিটা বৃত্ত leaf হয় আর প্রতিটা বাক্স শুধু যোগ করে, তোমার mental model নিখুঁত।
যখন তুমি একটা tree বানাতে পারবে, root-কে একটা প্রশ্ন করবে, আর উত্তর বিশ্বাস করবে — তুমি Composite আয়ত্ত করেছ। সুমাইয়ার পার্সেল 4.60 kg নিয়ে চট্টগ্রাম পৌঁছাল, দাদি চকলেট পেয়ে খুশি হলেন, আর জামাল ভাই একটাও বাক্স খোলেননি। তিনটা structural pattern শেষ। অসাধারণ অগ্রগতি — এই গতি ধরে রাখো!
সচরাচর জিজ্ঞাসা
- সহজ ভাষায় Composite pattern আসলে কী?
- Composite pattern দিয়ে তুমি tree structure বানাতে পারো, যেমন বাক্সের ভেতরে বাক্স। তারপর একটা single item আর পুরো একটা group-কে একই interface দিয়ে ট্রিট করা যায়। যেকোনো node-এ একটাই method call করো, যেমন getWeight(), আর tree নিজেই recursion সামলে নেয়।
- এই pattern-এ Leaf আর Composite মানে কী?
- Leaf হলো সহজ একটা node যার কোনো children নেই। সে নিজেই কাজ করে, যেমন একটা item তার নিজের ওজন দেয়। Composite হলো একটা container node যেখানে children-এর list থাকে। সে প্রতিটা child-কে একই প্রশ্ন করে আর result একত্রিত করে, যেমন একটা বাক্স তার ভেতরের সব কিছুর ওজন যোগ করে।
- real software-এ Composite pattern কোথায় ব্যবহার হয়?
- যেখানেই tree দেখবে সেখানে। HTML DOM-এ elements-এর ভেতরে elements, file system-এ folder-এর ভেতরে files আর folder, React, WPF, JavaFX-এর মতো UI framework-এ panel-এর ভেতরে buttons আর আরো panel, organisation chart, আর submenu সহ menu system।
- Composite-এ transparency বনাম safety trade-off কী?
- যদি add() আর remove() common Component interface-এ রাখো, তাহলে leaf-গুলোতেও এই method থাকে যদিও সেটার কোনো মানে নেই, এটা transparent কিন্তু unsafe। আর যদি এগুলো শুধু Composite class-এ রাখো, compiler leaf-কে রক্ষা করে, কিন্তু client-কে মাঝে মাঝে type check করতে হয়, মানে safe কিন্তু কম transparent। তোমার use case দেখে বেছে নিতে হবে।
- Composite আর Decorator-এর পার্থক্য কী?
- দুটোই shared interface দিয়ে recursive composition ব্যবহার করে। কিন্তু Composite অনেক children রাখে আর তাদের result combine করে। Decorator ঠিক একটা object wrap করে তাতে extra behaviour যোগ করে। সহজ মনে রাখার উপায়: অনেক children মানে Composite, একটা wrapped object মানে Decorator।
আরো দেখো
সম্পর্কিত পাঠ
Adapter Pattern: চল্লিশ টাকার প্লাগ যা পুরনো আর নতুন কোডকে মিলিয়ে দেয়
একটা সহজ ৩-পিন প্লাগ আর ২-পিন সকেটের গল্পের মাধ্যমে Adapter pattern শিখো। কোনো পক্ষ না বদলেই পুরনো কোডকে নতুন কোডের সাথে কাজ করাও।
Bridge Pattern: একটা রিমোট, অনেক ডিভাইস — subclass বিস্ফোরণ থামাও
টিভি আর রিমোটের গল্প দিয়ে Bridge pattern শেখো। একটা বড় class কে দুই ভাগে ভেঙে আলাদাভাবে বাড়াও — বাড়তি subclass-এর ঝামেলা আর নেই।
Decorator Pattern: Object-এ একটা একটা করে Layer চাপাও
চায়ের দোকানের এক মজার গল্প দিয়ে Decorator pattern শেখো। নতুন Subclass না বানিয়েই Runtime-এ Object-এ নতুন Behaviour যোগ করো — একটার পর একটা Layer Wrap করে।
Flyweight Pattern: একটা জার্সি ডিজাইন, পুরো দলের খেলোয়াড়
ক্রিকেট জার্সির গল্প দিয়ে Flyweight pattern শেখো। হাজার হাজার object-এর মধ্যে ভারী common data শেয়ার করো আর বিশাল পরিমাণ memory বাঁচাও।