Decorator Design Pattern

ধরো, তুমি একটি প্রিমিয়াম কফি শপে গেছ। সেখানে এক কাপ সাধারণ ব্ল্যাক কফির দাম ৫০ টাকা। কিন্তু তুমি চাইলে কফিটিকে নিজের মতো কাস্টমাইজ করতে পারো। তুমি যদি এক্সট্রা দুধ (Milk) নাও, তবে ২০ টাকা যোগ হবে। যদি চকোলেট সিরাপ (Chocolate) চাও, তবে আরও ৩০ টাকা যোগ হবে। আর যদি উপরে হুইপড ক্রিম (Whipped Cream) দিতে বলো, তবে আরও ২৫ টাকা যুক্ত হবে।

এখানে খেয়াল করে দেখো, তোমার মূল অবজেক্টটি (Black Coffee) কিন্তু পরিবর্তন হচ্ছে না। বরং মূল কফির চারপাশ দিয়ে তুমি নতুন নতুন ফ্লেভার বা লেয়ার যোগ করছ, এবং সাথে সাথে কফির দাম ও বর্ণনা আপডেট হয়ে যাচ্ছে। সফটওয়্যার ইঞ্জিনিয়ারিংয়ের দুনিয়ায়, মূল অবজেক্টে হাত না দিয়ে তার ওপর দিয়ে নতুন ফিচার মুড়িয়ে (Wrap) দেওয়ার এই চমৎকার কনসেপ্টটিকেই বলা হয় Decorator Design Pattern।

Decorator Design Pattern কী?

Decorator Pattern হলো একটি Structural Design Pattern, যা ইনহেরিটেন্স (Inheritance) ব্যবহার না করে কম্পোজিশন (Composition)-এর মাধ্যমে একটি অবজেক্টে নতুন ফিচার বা বিহেভিয়ার যুক্ত করার সুযোগ দেয়।

এটি মূলত অবজেক্টের চারদিকে একটি নতুন আবরণ বা 'র‍্যাপার' (Wrapper) তৈরি করে। এই প্যাটার্নটি Open/Closed Principle (OCP)-কে শতভাগ মেনে চলে, যার অর্থ হলো—একটি ক্লাস নতুন ফিচার যুক্ত করার জন্য উন্মুক্ত (Open) থাকবে, কিন্তু তার ভেতরের কোড পরিবর্তন করার জন্য বন্ধ (Closed) থাকবে।

ইনহেরিটেন্স বনাম ডেকোরেটর: কেন এটি এত স্পেশাল?

তুমি ভাবতে পারো, "কফির সাথে দুধ যোগ করার জন্য তো আমি MilkCoffee নামে একটি সাবক্লাস বানাতে পারি!" হ্যাঁ, পারো। কিন্তু সমস্যা হলো, এরপর কেউ যদি চকোলেট চায়, তবে ChocolateCoffee বানাতে হবে। কেউ যদি দুধ এবং চকোলেট দুটোই চায়, তবে MilkAndChocolateCoffee বানাতে হবে। এভাবে সাবক্লাস তৈরি করতে থাকলে একসময় ক্লাসের পাহাড় জমে যাবে, যাকে সফটওয়্যার ইঞ্জিনিয়ারিংয়ে "Class Explosion" বলা হয়।

Decorator প্যাটার্ন সাবক্লাস তৈরি না করে, অবজেক্টগুলোকে রানটাইমে একে অপরের ওপর লেয়ার হিসেবে স্ট্যাক (Stack) করার সুযোগ দেয়। ফলে মাত্র ৩-৪টি ডেকোরেটর ক্লাস দিয়ে তুমি অসংখ্য কম্বিনেশন তৈরি করতে পারবে।

কখন এবং কেন Decorator Pattern ব্যবহার করবে?

  • Runtime Modification: যখন কোড চলার সময় (Runtime) ডাইনামিকভাবে কোনো অবজেক্টের বিহেভিয়ার বা ফিচার পরিবর্তন করতে হয়।

  • Prevent Class Explosion: অনেকগুলো ফিচারের বিভিন্ন কম্বিনেশন দরকার হলে, ইনহেরিটেন্সের বদলে ডেকোরেটর ব্যবহার করা সবচেয়ে ভালো সমাধান।

  • Single Responsibility Principle (SRP): বিশাল একটি ক্লাসে সব ফিচার না রেখে, ছোট ছোট ডেকোরেটর ক্লাসে ফিচারগুলো ভাগ করে দেওয়া যায়।

Python Code Example: Coffee Shop Implementation

নিচে পাইথন (Python) ব্যবহার করে অবজেক্ট-ওরিয়েন্টেড উপায়ে ডেকোরেটর প্যাটার্নের একটি ক্লাসিক ইমপ্লিমেন্টেশন দেখানো হলো। (কোডটি সম্পূর্ণ ইংরেজিতে লেখা হয়েছে)।

from abc import ABC, abstractmethod

# ==========================================
# Step 1: Base Interface / Component
# ==========================================
class Coffee(ABC):
    @abstractmethod
    def get_cost(self) -> float:
        pass

    @abstractmethod
    def get_description(self) -> str:
        pass

# ==========================================
# Step 2: Base Concrete Class
# ==========================================
class BasicCoffee(Coffee):
    def get_cost(self) -> float:
        return 50.00
    
    def get_description(self) -> str:
        return "Simple Black Coffee"

# ==========================================
# Step 3: Abstract Decorator Class
# ==========================================
class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        # Composition: The decorator holds a reference to a Coffee object
        self._coffee = coffee
        
    def get_cost(self) -> float:
        return self._coffee.get_cost()
        
    def get_description(self) -> str:
        return self._coffee.get_description()

# ==========================================
# Step 4: Concrete Decorators
# ==========================================
class Milk(CoffeeDecorator):
    def get_cost(self) -> float:
        return super().get_cost() + 20.00
        
    def get_description(self) -> str:
        return super().get_description() + ", with Milk"

class Chocolate(CoffeeDecorator):
    def get_cost(self) -> float:
        return super().get_cost() + 30.00
        
    def get_description(self) -> str:
        return super().get_description() + ", with Chocolate Syrup"

# ==========================================
# Step 5: Client Code (Putting it all together)
# ==========================================
if __name__ == "__main__":
    # Order 1: Just a basic coffee
    my_coffee = BasicCoffee()
    print(f"{my_coffee.get_description()} -> Price: ${my_coffee.get_cost()}")

    # Order 2: Basic Coffee + Milk
    milk_coffee = Milk(BasicCoffee())
    print(f"{milk_coffee.get_description()} -> Price: ${milk_coffee.get_cost()}")

    # Order 3: Basic Coffee + Milk + Chocolate (Chaining decorators)
    premium_coffee = Chocolate(Milk(BasicCoffee()))
    print(f"{premium_coffee.get_description()} -> Price: ${premium_coffee.get_cost()}")

Decorator এর দুর্বলতা (Drawbacks) এবং সতর্কতা

১. Code Complexity (জটিলতা): অনেক বেশি ডেকোরেটর ব্যবহার করলে প্রচুর ছোট ছোট ক্লাস তৈরি হয়, যা নতুন ডেভেলপারদের জন্য বোঝা কঠিন হতে পারে।

২. Debugging Difficulty (ডিবাগিংয়ে সমস্যা): একটি অবজেক্ট যখন ৫-৬টি ডেকোরেটর দিয়ে র‍্যাপ (Wrap) করা থাকে, তখন কোনো বাগ বা এরর দেখা দিলে স্ট্যাক ট্রেস (Stack Trace) পড়ে মূল সমস্যাটি কোথায় তা খুঁজে বের করা বেশ কষ্টকর হয়ে যায়।

৩. Order Dependency: অনেক সময় ডেকোরেটরগুলো কোন সিরিয়ালে যুক্ত করা হচ্ছে, তার ওপর নির্ভর করে আউটপুট পরিবর্তন হয়ে যেতে পারে, যা সিস্টেমে অপ্রত্যাশিত বাগ তৈরি করে।

আধুনিক ফ্রেমওয়ার্কে (Django) Decorator-এর ব্যবহার

আধুনিক ওয়েব ফ্রেমওয়ার্কগুলোতে আমরা যে Middleware ব্যবহার করি, তা মূলত এই Decorator Pattern-এরই একটি চমৎকার আর্কিটেকচারাল রূপ।

Django-তে Middleware (Python): জ্যাঙ্গোতেও একইভাবে রিকোয়েস্ট এবং রেসপন্স হ্যান্ডেল করার জন্য মিডলওয়্যার চেইনিং ব্যবহৃত হয়। জ্যাঙ্গোতে মিডলওয়্যার একটি ক্লাস বা ফাংশন হিসেবে কাজ করে যা একটি get_response অবজেক্টকে র‍্যাপ করে রাখে।

# A simple representation of Django Middleware as a Decorator
class SimpleLoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Decorating before the view is called
        print("Request is incoming...") 
        
        response = self.get_response(request)
        
        # Decorating after the view generated the response
        print("Response is outgoing...")
        return response

পাইথন ল্যাঙ্গুয়েজে বিল্ট-ইন ফাংশনাল ডেকোরেটর (@decorator_name) রয়েছে। এটি ক্লাসিক ওওপি (OOP) ডেকোরেটর প্যাটার্নের একটি সিনট্যাকটিক সুগার (Syntactic Sugar)। তুমি যখন পাইথনে কোড লেখো, তখন ফাংশনের উপরে @ দিয়ে যে ডেকোরেটরগুলো কল করো, সেগুলোও মূলত এই প্যাটার্নের কনসেপ্ট ব্যবহার করেই মূল ফাংশনটিকে না বদলে তার ভেতরে নতুন লজিক পুশ করে।

কোডের ক্লাস মডিফাই না করে যদি ডাইনামিকভাবে নতুন ফিচার বা বিহেভিয়ার যুক্ত করার প্রয়োজন হয়, তবে Decorator Pattern হলো তোমার সেরা হাতিয়ার!