পাঠ ২২ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Software Engineering Principles & Git / স্ট্রাকচারাল প্যাটার্ন

স্ট্রাকচারাল প্যাটার্ন — Adapter, Decorator, Facade

Structural patterns — Adapter, Decorator & Facade
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • Adapter পিছনে কাজ করা মূল সমস্যা — ইনকম্প্যাটিবল ইন্টারফেস — ও এর সমাধান
  • Decorator কীভাবে "সাবক্লাস এক্সপ্লোশন" এড়িয়ে flexible-ভাবে আচরণ combine করে
  • Facade কীভাবে একটি জটিল সাবসিস্টেমকে অন্য ডেভেলপারদের জন্য ব্যবহারযোগ্য করে তোলে
  • তিনটি প্যাটার্নই বাস্তব, রানযোগ্য Python ক্লাস দিয়ে — কোনো সিমুলেশন নয়

১ · Adapter প্যাটার্ন — ইনকম্প্যাটিবল ইন্টারফেস মেলানো

Adapter প্যাটার্নAdapter Patternএকটি ক্লাসের বিদ্যমান ইন্টারফেসকে client কোড যে ইন্টারফেস আশা করে সেই ইন্টারফেসে রূপান্তর করে — মূল ক্লাসের সোর্স কোড পরিবর্তন না করেই। কাজে লাগে যখন একটি পুরনো বা থার্ড-পার্টি ক্লাসের ইন্টারফেস আপনার কোড যা আশা করে তার সাথে মেলে না — সরাসরি সেই ক্লাসের কোড এডিট করা প্রায়ই সম্ভব হয় না (থার্ড-পার্টি লাইব্রেরি) বা উচিতও না (পুরনো, ভালোভাবে টেস্ট করা কোড অযথা ঘাঁটানো)। Adapter মাঝখানে দাঁড়িয়ে দুটোর মধ্যে অনুবাদক হিসেবে কাজ করে — সরাসরি M4/L16-এর OCPOpen/Closed Principleএক্সটেনশনের জন্য open, মডিফিকেশনের জন্য closed — Adapter ঠিক এই নীতিই মেনে চলে: বিদ্যমান ক্লাসকে না বদলে নতুন compatibility যোগ করে। মেনে চলা একটি উদাহরণ: বিদ্যমান কোড না বদলিয়ে নতুন কমপ্যাটিবিলিটি যোগ করা।

Python
# Adapter pattern -- ধারণা: LegacyPrinter-এর ইন্টারফেস client কোড যা আশা করে তার
# সাথে মেলে না -- PrinterAdapter মাঝখানে দাঁড়িয়ে ইন্টারফেস "অনুবাদ" করে দেয়,
# LegacyPrinter-এর একটি লাইন কোডও না বদলিয়ে।

class LegacyPrinter:
    """একটি পুরনো/থার্ড-পার্টি ক্লাস -- ইন্টারফেস client-এর প্রত্যাশার সাথে মেলে না।"""
    def print_text(self, s):
        return f"[LegacyPrinter] {s}"

class Printer:
    """Client কোড এই ইন্টারফেস আশা করে -- শুধু একটি .print(document) মেথড।"""
    def print(self, document):
        raise NotImplementedError

class PrinterAdapter(Printer):
    """LegacyPrinter-কে client-এর প্রত্যাশিত .print() ইন্টারফেসে অনুবাদ করে।"""
    def __init__(self, legacy_printer):
        self.legacy_printer = legacy_printer

    def print(self, document):
        return self.legacy_printer.print_text(document)

def client_code(printer: Printer, document: str):
    # client_code শুধু Printer ইন্টারফেস চেনে -- LegacyPrinter সম্পর্কে কিছুই জানে না।
    return printer.print(document)

legacy = LegacyPrinter()
adapter = PrinterAdapter(legacy)
print(client_code(adapter, "রিপোর্ট.pdf"))

    

২ · Decorator প্যাটার্ন — রানটাইমে আচরণ স্ট্যাক করা

Decorator প্যাটার্নDecorator Patternএকটি individual অবজেক্টকে রানটাইমে wrap করে নতুন আচরণ যোগ করে, একই ইন্টারফেস বজায় রেখে — সাবক্লাসিং থেকে সম্পূর্ণ আলাদা মেকানিজম। সাধারণ ইনহেরিটেন্স থেকে ভিন্ন — ইনহেরিটেন্সে প্রতিটি নতুন COMBINATION-এর জন্য আলাদা সাবক্লাস লাগে (যেমন CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar — একে "সাবক্লাস এক্সপ্লোশন" বলা হয়, যত বেশি অপশনাল ফিচার তত বেশি সাবক্লাস দরকার)। Decorator এই সমস্যা এড়ায় — প্রতিটি ডেকোরেটর একই ইন্টারফেস মেনে wrapped অবজেক্টকে ধরে রাখে, এবং wrapped অবজেক্টের ফলাফলের উপর নিজের অংশটুকু যোগ করে দেয়। একাধিক ডেকোরেটর একে অপরের উপর স্ট্যাক করা যায় — যেকোনো COMBINATION, নতুন কোনো ক্লাস ছাড়াই।

Coffee — ৫০ টাকা + MilkDecorator — ৫০+১৫ = ৬৫ টাকা + SugarDecorator — ৬৫+৫ = ৭০ টাকা
প্রতিটি Decorator আগের লেয়ারের .cost() কল করে তার নিজের অংশ যোগ করে — চূড়ান্ত ফলাফল হলো সব লেয়ারের সঠিক যোগফল।
Python
# Decorator pattern -- প্রতিটি ডেকোরেটর একটি coffee-সদৃশ অবজেক্ট wrap করে,
# একই ইন্টারফেস রাখে (.cost(), .description()), এবং wrapped অবজেক্টের ফলাফলের
# উপর নিজের অংশ যোগ করে -- একাধিক ডেকোরেটর একে অপরের উপর স্ট্যাক করা যায়।

class Coffee:
    def cost(self):
        return 50

    def description(self):
        return "কফি"

class CoffeeDecorator(Coffee):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def cost(self):
        return self.wrapped.cost()

    def description(self):
        return self.wrapped.description()

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return self.wrapped.cost() + 15

    def description(self):
        return self.wrapped.description() + " + দুধ"

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return self.wrapped.cost() + 5

    def description(self):
        return self.wrapped.description() + " + চিনি"

base = Coffee()
with_milk = MilkDecorator(base)
with_milk_sugar = SugarDecorator(with_milk)

print(f"{base.description()}: {base.cost()} টাকা")
print(f"{with_milk.description()}: {with_milk.cost()} টাকা")
print(f"{with_milk_sugar.description()}: {with_milk_sugar.cost()} টাকা")

    
হাতে হিসাব করলে: বেস কফি ৫০ টাকা। MilkDecorator যোগ হলে ৫০ + ১৫ = ৬৫ টাকা। তার উপর SugarDecorator যোগ হলে ৬৫ + ৫ = ৭০ টাকা — অর্থাৎ ৫০ + ১৫ + ৫ = ৭০, প্রতিটি লেয়ারের খরচ ঠিকভাবে যোগ হয়েছে। কোনো ডেকোরেটর তার নিজের স্তরের উপরে কিছু জানে না — শুধু তার ঠিক নিচেরটাকে wrap করে।

৩ · Facade প্যাটার্ন — জটিল সাবসিস্টেম সরলীকরণ

Facade প্যাটার্নFacade Patternএকটি জটিল সাবসিস্টেমের একাধিক ক্লাসের সামনে একটি সরল, একক ইন্টারফেস বসায় — client কোডকে সাবসিস্টেমের ভেতরের জটিলতা coordinate করতে হয় না। তিনটির মধ্যে সবচেয়ে সরল — client শুধু Facade-এর সাথে কথা বলে, ভেতরে কতগুলো ক্লাস আছে বা তারা কোন ক্রমে কল হয় তা জানার দরকার নেই। এটি সরাসরি L03-এর usability অ্যাট্রিবিউটUsabilityসিস্টেম কতটা সহজে ব্যবহার করা যায় — এখানে end user নয়, বরং অন্য ডেভেলপারদের জন্য কোডবেসের internal API-এর usability। -এর একটি প্রয়োগ, তবে এখানে "ব্যবহারকারী" হলো অন্য ডেভেলপার, আপনার কোডবেসের একজন end user নয়।

Python
# Facade pattern -- ComputerFacade জটিল সাবসিস্টেমের (CPU, Memory, Disk) মেথডগুলো
# সঠিক ক্রমে নিজে কল করে -- client শুধু .start() কল করে, ভেতরের জটিলতা জানতেও হয় না।

class CPU:
    def freeze(self):
        return "CPU: ফ্রিজ করা হলো"

    def jump(self, position):
        return f"CPU: অবস্থান {position}-এ জাম্প করা হলো"

    def execute(self):
        return "CPU: এক্সিকিউশন শুরু হলো"

class Memory:
    def load(self, position, data):
        return f"Memory: অবস্থান {position}-এ '{data}' লোড করা হলো"

class Disk:
    def read(self, sector, size):
        return f"Disk: সেক্টর {sector} থেকে {size} বাইট পড়া হলো"

class ComputerFacade:
    """একটি সরল .start() ইন্টারফেসের পেছনে CPU, Memory, Disk-কে সমন্বয় করে।"""
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.disk = Disk()

    def start(self):
        steps = [self.cpu.freeze()]
        boot_data = self.disk.read(sector=0, size=512)
        steps.append(boot_data)
        steps.append(self.memory.load(position=0, data="বুট-লোডার"))
        steps.append(self.cpu.jump(position=0))
        steps.append(self.cpu.execute())
        return steps

computer = ComputerFacade()
for step in computer.start():
    print(step)

    
মূল কথা · Key takeaway

তিনটি প্যাটার্নই "COMPOSITION" নিয়ে — কিন্তু ভিন্ন উদ্দেশ্যে: Adapter ইন্টারফেস মেলায়, Decorator আচরণ যোগ করে, Facade জটিলতা লুকায়। বাস্তব কোডবেসে এই তিনটিই প্রায়ই একসাথে দেখা যায় — যেমন একটি Facade নিজেই ভেতরে একটি Adapter ব্যবহার করতে পারে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ Adapter ও Decorator দুটোই একটি অবজেক্টকে "wrap" করে — তাহলে এই দুটো প্যাটার্নের মূল পার্থক্য কোথায়?

উদ্দেশ্যে। Adapter wrap করে কারণ ইন্টারফেস মেলে না — এটি শুধু একটি ইন্টারফেসকে অন্য ইন্টারফেসে "অনুবাদ" করে, নতুন কোনো কার্যকারিতা যোগ করে না। Decorator wrap করে ইচ্ছাকৃতভাবে নতুন আচরণ/কার্যকারিতা যোগ করতে — wrapped অবজেক্টের ইন্টারফেস আগে থেকেই সঠিক, Decorator শুধু তার উপর কিছু যোগ করে। একটির লক্ষ্য কমপ্যাটিবিলিটি, অন্যটির লক্ষ্য এক্সটেনশন।

প্র ০২ কফি উদাহরণে যদি MilkDecorator ও SugarDecorator-এর প্রয়োগের ক্রম উল্টে দেওয়া হয় (আগে Sugar, পরে Milk), চূড়ান্ত খরচ কি বদলাবে?

না, চূড়ান্ত খরচ একই থাকবে — ৫০ + ১৫ + ৫ = ৭০ টাকা, ক্রম যাই হোক না কেন, কারণ প্রতিটি ডেকোরেটর শুধু একটি নির্দিষ্ট সংখ্যা (+১৫ বা +৫) যোগ করছে, আর যোগফল commutative (ক্রম-নিরপেক্ষ)। তবে description()-এর টেক্সট আউটপুটের ক্রম বদলে যাবে ("+ চিনি + দুধ" বনাম "+ দুধ + চিনি") — সব ক্ষেত্রে ফলাফল ক্রম-নিরপেক্ষ থাকবে না, সেটা নির্ভর করে মেথডটি ঠিক কী কম্বাইন করছে।

প্র ০৩ ComputerFacade ছাড়া client কোড সরাসরি CPU, Memory, Disk ব্যবহার করলে কী কী সমস্যা হতে পারে?

client কোডকে জানতে হতো ঠিক কোন ক্রমে মেথডগুলো কল করতে হয় (freeze → read → load → jump → execute) — ক্রম ভুল হলে সিস্টেম সঠিকভাবে বুট নাও হতে পারে। এছাড়া প্রতিটি জায়গায় যেখানে কম্পিউটার স্টার্ট করা দরকার, সেই একই ৫-লাইনের সিকোয়েন্স বারবার ডুপ্লিকেট হতো (সরাসরি L15-এর DRY লঙ্ঘন) — Facade এই লজিকটিকে একবারই লিখে সবার জন্য পুনঃব্যবহারযোগ্য করে দেয়।

অনুশীলন

  1. চিন্তা করুন: আপনার কোম্পানি সম্প্রতি একটি নতুন থার্ড-পার্টি পেমেন্ট গেটওয়ে লাইব্রেরি যুক্ত করেছে, কিন্তু তার মেথডের নাম ও প্যারামিটার আপনার বিদ্যমান PaymentProcessor ইন্টারফেসের সাথে মেলে না। Adapter প্যাটার্ন কীভাবে এই সমস্যা সমাধান করবে তা লিখুন।

    একটি PaymentGatewayAdapter ক্লাস বানাতে হবে যা PaymentProcessor ইন্টারফেস ইমপ্লিমেন্ট করে (client কোড যা আশা করে), কিন্তু ভেতরে নতুন থার্ড-পার্টি লাইব্রেরির actual মেথড কল করে — প্যারামিটার ও রিটার্ন ভ্যালু সঠিকভাবে রূপান্তর করে। এতে বিদ্যমান PaymentProcessor-নির্ভর কোডের একটি লাইনও বদলাতে হয় না, শুধু নতুন Adapter ক্লাসটি instantiate করে ইনজেক্ট করলেই চলবে।

  2. পরীক্ষা করুন: Decorator কোড সেলে একটি নতুন WhippedCreamDecorator ক্লাস (খরচ +২০) যোগ করুন এবং তিনটি ডেকোরেটর (Milk, Sugar, WhippedCream) স্ট্যাক করে চূড়ান্ত খরচ Run করে যাচাই করুন।

    class WhippedCreamDecorator(CoffeeDecorator): def cost(self): return self.wrapped.cost() + 20 — এই ক্লাসটি অন্য দুটোর মতোই CoffeeDecorator থেকে ইনহেরিট করে। তিনটি স্ট্যাক করলে চূড়ান্ত খরচ হবে ৫০ + ১৫ + ৫ + ২০ = ৯০ টাকা — কোনো নতুন সাবক্লাস কম্বিনেশন (যেমন "CoffeeWithMilkSugarCream") তৈরি করার দরকার হয়নি, শুধু আরেকটি লেয়ার wrap করা হয়েছে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
ক্রিয়েশনাল প্যাটার্ন — Builder ও Prototype