Adapter Design Pattern

ধরো, তুমি বাজার থেকে একটি নতুন ফ্যান কিনেছ, যার প্লাগটি তিন-পিনের (Three-pin)। কিন্তু তোমার বাসার দেয়ালের সকেটটি হলো দুই-পিনের (Two-pin)। এখন তুমি কী করবে? সরাসরি তো আর ফ্যানের প্লাগ সকেটে ঢোকাতে পারবে না। এই সমস্যার সবচেয়ে সহজ সমাধান হলো একটি 'অ্যাডাপ্টার' (Adapter) কিনে আনা, যা থ্রি-পিনের প্লাগকে টু-পিনের সকেটে সুন্দরভাবে ফিট করাবে।

সফটওয়্যার ইঞ্জিনিয়ারিংয়েও ঠিক একই রকম ঘটনা ঘটে। যখন দুটি ভিন্ন ইন্টারফেস বা সিস্টেম একে অপরের সাথে সরাসরি কথা বলতে পারে না, তখন তাদের মাঝে যোগাযোগ স্থাপন করার জন্য আমরা যে ডিজাইন প্যাটার্নটি ব্যবহার করি, তাকেই বলা হয় Adapter Design Pattern।

Adapter Design Pattern কী?

Adapter Pattern হলো একটি Structural Design Pattern। এটি এক ধরনের মধ্যস্থতাকারী (Bridge) বা র‍্যাপার (Wrapper) হিসেবে কাজ করে, যা দুটি অসামঞ্জস্যপূর্ণ বা ইনকমপ্যাটিবল (Incompatible) ইন্টারফেসকে একসাথে কাজ করার সুযোগ করে দেয়।

সহজ কথায়, এটি ক্লায়েন্টের রিকোয়েস্টকে এমন একটি ফরমেটে রূপান্তর করে, যা পেছনের লিগ্যাসি (Legacy) কোড বা থার্ড-পার্টি লাইব্রেরি বুঝতে পারে।

Adapter Pattern-এর প্রকারভেদ (Deep Dive)

সফটওয়্যার আর্কিটেকচারে Adapter মূলত দুটি উপায়ে ইমপ্লিমেন্ট করা যায়:

১. Object Adapter: এটি কম্পোজিশন (Composition) ব্যবহার করে কাজ করে। অর্থাৎ, অ্যাডাপ্টার ক্লাসটি পুরোনো ক্লাসের একটি অবজেক্টকে নিজের ভেতরে ধারণ করে (Wrap করে) এবং মেথড কলগুলো তার কাছে ডেলিগেট বা পাস করে দেয়। এটি সবচেয়ে বেশি ব্যবহৃত হয়।

২. Class Adapter: এটি মাল্টিপল ইনহেরিটেন্স (Multiple Inheritance) ব্যবহার করে কাজ করে। অ্যাডাপ্টার ক্লাসটি একই সাথে ক্লায়েন্টের ইন্টারফেস এবং পুরোনো ক্লাস; উভয়কেই ইনহেরিট করে। তবে সব প্রোগ্রামিং ল্যাঙ্গুয়েজে মাল্টিপল ইনহেরিটেন্স সাপোর্ট করে না (যেমন: জাভা বা পিএইচপি), তাই এটি খুব একটা ব্যবহৃত হয় না।

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

  • Legacy Code Integration: যখন তোমার পুরোনো কোনো সিস্টেমের কোড মডিফাই না করেই নতুন সিস্টেমের সাথে সামঞ্জস্যপূর্ণ করতে হয়।

  • Third-Party Libraries: তুমি যখন বাইরের কোনো লাইব্রেরি বা API ব্যবহার করো, যার ডেটা ফরমেট তোমার প্রজেক্টের ইন্টারফেসের সাথে মেলে না, তখন একটি অ্যাডাপ্টার লিখে নেওয়া সবচেয়ে নিরাপদ।

  • Open/Closed Principle (OCP): পুরোনো কোডে হাত না দিয়ে অ্যাডাপ্টারের মাধ্যমে নতুন ফিচার যুক্ত করা যায়, যা OCP-কে শতভাগ সমর্থন করে।

Python Code Example: Payment System Adapter

ধরা যাক, আমরা একটি পুরোনো OldPaymentGateway ব্যবহার করছি, যেখানে পেমেন্ট করার মেথডের নাম pay()। কিন্তু আমাদের নতুন সিস্টেমে নিয়ম করা হয়েছে যে সব পেমেন্ট গেটওয়েকে NewPaymentGateway ইন্টারফেস মেনে চলতে হবে, যার মেথডের নাম make_payment()। এই দুইয়ের মধ্যে সংযোগ ঘটাবে আমাদের PaymentAdapter।

from abc import ABC, abstractmethod

# ==========================================
# Legacy System (Old System with incompatible interface)
# ==========================================
class OldPaymentGateway:
    def pay(self, amount: float):
        print(f"Processing payment of ${amount} via Old Payment Gateway.")

# ==========================================
# Target Interface (The interface client expects)
# ==========================================
class NewPaymentGateway(ABC):
    @abstractmethod
    def make_payment(self, amount: float):
        pass

# ==========================================
# The Adapter Class (Wrapper using Composition)
# ==========================================
class PaymentAdapter(NewPaymentGateway):
    def __init__(self, old_gateway: OldPaymentGateway):
        # Holding a reference to the wrapped object
        self.old_gateway = old_gateway

    def make_payment(self, amount: float):
        # Translating the new method call to the old method format
        print("Adapter translates the request...")
        self.old_gateway.pay(amount)

# ==========================================
# Client Code
# ==========================================
if __name__ == "__main__":
    # We have an existing old gateway
    legacy_gateway = OldPaymentGateway()
    
    # The client strictly wants to use 'make_payment', not 'pay'
    # So we wrap the legacy gateway inside our Adapter
    adapter = PaymentAdapter(legacy_gateway)
    
    # Client executes the code flawlessly
    adapter.make_payment(500.00)

Adapter Pattern-এর সীমাবদ্ধতা (Drawbacks)

১. Overhead এবং Complexity: অসামঞ্জস্যতা দূর করার জন্য বারবার নতুন অ্যাডাপ্টার ক্লাস তৈরি করতে হয়। এর ফলে কোডবেসের সাইজ বাড়ে এবং অতিরিক্ত লেয়ার (Layer) তৈরি হওয়ায় কিছুটা ওভারহেড দেখা দিতে পারে।

২. অতিরিক্ত ডিপেন্ডেন্সি: পুরোনো এবং নতুন সিস্টেমের মাঝখানে অ্যাডাপ্টার বসে গিয়ে কোডে একটি নতুন ডিপেন্ডেন্সি যোগ করে, যা মাঝে মাঝে ডিবাগিংকে একটু জটিল করে তোলে।

৩. অনেক সময় লিগ্যাসি সিস্টেম যদি পুরোপুরি পরিবর্তন করে ফেলা সম্ভব হয়, তবে অ্যাডাপ্টার বানানোর চেয়ে কোড রিফ্যাক্টর (Refactor) করে নেওয়াই বেশি ভালো সিদ্ধান্ত।

Django তে Adapter Pattern এর ব্যবহার

আধুনিক ওয়েব ফ্রেমওয়ার্কগুলোতে প্লাগ-অ্যান্ড-প্লে (Plug-and-play) সুবিধা দেওয়ার জন্য Adapter Pattern ব্যাপকভাবে ব্যবহৃত হয়।

Django (Python Framework):

  • ORM Database Backends: জ্যাঙ্গো ORM (Object-Relational Mapping)-এ তুমি যখন পাইথনের কোডে কোয়ারি লেখো, জ্যাঙ্গোর ডাটাবেস ব্যাকএন্ড অ্যাডাপ্টারগুলো (যেমন: django.db.backends.postgresql বা mysql) সেই পাইথন কোডকে ডাটাবেস-স্পেসিফিক SQL-এ কনভার্ট করে।

  • Authentication Backends: জ্যাঙ্গোতে মাল্টিপল অথেনটিকেশন মেকানিজম (LDAP, OAuth, ModelBackend) একসাথে কাজ করাতে অ্যাডাপ্টার প্যাটার্নের কনসেপ্ট ব্যবহৃত হয়।

Adapter Design Pattern হলো একটি ব্রিজ বা দোভাষী, যা দুই ভিন্ন ভাষার সিস্টেমকে একসাথে কাজ করতে সাহায্য করে। এটি তোমার কোডকে রিইউজেবল এবং ফ্লেক্সিবল করে তোলে, তবে খেয়াল রাখতে হবে যেন অযথাই অতিরিক্ত অ্যাডাপ্টার লেয়ার তৈরি করে সিস্টেমকে ওভার-ইঞ্জিনিয়ারিংয়ের দিকে ঠেলে দেওয়া না হয়।