Decorator Pattern: Object-এ একটা একটা করে Layer চাপাও
চায়ের দোকানের এক মজার গল্প দিয়ে Decorator pattern শেখো। নতুন Subclass না বানিয়েই Runtime-এ Object-এ নতুন Behaviour যোগ করো — একটার পর একটা Layer Wrap করে।
চায়ের দোকান আর একটা খিদেপেটা ছেলে
ধরো রুবেল — ক্লাস নাইনের ছাত্র, বয়স পনের, সবসময় পেটে খিদে। প্রতিদিন কোচিং শেষে বাসার পথে সে থামে জামাল ভাই-এর চায়ের দোকানে। জামাল ভাই বিশ বছর ধরে চা বানাচ্ছেন। একটা জিনিস অসাধারণ পারেন তিনি: সাদা চা। এক কাপ মাত্র দশ টাকা।
আজ রুবেলের হাতে পকেট মানি আছে। "ভাই, আদা দেন।"
দেখো জামাল ভাই কী করলেন। তিনি চা ফেলে নতুন করে "আদা চা" বানাতে যাননি। সেই একই কাপ চায়ে আদা কুচি ছেড়ে দিলেন। সাদা চা হয়ে গেল আদা চা। দাম বাড়ল পাঁচ টাকা।
রুবেল আজ মেজাজে আছে। "এলাচিও দেন, ভাই।"
জামাল ভাই আদা চায়েই এলাচি ছেড়ে দিলেন। এখন সেটা আদা-এলাচি চা। আরও সাত টাকা।
রুবেলের পাশে দাঁড়িয়ে আছে তার বড় আপু ফাতেমা, CSE-র দ্বিতীয় বর্ষের ছাত্রী। সে চায় দুধ, আদা আর এলাচি একসাথে। জামাল ভাইয়ের কাছে কোনো সমস্যা নেই। মাঝখানে সেই একই সাদা চা, উপরে তিনটা জিনিস — বিলে তিনটা ছোট সংযোজন।
ফাতেমা দোকানটা দেখছে ঠিক যেভাবে একজন CSE ছাত্রীই দেখতে পারে। হঠাৎ রুবেলের হাত ধরে বলল, "দেখলি? ভাই কখনো নতুন আইটেম বানান না। তাঁর কাছে একটা Base Item আর কিছু Topping আছে। প্রতিটা Topping তার নিচের চাকে Wrap করে। প্রতিটা Topping নিজের স্বাদ আর দাম যোগ করে। আর প্রতিটা Layered চা শেষ পর্যন্ত একটা চাই — একইভাবে চুমুক দিয়ে খাওয়া যায়।"
তারপর মোক্ষম প্রশ্ন করল, "মেনু যদি class দিয়ে লেখা হতো? PlainTea, GingerTea, CardamomTea, GingerCardamomTea, MilkGingerCardamomTea..." জামাল ভাইকে প্রতিটা combination-এর জন্য নতুন class লিখতে হতো। পাঁচটা উপকরণ থাকলে ২ × ২ × ২ × ২ × ২ = ৩২টা class। দশটা উপকরণ থাকলে এক হাজারেরও বেশি class। বেচারা চা ওয়ালা চা বানানোর বদলে সারাজীবন menu card লিখতেই কাটিয়ে দিতেন।
জামাল ভাইয়ের কৌশল — একটা base রাখো, তাকে layer-এ layer-এ wrap করো, প্রতিটা layer কিছু একটা যোগ করে — এটাই হলো Decorator pattern। এই পুরো আর্টিকেলটা সেই চায়ের দোকানের গল্প, ধীরে ধীরে বলা।
Journey-টা পুরো pattern-টাকে ছোট করে দেখাচ্ছে। Order phase-এ layer বেছে নেওয়া হয়। Cook phase-এ base-এর recipe না ছুঁয়েই layer stack করা হয়। Pay phase-এ stack-এর নিচ থেকে উপর পর্যন্ত প্রতিটা layer-এর দাম যোগ করা হয়।
Decorator pattern কী?
Decorator হলো একটা structural design pattern। এটা তোমাকে একটা object-কে আরেকটা object দিয়ে wrap করে নতুন behaviour যোগ করতে দেয়। এই wrapping object-কে বলা হয় decorator। Decorator সেই object-এর মতোই একই interface implement করে যেটাকে সে wrap করছে। কারণ wrapper-টা দেখতে ভেতরের জিনিসের মতোই, তুমি একটা wrapper-কে আবার wrap করতে পারো — এভাবে যতটা layer খুশি stack করতে পারো।
প্রতিটা layer তিনটা কাজ করে:
- ভেতরের object-এর (অর্থাৎ wrappee-র) একটা reference রাখে।
- সেই inner object-এ call forward করে।
- সেই call-এর আগে, পরে বা মাঝখানে নিজের ছোট্ট behaviour যোগ করে।
শেষে client-এর কাছে মাত্র একটা reference থাকে। সে একটা method call করে। Call-টা প্রতিটা layer-এর মধ্য দিয়ে ঢেউয়ের মতো যায়, আর প্রতিটা layer নিজের স্বাদ মেশায় — ঠিক যেমন প্রতিটা উপকরণ চায়ে স্বাদ যোগ করে।
সহজে মনে রাখো: Decorator = একই চেহারা, বাড়তি শক্তি। বাইরের interface কখনো বদলায় না। শুধু behaviour বাড়ে, layer-এর পর layer। Interface বদলালে সেটা আর Decorator থাকে না — সেটা হবে Adapter।
এই pattern-কে Wrapper-ও বলা হয়, কারণ এটা আক্ষরিক অর্থেই wrap করে।
College corner: Decorator হলো "Inheritance-এর চেয়ে Composition পছন্দ করো" এই design principle-এর জীবন্ত প্রমাণ। Inheritance একটা is-a relationship তৈরি করে — class compile হওয়ার সময় ঠিক হয়ে যায়, চিরকালের জন্য। Composition একটা has-a relationship তৈরি করে — object-এ object-এ আলাদাভাবে সাজানো যায়, program চলার সময়ে। Decorator চালাকিভাবে দুটোই ব্যবহার করে: interface inherit করে (যাতে সে component-এর যেকোনো জায়গায় দাঁড়াতে পারে — এটা Liskov Substitution Principle কাজে লাগছে) আর behaviour compose করে (যাতে প্রতিটা object আলাদা stack বহন করতে পারে)। ফলাফলটা Open/Closed Principle-ও মেনে চলে: system নতুন topping-এর জন্য open (শুধু একটা class যোগ করো) কিন্তু modification-এর জন্য closed (কেউ আর কখনো PlainTea edit করবে না)।
যে সমস্যাটা এটা সমাধান করে
দেখা যাক pattern ছাড়া কী হয়। ধরো ফাতেমা তার university app-এর জন্য একটা notification system লিখছে। Version one শুধু email-এ message পাঠায়:
class EmailNotifier {
send(message: string): void {
console.log(`Email sent: ${message}`);
}
}একটু পরে parents SMS চাইলেন। Teachers WhatsApp চাইলেন। Principal push notification চাইলেন। তাই সে subclass বানাতে শুরু করল:
// ভুল পথ: প্রতিটা combination-এর জন্য আলাদা class
class EmailAndSmsNotifier { /* ... */ }
class EmailAndWhatsAppNotifier { /* ... */ }
class EmailAndSmsAndWhatsAppNotifier { /* ... */ }
class SmsAndPushNotifier { /* ... */ }
// ... এবং এটা কখনো শেষ হয় নাnটা channel থাকলে ২ⁿটা combination সম্ভব। চারটা channel মানে ১৬টা class পর্যন্ত। পাঁচটা মানে ৩২। আর একটা দ্বিতীয়, আরও চালাক সমস্যাও আছে: inheritance compile time-এ ঠিক হয়ে যায়। একবার কোনো object EmailAndSmsNotifier হিসেবে জন্ম নিলে, program চলার সময় সে কখনো "শুধু email" বা "email plus push" হতে পারবে না। পছন্দটা চিরকালের জন্য জমে যায়।
মূল কারণ: inheritance পুরো class-এ প্রযোজ্য হয়, একটা object-এ নয়। তুমি এই বিশেষ notifier-কে বলতে পারবে না, "এখন থেকে SMS-ও পাঠাও।" দরকার হলো runtime-এ object অনুযায়ী behaviour compose করার একটা উপায়।
বাঁ দিকে, প্রতিটা নতুন combination-এর জন্য নতুন class লাগছে। ডান দিকে, কোন wrapper stack করবে তা বেছে নিলেই যেকোনো combination তৈরি হয়। চারটা ছোট class দিয়ে ষোলটা combination পাওয়া সম্ভব।
Topping বাড়ার সাথে সাথে explosion কতটা খারাপ হয়? এই কঠিন অঙ্কটা দেখো:
প্রথম line হলো subclassing পথ: ২ⁿটা class, মাত্র দশটা topping-এ ১০২৪-তে পৌঁছে যায়। দ্বিতীয় line হলো decorator পথ: একটা base আর প্রতিটা topping-এর জন্য একটা ছোট class, n + 1-এ থামে। এই একটা chart-ই pattern-এর সবচেয়ে জোরালো যুক্তি। কেউ "শুধু subclass করলেই হয় না কেন?" জিজ্ঞেস করলে এই curve দেখিয়ে দাও।
কীভাবে কাজ করে, ধাপে ধাপে
Pattern-টার চারটা ছোট অংশ আছে। এই ধাপগুলো follow করলে যেকোনো সমস্যায় এটা বানাতে পারবে:
- একটা Component interface define করো। এটা সেই সাধারণ shape যেটা real object আর প্রতিটা wrapper দুজনেই মানবে। আমাদের চায়ের দোকানে:
getLabel()আরgetCost()। - ConcreteComponent লেখো। এটা সেই base object যেটা আসল কাজটা করে — সাদা চা। Decorator-গুলো এর behaviour-এ যোগ করবে।
- একটা base Decorator class লেখো। এটা Component interface implement করে এবং আরেকটা Component-এর (wrappee-র) reference রাখে। Default-এ এটা শুধু প্রতিটা call ভেতরে forward করে। এটা একটা transparent pass-through।
- ConcreteDecorator লেখো। প্রতিটা base decorator extend করে আর forward করা call-এর আশেপাশে নিজের behaviour যোগ করতে একটা method override করে। আদা তার নাম আর পাঁচ টাকা যোগ করে। এলাচি তার নাম আর সাত টাকা যোগ করে।
- Client-কে compose করতে দাও। Client একটা সাদা চা বানায়, তারপর যেকোনো ক্রমে যেকোনো decorator দিয়ে wrap করে। Final object-টা তখনও সাধারণ
Componentহিসেবে typed।
Magic line হলো এটা: একটা decorator IS-A Component (তাই সে যেকোনো জায়গায় component-এর মতো দাঁড়াতে পারে) আর HAS-A Component (তাই সে একটাকে wrap করতে পারে)। এজন্যই decorator stack হয়।
এখানে pattern-এর পুরো cast, জামাল ভাইয়ের দোকানে map করা:
| Pattern-এর অংশ | চায়ের দোকানে | Code-এ | কাজ |
|---|---|---|---|
| Component | "একটা চা, যেকোনো চা" | Tea interface | সবাই যে সাধারণ shape মানে |
| ConcreteComponent | কাপে সাদা চা | PlainTea | আসল base কাজ করে |
| Base Decorator | "একটা উপকরণ, যেকোনো উপকরণ" | AddOnDecorator | Wrappee ধরে রাখে, default-এ forward করে |
| ConcreteDecorator | আদা, এলাচি, দুধ | GingerAddOn ইত্যাদি | একটা স্বাদ আর একটা দাম যোগ করে |
| Client | রুবেল আর ফাতেমা order করছে | Calling code | কোন layer stack করবে তা বেছে নেয় |
Diagram-টা উপর থেকে নিচে পড়ো। PlainDosa আর ToppingDecorator দুজনেই Dosa-র মতো আচরণ করার প্রতিশ্রুতি দেয়। কিন্তু decorator-টা ভেতরে একটা Dosa-ও রাখে। তাই CheeseTopping-এর ভেতরে MasalaTopping থাকতে পারে, যার ভেতরে PlainDosa। তিনটা layer, একটা interface।
একটা order-এর জীবন state হিসেবে দেখাটাও সাহায্য করে। একটা order সাদা base হিসেবে শুরু হয়, যেকোনো সংখ্যকবার wrap হয়, আর bill total করলে serve করা হয়:
Wrapped-এর উপর loop-টাই pattern-এর হৃদয় — তুমি সেই state-এ যতক্ষণ খুশি থাকতে পারো, একটার পর একটা layer যোগ করতে পারো, কারণ প্রতিটা wrap আরেকটা valid object তৈরি করে।
বাস্তব Code Example
TypeScript-এ পুরো চায়ের দোকানটা code করা যাক। দেখো wrapper-গুলো কীভাবে stack হয়।
// Step 1: The Component interface.
// Every dosa — plain or loaded with toppings — must answer
// these two questions: what are you, and what do you cost?
interface Dosa {
getDescription(): string;
getCost(): number;
}
// Step 2: The ConcreteComponent — the base item.
class PlainDosa implements Dosa {
getDescription(): string {
return "Plain Dosa";
}
getCost(): number {
return 40; // rupees
}
}
// Step 3: The base Decorator.
// It IS a Dosa (implements the interface)
// and it HAS a Dosa (holds the wrappee).
abstract class ToppingDecorator implements Dosa {
constructor(protected wrappee: Dosa) {}
// By default, just pass the call inward.
getDescription(): string {
return this.wrappee.getDescription();
}
getCost(): number {
return this.wrappee.getCost();
}
}
// Step 4: Concrete decorators. Each adds one topping.
class MasalaTopping extends ToppingDecorator {
getDescription(): string {
return this.wrappee.getDescription() + " + Masala";
}
getCost(): number {
return this.wrappee.getCost() + 20;
}
}
class CheeseTopping extends ToppingDecorator {
getDescription(): string {
return this.wrappee.getDescription() + " + Cheese";
}
getCost(): number {
return this.wrappee.getCost() + 30;
}
}
class ButterTopping extends ToppingDecorator {
getDescription(): string {
return this.wrappee.getDescription() + " + Butter";
}
getCost(): number {
return this.wrappee.getCost() + 15;
}
}
// Step 5: The client composes the order — STACKING wrappers.
// Order 1: just a plain dosa.
let order1: Dosa = new PlainDosa();
console.log(`${order1.getDescription()} = Rs.${order1.getCost()}`);
// Order 2: masala dosa. Wrap once.
let order2: Dosa = new MasalaTopping(new PlainDosa());
console.log(`${order2.getDescription()} = Rs.${order2.getCost()}`);
// Order 3: Sneha's butter cheese masala dosa. Wrap three times!
// Read it from inside out: PlainDosa -> Masala -> Cheese -> Butter.
let order3: Dosa = new ButterTopping(
new CheeseTopping(
new MasalaTopping(
new PlainDosa()
)
)
);
console.log(`${order3.getDescription()} = Rs.${order3.getCost()}`);
// You can even add a topping AFTER the order is made.
// Arjun leans over: "One more cheese, please!"
order3 = new CheeseTopping(order3);
console.log(`${order3.getDescription()} = Rs.${order3.getCost()}`);Output:
Plain Dosa = Rs.40
Plain Dosa + Masala = Rs.60
Plain Dosa + Masala + Cheese + Butter = Rs.105
Plain Dosa + Masala + Cheese + Butter + Cheese = Rs.135শেষ দুটো line মনোযোগ দিয়ে দেখো। আমরা ইতিমধ্যে শেষ হওয়া একটা order-এ runtime-এ এক line code দিয়ে দ্বিতীয় cheese layer যোগ করলাম। কোনো নতুন class নেই। পুরনো class edit নেই। প্রতিটা getCost() call প্রতিটা layer-এর মধ্য দিয়ে যায় — butter cheese-কে জিজ্ঞেস করে, cheese masala-কে, masala plain dosa-কে — আর উত্তরটা ফিরে আসে প্রতিটা ধাপে বাড়তে বাড়তে। Layer-গুলোর মধ্য দিয়ে এই ripple-টাই Decorator pattern-এর হৃদস্পন্দন।
এখানে সেই ripple, একটা sequence diagram হিসেবে। ফাতেমার একটা নিরীহ call, চার ধাপ নিচে, চারটা উত্তর উপরে:
আরও লক্ষ্য করো: প্রতিটা order variable সাধারণ Dosa হিসেবে typed। Client কখনো জানে না, বা জানার দরকারও নেই, ভেতরে কয়টা layer আছে।
দামটা আসলে কোথায় যাচ্ছে? বেশিরভাগ base-এ, প্রতিটা layer তার ছোট অংশ নিচ্ছে:
এই pie যেকোনো decorated object-এর জন্য একটা ভালো mental model: base মূল behaviour দেয়, আর প্রতিটা wrapper একটা সুনির্দিষ্ট slice দেয়। কোনো slice ভুল হলে, কোন class খুলতে হবে তুমি জানো।
C#-এ একই ধারণা
C#-এও shape একই। এখানে একটু অন্য flavour-এর compact version — একটা সহজ message sender-এ logging যোগ করা:
// Component
public interface INotifier
{
void Send(string message);
}
// ConcreteComponent — the base behaviour
public class EmailNotifier : INotifier
{
public void Send(string message)
=> Console.WriteLine($"Email: {message}");
}
// Base decorator: is-a INotifier, has-a INotifier
public abstract class NotifierDecorator : INotifier
{
protected readonly INotifier Wrappee;
protected NotifierDecorator(INotifier wrappee) => Wrappee = wrappee;
public virtual void Send(string message) => Wrappee.Send(message);
}
// Concrete decorators
public class SmsDecorator : NotifierDecorator
{
public SmsDecorator(INotifier w) : base(w) { }
public override void Send(string message)
{
base.Send(message); // pass inward first
Console.WriteLine($"SMS: {message}"); // then add behaviour
}
}
public class WhatsAppDecorator : NotifierDecorator
{
public WhatsAppDecorator(INotifier w) : base(w) { }
public override void Send(string message)
{
base.Send(message);
Console.WriteLine($"WhatsApp: {message}");
}
}
// Client: stack the layers
INotifier notifier = new WhatsAppDecorator(
new SmsDecorator(
new EmailNotifier()));
notifier.Send("PTM on Friday at 10 AM");
// Output:
// Email: PTM on Friday at 10 AM
// SMS: PTM on Friday at 10 AM
// WhatsApp: PTM on Friday at 10 AMSend-এ একটা call, তিনটা channel কাজ করে। Channel বদলাতে হলে কোন wrapper stack করবে শুধু সেটা বদলাও — একটাও class edit করতে হয় না।
Python-এ একটু, আর ক্রমের প্রশ্ন
Python-এ decorator প্রায় ভারহীন মনে হয়, কারণ function-গুলোও object। এখানে আবার দোকানের গল্প, আর সবচেয়ে গুরুত্বপূর্ণ ব্যবহারিক নিয়মের একটা প্রদর্শনী — ক্রম গুরুত্বপূর্ণ:
class PlainDosa:
def description(self): return "Plain Dosa"
def cost(self): return 40
class Topping:
"""Base decorator: holds the wrappee, forwards by default."""
def __init__(self, wrappee): self.wrappee = wrappee
def description(self): return self.wrappee.description()
def cost(self): return self.wrappee.cost()
class Masala(Topping):
def description(self): return self.wrappee.description() + " + Masala"
def cost(self): return self.wrappee.cost() + 20
class SchezwanSauce(Topping):
"""A spicy wrapper that DOUBLES the price of whatever it covers."""
def description(self): return self.wrappee.description() + " + Schezwan"
def cost(self): return self.wrappee.cost() * 2
# Order A: masala first, then schezwan doubles (40 + 20) = 120
order_a = SchezwanSauce(Masala(PlainDosa()))
# Order B: schezwan doubles the base (40 * 2), then masala adds 20 = 100
order_b = Masala(SchezwanSauce(PlainDosa()))
print(order_a.description(), "= Rs.", order_a.cost()) # Rs. 120
print(order_b.description(), "= Rs.", order_b.cost()) # Rs. 100একই দুটো topping, একই base, আলাদা bill — শুধু stacking order বদলেছে বলে। এক line অদলবদলে বিশ টাকার পার্থক্য।
College corner: এই ordering effect কোনো খেলনা বিষয় নয়। এটা decorator-এ সবচেয়ে সাধারণ production bug। আগে compress তারপর encrypt করা ভালো কাজ করে, কারণ plain text-এ pattern থাকে যা compression খুঁজে পায়। আগে encrypt তারপর compress করলে প্রায় কিছুই shrink হয় না, কারণ ভালো ciphertext random noise-এর মতো দেখায় — compress করার মতো কোনো pattern থাকে না। একইভাবে, middleware pipeline-এ authentication wrapper-কে logging wrapper-এর বাইরে রাখলে মানে failed login কখনো log হয় না। সাধারণ নিয়ম: একটা decorator শুধু সেটাই দেখতে বা পরিবর্তন করতে পারে যা তার ভেতরের layer-গুলো ইতিমধ্যে তৈরি করেছে। Stack order-কে তোমার design-এর অংশ হিসেবে ভাবো, লিখে রাখো, আর অন্তত একটা property test করো যেটা ক্রম বদলালে ভেঙে যায়।
বাস্তব Software-এ কোথায় দেখা যায়
Decorator pattern শুধু পাঠ্যপুস্তকের ধারণা নয়। তুমি প্রতিদিন যেসব tool ব্যবহার করো তার ভেতরে এটা চুপচাপ কাজ করছে।
- Java I/O Streams. এটা সবচেয়ে বিখ্যাত উদাহরণ।
new BufferedReader(new InputStreamReader(new FileInputStream("f.txt")))হলো তিনটা decorator layer।FileInputStreamraw byte পড়ে,InputStreamReadercharacter decoding যোগ করে,BufferedReaderbuffering আর সহজreadLine()method যোগ করে। প্রতিটা wrapper একটা stream নেয় আর আরও সমৃদ্ধ stream ফিরিয়ে দেয়। - .NET Stream Wrappers. হুবহু একই ধারণা: compression-এর জন্য
FileStream-কেGZipStream-এ wrap করো, তারপর encryption-এর জন্য সেটাকেCryptoStream-এ wrap করো। প্রতিটা wrapper এখনও একটাStream, তাই stack করতেই থাকতে পারো। - Middleware Pipelines. Express (Node.js) আর ASP.NET Core-এর মতো web framework প্রতিটা request-কে middleware-এর একটা chain-এর মধ্য দিয়ে পাঠায় — logging, authentication, compression। প্রতিটা অংশ পরের handler-কে wrap করে আর তার চারপাশে behaviour যোগ করে।
- TypeScript/Java Annotation আর Decorator. TypeScript-এর
@decoratorsyntax আর NestJS-এর মতো library একই spirit ব্যবহার করে: কোনো class বা method edit না করে তাতে বাড়তি behaviour wrap করো। - UI Toolkit. Window component-এর চারপাশে scrollbar বা border যোগ করা classically decorator দিয়ে করা হয় — wrapped widget এখনও একটা widget।
- Open-source Reference. iluwatar-এর java-design-patterns Repository-তে একটা পরিষ্কার, runnable decorator example আছে যেটায় হাজার হাজার star — বাড়তি পড়ার জন্য দারুণ।
কখন ব্যবহার করবে আর কখন করবে না
| পরিস্থিতি | Decorator ব্যবহার করবে? |
|---|---|
| Runtime-এ একটা object-এ behaviour যোগ করতে হবে, পুরো class-এ নয় | হ্যাঁ |
| Feature অনেকভাবে combine হতে পারে (topping, channel, filter) | হ্যাঁ — wrapper ২ⁿ subclass-এর চেয়ে ভালো |
| পরে কোনো behaviour সরিয়ে ফেলতে হতে পারে | হ্যাঁ — unwrap করো; subclass কখনো un-subclass হয় না |
Class-টা final/sealed আর extend করা যাচ্ছে না | হ্যাঁ — পরিবর্তে wrap করো |
| শুধু একটা fixed combination আছে, একবারের জন্য সিদ্ধান্ত নেওয়া | না — একটা সহজ subclass বা plain method সহজতর |
| Object-এর behaviour না, interface বদলাতে হবে | না — ওটা Adapter pattern |
| যোগ করা behaviour-কে object-এর private internal দেখতে বা বদলাতে হবে | না — decorator শুধু public interface দেখে |
Object identity গুরুত্বপূর্ণ (code === বা concrete class-এ type check করে) | সতর্ক থাকো — wrapper আলাদা object |
একই সিদ্ধান্ত, একটা map হিসেবে। খুঁজে বের করো তোমার সমস্যা কোথায় পড়ে:
উপরের ডান quadrant — অনেক combination, program চলার সময় বেছে নেওয়া — এটা decorator-এর এলাকা। নিচের বাঁ — একটা combination, চিরকালের জন্য জানা — subclass বা এমনকি plain function-এর চেয়ে জটিল কিছু দরকার নেই।
ছাত্রছাত্রীরা যে ভুলগুলো করে
ভুল ১: Call forward করতে ভুলে যাওয়া। একটা decorator-কে অবশ্যই wrappee-কে call করতে হবে। তোমার getCost() যদি শুধু topping-এর দাম ফেরত দেয় আর this.wrappee.getCost() ভুলে যায়, তাহলে পুরো inner stack চুপ হয়ে যায়। প্রতিটা decorator method সাধারণত ভেতরে delegate করবে।
ভুল ২: Layer-এর ক্রম উপেক্ষা করা। Encrypt-then-compress আর compress-then-encrypt আলাদা ফলাফল দেয় — encrypted data compress করলে প্রায় সংকুচিত হয় না। Stack order হলো তোমার program-এর logic-এর অংশ, লিখে রাখো।
ভুল ৩: Interface ভাঙা। তোমার wrapper যদি এমন নতুন public method যোগ করে যেটা interface-এ নেই, তাহলে Component type ধরে রাখা client-গুলো সেটা দেখতে পাবে না। Caller-দের concrete wrapper type জানতে হলে তুমি pattern-এর মূল সুবিধা হারিয়ে ফেলেছ।
ভুল ৪: Decorated object-এ instanceof ব্যবহার করা। order3 instanceof PlainDosa হলো false, যদিও ভেতরে গভীরে একটা plain dosa আছে। Decorated object concrete-type check-এ ব্যর্থ হয়। সবসময় interface-এর বিপরীতে program করো।
ভুল ৫: হাজারটা layer-এ মৃত্যু। দশটা ছোট decorator, প্রতিটা আধা line behaviour যোগ করছে, stack trace দশ frame গভীর করে আর debugging কষ্টকর করে। অনেক layer যদি সবসময় একসাথে চলে, তাদের merge করো বা একটা configuration list থেকে build করো।
প্রতিবেশী Pattern-গুলোর সাথে তুলনা
Decorator-এর দুটো দেখতে-একরকম প্রতিবেশী আছে: Proxy আর Adapter। তিনটাই একটা object wrap করে। পার্থক্য হলো কেন wrap করে।
| প্রশ্ন | Decorator | Proxy | Adapter |
|---|---|---|---|
| Client-এর কাছে interface | Wrapped object-এর মতোই | Wrapped object-এর মতোই | আলাদা (রূপান্তরিত) |
| মূল উদ্দেশ্য | Behaviour যোগ করা | Access নিয়ন্ত্রণ করা (lazy load, permission, cache) | Shape বদলানো যাতে অসামঞ্জস্যপূর্ণ code connect করতে পারে |
| কে wrapper বানায়? | Client, ইচ্ছা করে | সাধারণত framework/wiring; client জানতেও নাও পারে | Integration code |
| অনেক layer গভীরে stack? | হ্যাঁ, এটাই উদ্দেশ্য | কদাচিৎ — সাধারণত একটা | কদাচিৎ |
| এক line স্মৃতির সূত্র | বাড়তি muscle, একই চেহারা | Gatekeeper, একই চেহারা | নতুন চেহারা, একই muscle |
আরও এক line করে:
- Composite অনেক child রাখে আর সবাইকে একইভাবে দেখে। Decorator ঠিক একটা child রাখে আর behaviour যোগ করে। UI tree-তে এরা প্রায়ই একসাথে কাজ করে।
- Strategy একটা আলাদা algorithm plug in করে object-এর ভেতরটা বদলায়। Decorator চারপাশে wrap করে বাইরেটা বদলায়।
- Chain of Responsibility-ও linked object-এর মধ্য দিয়ে call পাঠায়, কিন্তু একটা handler chain বন্ধ করতে পারে। Decorator stack-এ প্রতিটা layer forward করবে বলেই আশা করা হয়।
এই আর্টিকেলের সব কিছু, একটা tree-তে ঝুলানো:
দ্রুত Revision Box
+--------------------------------------------------------------+
| DECORATOR — QUICK REVISION |
+--------------------------------------------------------------+
| Idea : Wrap an object to add behaviour. Same interface. |
| Nickname : Wrapper |
| Analogy : Plain dosa + masala + cheese + butter layers |
| Key trick : Decorator IS-A Component and HAS-A Component |
| -> so wrappers can stack on wrappers |
| Parts : Component, ConcreteComponent, |
| BaseDecorator, ConcreteDecorators |
| Wins : No 2^n subclass explosion; add/remove at runtime |
| Watch out : Layer ORDER matters; always forward the call; |
| instanceof checks break on wrapped objects |
| Cousins : Proxy (controls access), Adapter (new interface) |
| Real life : Java BufferedReader, .NET GZipStream/CryptoStream|
| Express / ASP.NET middleware pipelines |
+--------------------------------------------------------------+Practice Exercise
নিজে নিজে চেষ্টা করো। উপরের example-টা template হিসেবে ব্যবহার করো।
- চায়ের দোকান।
getCost()আরgetLabel()দিয়ে একটাBeverageinterface বানাও। Base হিসেবেPlainChai(১০ টাকা) বানাও, তারপর decorator:Ginger(+৫),Elaichi(+৭), আরExtraStrong(+৫)।Gingerদুইবার stack করে "double ginger elaichi chai" বানাও। Label আর total print করো। - Text Pipeline।
read(): stringসহ একটাTextSourceinterface বানাও। Base class একটা fixed sentence ফেরত দেয়। তিনটা decorator লেখো:UpperCaseDecorator,TrimDecorator, আরStarBorderDecorator(আগে পরে***যোগ করে)। দুটো ভিন্ন ক্রমে stack করো আর দুটো ফলাফল print করো। এক বাক্যে ব্যাখ্যা করো কেন output-গুলো আলাদা। - Challenge — Unwrap করা। এই pattern মাঝের একটা layer সরিয়ে নেওয়া কঠিন করে। একটা ছোট function লেখো যেটা একটা দেওয়া topping ছাড়া decorator stack পুনর্নির্মাণ করে (hint: topping-গুলো একটা array-তে রাখো, তারপর array-কে stack-এ fold/reduce করো)। এটা তোমাকে শেখাবে কেন অনেক real system list থেকে decorator configure করে।
- College challenge — Explosion মাপো। একটা ছোট script লেখো যেটা n = 1 থেকে 12 পর্যন্ত print করে subclassing পথে কতটা class লাগবে (২ⁿ) বনাম decorator পথে (n + 1)। তারপর দুটো বাক্য লেখো কোন SOLID principle-গুলো decorator পথ পূরণ করে আর কেন।
সচরাচর জিজ্ঞাসা
- Decorator pattern সহজ ভাষায় কী?
- Decorator হলো একটা Object-কে আরেকটা Object দিয়ে Wrap করে নতুন Behaviour যোগ করার উপায়। Wrapper-টা হুবহু একই Interface ব্যবহার করে, তাই Client বুঝতেই পারে না ভেতরে কী আছে। চাইলে একের পর এক অনেক Wrapper Stack করতে পারো — ঠিক চায়ে একটার পর একটা উপকরণ যোগ করার মতো।
- Decorator আর Inheritance-এর মধ্যে পার্থক্য কী?
- Inheritance পুরো Class-এর জন্য Compile Time-এ Behaviour ঠিক করে দেয়। Decorator Runtime-এ একটা নির্দিষ্ট Object-এ Behaviour যোগ করে। পরে Decorator সরিয়ে ফেলা যায়, কিন্তু Parent Class কখনো সরানো যায় না।
- Decorator আর Proxy-এর পার্থক্য কী?
- দুটোই একই Interface দিয়ে Object Wrap করে। Decorator বাড়তি Behaviour যোগ করে, Client ইচ্ছামতো Stack করতে পারে। Proxy Access নিয়ন্ত্রণ করে — Lazy Loading, Permission, Caching — আর সাধারণত Client-এর হয়ে Object পরিচালনা করে।
- Decorator-এর ক্রম কি গুরুত্বপূর্ণ?
- হ্যাঁ, অনেক বেশি। আগে Compress তারপর Encrypt আর আগে Encrypt তারপর Compress সম্পূর্ণ আলাদা ফলাফল দেয়। Wrapper-গুলো কোন ক্রমে চলবে সেটা সবসময় মাথায় রাখো।
- বাস্তব Software-এ Decorator কোথায় দেখা যায়?
- Java-র BufferedReader, .NET-এর GZipStream আর CryptoStream, Express বা ASP.NET Core-এর Middleware Pipeline — সবখানেই Decorator-এর ধারণা কাজ করছে।
আরো দেখো
সম্পর্কিত পাঠ
Proxy Pattern: দারোয়ান যে মালিকের সাথে দেখা করার আগেই চেক করে
দারোয়ানের গল্প দিয়ে Proxy pattern শেখো। একটা real object-এর সামনে same interface-এর একটা stand-in রেখে সেই object-এ access নিয়ন্ত্রণ করো।
Adapter Pattern: চল্লিশ টাকার প্লাগ যা পুরনো আর নতুন কোডকে মিলিয়ে দেয়
একটা সহজ ৩-পিন প্লাগ আর ২-পিন সকেটের গল্পের মাধ্যমে Adapter pattern শিখো। কোনো পক্ষ না বদলেই পুরনো কোডকে নতুন কোডের সাথে কাজ করাও।
Composite Pattern: বাক্সের ভেতরে বাক্স — একটা জিনিস আর অনেক জিনিসকে একই চোখে দেখো
কুরিয়ারের পার্সেলে বাক্সের ভেতরে বাক্সের উদাহরণ দিয়ে Composite pattern শেখো। একটা আইটেম আর পুরো গ্রুপকে একই interface দিয়ে ট্রিট করো, আর সহজ recursion দিয়ে মোট হিসাব বের করো।
Facade Pattern: একটা ফোন কলেই পুরো জটিল ট্রিপ বুক
ট্রাভেল এজেন্টের গল্প দিয়ে Facade pattern শেখো। অনেকগুলো জটিল subsystem-কে একটা সহজ method-এর পেছনে লুকিয়ে রাখো, যাতে client code ছোট আর পরিষ্কার থাকে।