সরাসরি পর্বে যান
পর্ব ৩৪ · ৭০-এর মধ্যে · মডিউল ৬

Structural pattern — adapter, decorator, facade, proxy আর composite

Structural Design Patterns — Adapter, Decorator, Facade, Proxy & Composite (plus Bridge and Flyweight)

Taught in Bangla, this lesson covers structural design patterns: object versus class adapters for wrapping a third-party payment SDK, decorators and why wrapping order matters, how Python's @decorator differs from GoF Decorator, Facade for complex subsystems, a caching proxy with JavaScript's Proxy, Composite for computing totals over a tree, brief notes on Bridge and Flyweight, and a side-by-side comparison of Adapter, Facade, Proxy and Decorator.

এই পর্বে যা শিখবেন

  • প্রতিটি structural pattern-এর উদ্দেশ্য, সমস্যা, কাঠামো, কোড, ফলাফল আর সাধারণ ফাঁদ
  • Object adapter বনাম class adapter, আর একটি তৃতীয় পক্ষের payment SDK মানিয়ে নেওয়ার নিরাপদ উপায়
  • Decorator-এর ক্রম কেন গুরুত্বপূর্ণ, আর Python-এর @decorator GoF-এর Decorator থেকে কোথায় আলাদা
  • JavaScript-এর ভাষাগত Proxy দিয়ে caching proxy, আর Composite দিয়ে গাছের মোট হিসাব
  • Adapter, Facade, Proxy আর Decorator — চারটিকে কাঠামো আর উদ্দেশ্য দেখে আলাদা করা

১ · Structural pattern কী — জিনিস জোড়া লাগানোর নকশা

পর্ব ৩৩-এ (Creational pattern) দেখেছেন object কীভাবে তৈরি হয়। এবার প্রশ্ন: তৈরি হওয়া object-গুলো কীভাবে জোড়া লাগে? একটি বাড়ির কথা ভাবুন। ইট, রড, পাইপ, বৈদ্যুতিক তার — সব আলাদাভাবে ভালো মানের হলেও জোড়ার জায়গাগুলো ভুল হলে বাড়ি টেকে না। প্লাম্বার জানেন কোন মাপের পাইপের সাথে কোন মাপের জোড়া (fitting) লাগে; ইলেকট্রিশিয়ান জানেন এক বোর্ড থেকে কয়টি লাইন টানা নিরাপদ। Structural pattern হলো সফটওয়্যারের এই জোড়ার নকশা।

১৯৯৪ সালের Gang of Four (GoF) বই Design Patterns-এ ২৩টি design pattern তিন শ্রেণিতে ভাগ করা: ৫টি creational, ৭টি structural আর ১১টি behavioral. Structural সাতটি হলো Adapter, Bridge, Composite, Decorator, Facade, Flyweight আর Proxy. Pattern-এর সাধারণ ভাষা — নাম, উদ্দেশ্য, প্রয়োগক্ষেত্র, ফলাফল — পর্ব ৩২-এ (Design pattern কী) এসেছে; এখানে সরাসরি সাতটির ভেতরে ঢুকব।

মূল ধারণা

প্রায় প্রতিটি structural pattern একই দুটি উপাদান দিয়ে গড়া: একটি interface (চুক্তি) আর object composition — একটি object আরেকটিকে ভেতরে রেখে তার কাছে কাজ পাঠায়, যাকে বলে delegation. পর্ব ২৬-এর “composition over inheritance” আর পর্ব ২৮-এর dependency inversion এখানে হাতে-কলমে কাজে লাগে। পার্থক্যগুলো আসে উদ্দেশ্যে: মোড়কটি interface বদলাচ্ছে, না একই রেখে কিছু যোগ করছে, না প্রবেশ নিয়ন্ত্রণ করছে, না গাছ বানাচ্ছে।

GoF আরও একটি ভাগ করে: class স্তরের pattern inheritance দিয়ে compile-এর সময়েই সম্পর্ক বাঁধে, আর object স্তরের pattern composition দিয়ে চলার সময় সম্পর্ক গড়ে। সাতটির মধ্যে শুধু Adapter-এর দুই রূপই আছে — class adapter আর object adapter; বাকি ছয়টি object স্তরের। এজন্যই structural pattern বোঝার আগে পর্ব ২৯-এর UML class diagram-এর তিনটি তীর চোখে পরিষ্কার থাকা দরকার: ফাঁপা ত্রিভুজসহ ড্যাশ রেখা (realization, interface মানা), ফাঁপা ত্রিভুজসহ টানা রেখা (generalization, inheritance) আর খোলা তীরের association (ধরে রাখা)।

বিদেশ থেকে আনা একটি ল্যাপটপ চার্জারের প্লাগ চ্যাপ্টা দুই-পিনের, আর বাসার সকেট গোল তিন-পিনের। আপনি চার্জার কেটে নতুন প্লাগ লাগান না, সকেটও ভাঙেন না — মাঝখানে একটি কনভার্টার প্লাগ বসান। এটিই Adapter. এবার সেই সকেটে একটি সার্জ প্রটেক্টর বসালেন: সকেটের আকার একই, কিন্তু বাড়তি সুরক্ষা যোগ হলো — Decorator. বাসার মেইন সুইচ বোর্ড একটি সুইচে পুরো ঘরের অনেক লাইন চালু-বন্ধ করে — Facade. আর মিটার রিডারকে ঢুকতে দেওয়ার আগে যে দারোয়ান পরিচয় দেখেন — Proxy. পুরো পর্বে এই চারটি ছবি মনে রাখুন।

২ · Adapter — অমিল interface মিলিয়ে দেওয়া

উদ্দেশ্য (GoF-এর ভাষায়, সংক্ষেপে): একটি class-এর interface-কে client-এর প্রত্যাশিত আরেকটি interface-এ রূপান্তর করা, যাতে অমিল interface-এর কারণে যারা একসাথে কাজ করতে পারত না, তারা পারে। Adapter-এর আরেক নাম wrapper.

সমস্যা — দুটি payment gateway, দুই রকম ভাষা

একটি কাল্পনিক অনলাইন বাজার অ্যাপের checkout কোড চায় খুব সহজ একটি চুক্তি: pay(amount) ডাকুন, ফেরত পান { ok, id }. কিন্তু দুটি কাল্পনিক gateway-এর SDK এই ভাষায় কথা বলে না:

  • TakaPay: টাকা নয়, পয়সায় পরিমাণ চায়, একটি reference-সহ object-এ: makePayment({ paisa, ref }). ফেরত দেয় { status: "OK", txn }.
  • DeshCard: দুই ধাপে কাজ করে — আগে authorize("349.50") (পরিমাণ string-এ), তারপর capture(authId). সফল হলে code হয় 0.

SDK-র কোড আপনার নয়, বদলাতে পারবেন না; আর checkout-এর ভেতরে if (gateway === "takapay") … else … লিখলে পর্ব ২৭-এর open-closed principle ভাঙবে — প্রতিটি নতুন gateway-তে checkout বদলাতে হবে। এখানে যিনি মানিয়ে নেওয়া হচ্ছেন, সেই SDK হলো adaptee; আপনার চুক্তি pay(amount) হলো target.

কাঠামো আর কোড

প্রতিটি SDK-র জন্য একটি ছোট adapter class, যা target চুক্তি মানে আর ভেতরে SDK-র একটি object ধরে রাখে। Checkout শুধু target-কে চেনে:

// Two fictional payment SDKs with different APIs (we cannot change them)
class TakaPaySdk {                       // wants paisa and a reference
  makePayment(req) { return { status: "OK", txn: "TP-" + (1000 + req.paisa / 100) }; }
}
class DeshCardSdk {                      // two steps: authorize, then capture
  authorize(amountStr) { return "AUTH-" + amountStr.replace(".", ""); }
  capture(authId) { return { code: 0, ref: authId.replace("AUTH", "DC") }; }
}

// Our interface: pay(amount) -> { ok, id }
class TakaPayAdapter {
  constructor(sdk) { this.sdk = sdk; }   // object adapter: holds the adaptee
  pay(amount) {
    const r = this.sdk.makePayment({ paisa: Math.round(amount * 100), ref: "ORD" });
    return { ok: r.status === "OK", id: r.txn };
  }
}
class DeshCardAdapter {
  constructor(sdk) { this.sdk = sdk; }
  pay(amount) {
    const auth = this.sdk.authorize(amount.toFixed(2));
    const r = this.sdk.capture(auth);
    return { ok: r.code === 0, id: r.ref };
  }
}

// Checkout knows only pay(amount)
function checkout(gateway, amount) {
  const res = gateway.pay(amount);
  console.log(gateway.constructor.name.padEnd(16), amount, "->", JSON.stringify(res));
}
checkout(new TakaPayAdapter(new TakaPaySdk()), 500);
checkout(new DeshCardAdapter(new DeshCardSdk()), 349.5);

আউটপুট:

TakaPayAdapter   500 -> {"ok":true,"id":"TP-1500"}
DeshCardAdapter  349.5 -> {"ok":true,"id":"DC-34950"}

checkout function দুই ক্ষেত্রেই হুবহু একই লাইন চালাল — gateway.pay(amount). TakaPay adapter ৫০০ টাকাকে ৫০০০০ পয়সায় বদলে পাঠিয়েছে; DeshCard adapter ৩৪৯.৫-কে "349.50" string বানিয়ে দুই ধাপের ডাক সেরেছে। দুই SDK-র দুই রকম উত্তর (status বনাম code, txn বনাম ref) একই { ok, id } আকারে ফিরেছে। তৃতীয় একটি gateway এলে শুধু তৃতীয় একটি adapter লাগবে; checkout ছোঁয়ার দরকার নেই।

নিচের ডায়াগ্রামে একটি ডাক adapter-এর ভেতর দিয়ে কীভাবে অনুবাদ হয়ে যায় আর ফিরে আসে, ধাপে ধাপে দেখুন:

  1. Checkoutpay(500)শুধু target চেনে
  2. TakaPayAdapterটাকা → পয়সাঅনুবাদক
  3. TakaPaySdkmakePayment()adaptee, বদলানো যায় না
checkoutclient-এর দিক
const res = gateway.pay(500);
// gateway কোন SDK, checkout জানে না
TakaPayAdapter.payভেতরে যাওয়ার পথে অনুবাদ
amount = 500          // টাকা
paisa  = Math.round(500 * 100) = 50000
this.sdk.makePayment({ paisa: 50000, ref: "ORD" })
TakaPaySdkSDK নিজের ভাষায় উত্তর দিল
return { status: "OK", txn: "TP-1500" }
TakaPayAdapter.payফেরার পথে অনুবাদ
ok = (r.status === "OK")  // true
id = r.txn               // "TP-1500"
return { ok: true, id: "TP-1500" }
checkoutএকই চুক্তি, অন্য SDK
res = { ok: true, id: "TP-1500" }
// DeshCard হলে: pay(349.5) → authorize("349.50")
//   → "AUTH-34950" → capture() → { code: 0, ref: "DC-34950" }
→ { ok: true, id: "DC-34950" }
  1. Client ডাকে target-এর ভাষায়। Checkout pay(500) ডাকল। সে শুধু জানে চুক্তিটি: একটি টাকার পরিমাণ দিলে { ok, id } ফিরবে। কোন gateway, কোন একক — এসব তার মাথাব্যথা নয়।
  2. Adapter ভেতরে যাওয়ার পথে অনুবাদ করে। TakaPay পয়সায় হিসাব চায়, তাই ৫০০ × ১০০ = ৫০০০০ পয়সা। Floating point-এর ভুল এড়াতে Math.round. এককের এই রূপান্তর adapter-এর সবচেয়ে গুরুত্বপূর্ণ আর সবচেয়ে ভুলপ্রবণ কাজ।
  3. Adaptee নিজের মতো কাজ করে। SDK জানেই না যে তাকে মানিয়ে নেওয়া হচ্ছে। সে নিজের ভাষায় উত্তর দিল: status: "OK" আর txn: "TP-1500".
  4. ফেরার পথে আবার অনুবাদ। Adapter SDK-র উত্তরকে target-এর আকারে বদলাল: status === "OK" থেকে ok: true, txn থেকে id. বাস্তবে SDK-র error code আর exception-কেও এখানে আপনার নিজের error-এ বদলাতে হয়।
  5. Client একই ফল পায়, SDK যা-ই হোক। DeshCard adapter দুই ধাপে (authorize, তারপর capture) কাজ করেও একই আকারের { ok: true, id: "DC-34950" } দেয়। Checkout-এর কোড একটি অক্ষরও বদলায়নি।

Object adapter বনাম class adapter

🧩 Object adapter (composition)

Adapter target মানে আর adaptee-র একটি object ধরে রাখে — উপরের কোডের মতো। Adaptee-র যেকোনো subclass, এমনকি test-এর জন্য একটি নকল SDK-ও constructor-এ দেওয়া যায়। প্রায় সব আধুনিক কোডে এটিই।

this.sdk = sdk

🧬 Class adapter (inheritance)

Adapter adaptee-কে inherit করে আর একই সাথে target মানে — Java-তে class TakaPayAdapter extends TakaPaySdk implements PaymentGateway, C++-এ multiple inheritance. ডাক এক ধাপ কম, adaptee-র কিছু আচরণ override করা যায়; কিন্তু একটি নির্দিষ্ট adaptee class-এ বাঁধা, আর adaptee-র সব public method adapter-এর গায়েও চলে আসে।

extends Adaptee implements Target

এই দুটিকে বলে object adapter আর class adapter. JavaScript আর Python-এ interface আলাদা ঘোষণা করা লাগে না বলে class adapter খুব কম দেখা যায়; Java, C# আর C++-এ বিশেষ ক্ষেত্রে আসে।

ফলাফল আর ফাঁদ

  • লাভ: বাইরের নির্ভরতা এক জায়গায় আটকে থাকে। SDK-র নতুন version এলে, বা gateway বদলালে, বদলায় শুধু adapter. Test-এ adapter-এর বদলে একটি fake gateway বসানো যায় (test double-এর বিস্তারিত পর্ব ৪৯-এ)।
  • ফাঁদ ১ — এককের ভুল: টাকা বনাম পয়সা, সেকেন্ড বনাম millisecond, UTC বনাম স্থানীয় সময়। ১৯৯৯ সালে Mars Climate Orbiter হারানোর প্রকাশিত কারণ ছিল দুই দলের সফটওয়্যারের মধ্যে unit of measure-এর অমিল (pound-force-second বনাম newton-second) — ঠিক সেই সীমানায়, যেখানে একটি adapter-এর কাজ ছিল রূপান্তর। এককের নাম variable-এ রাখুন (paisa, amountTaka) আর রূপান্তরের test লিখুন।
  • ফাঁদ ২ — ফুটো adapter: target-এ SDK-র type বা error (TakaPayError) ফাঁস হলে client আবার SDK-তে বাঁধা পড়ে। Adapter-এর বাইরে শুধু আপনার নিজের type যাবে।
  • ফাঁদ ৩ — adapter-এ ব্যবসার নিয়ম: “৫০০০ টাকার বেশি হলে OTP” — এটি domain-এর নিয়ম, adapter-এর নয়। Adapter শুধু অনুবাদ করবে।
  • ফাঁদ ৪ — অর্থ মেলে না: দুটি API-র ধারণা পুরোপুরি না মিললে (একটি refund সমর্থন করে, আরেকটি করে না) adapter চুপচাপ “সফল” ফেরত দেবে না; স্পষ্ট error দেবে। Interface-এর contract পর্ব ৩১-এ (Interface আর contract) দেখেছেন।

বড় পরিসরে এই চিন্তাটিই architecture হয়ে ওঠে: domain-এর চারপাশে port (interface) আর বাইরের জগতের সাথে adapter — পর্ব ৪০-এ hexagonal architecture-এ আসবে। Eric Evans-এর Domain-Driven Design (২০০৩) এর একটি রূপকে বলে anti-corruption layer.

৩ · Decorator — একই interface, মোড়কের ওপর মোড়ক

উদ্দেশ্য: একটি object-এর সাথে চলার সময় বাড়তি দায়িত্ব জুড়ে দেওয়া, subclass না বানিয়ে। Decorator মূল object-এর একই interface মানে, ভেতরে মূলটিকে রাখে, আর প্রতিটি ডাকের আগে বা পরে নিজের কাজটুকু করে ডাকটি ভেতরে পাঠায়।

পাড়ার চায়ের দোকানে “এক কাপ চা” মানে লিকার। কেউ চান দুধ-চা, কেউ আদা-চা, কেউ দুধ-চিনি-আদা সব। দোকানি প্রতিটি সমন্বয়ের জন্য আলাদা কেতলি রাখেন না — একই লিকারে একে একে দুধ, চিনি, আদা যোগ করেন। প্রতিটি যোগের পরও জিনিসটি “চা”-ই থাকে; দাম শুধু একটু করে বাড়ে। Decorator ঠিক এমন: প্রতিটি মোড়ক যোগের পরও object একই interface-এর।

সমস্যা — inheritance দিয়ে করলে class-এর বিস্ফোরণ

একটি কাল্পনিক মুদ্রা-বিনিময় হারের service থেকে data আনার RateFetcher আছে। কোথাও লাগবে log, কোথাও ব্যর্থ হলে আবার চেষ্টা (retry), কোথাও ফল জমিয়ে রাখা (cache). প্রতিটি সমন্বয়ের জন্য একটি subclass বানালে: তিনটি ঐচ্ছিক বৈশিষ্ট্যে ২³ = ৮টি সমন্বয় (কিছুই না থেকে তিনটিই পর্যন্ত), চারটিতে ১৬টি — subclass explosion. তার ওপর ক্রমও আলাদা অর্থ বহন করে (নিচে দেখবেন), তাই সত্যিকারের সম্ভাবনা আরও বেশি। Decorator-এ লাগে শুধু তিনটি ছোট class, আর সমন্বয় বানানো হয় চলার সময় — মোড়ক বসিয়ে।

কাঠামো

একটি interface (DataSource, operation fetch(code)); একটি আসল বাস্তবায়ন (RateFetcher); আর decorator-গুলো, যারা নিজেরাও DataSource আর ভেতরে আরেকটি DataSource ধরে রাখে। মূল কথা: decorator ভেতরের জিনিসকে interface দিয়ে চেনে, নির্দিষ্ট class দিয়ে নয় — তাই একটি decorator আরেকটি decorator-কে মুড়তে পারে। অংশ ৮-এর ছবিতে কাঠামোটি adapter আর proxy-র পাশাপাশি দেখবেন।

ক্রম বদলালে ফল বদলায় — Python-এ চালিয়ে দেখুন

নিচের কোডে তিনটি মোড়ক — logged, retry, cached — একটি কাল্পনিক হার-সার্ভারের function-এর চারপাশে দুই রকম ক্রমে বসানো হয়েছে। সার্ভারটি প্রথম ডাকে timeout দেয়, পরে ঠিকমতো উত্তর দেয়। প্রতিটি ক্রমে একই প্রশ্ন দুবার করা হয়েছে ("USD"):

import functools
def logged(f):
    @functools.wraps(f)
    def w(code):
        print(f"  log: -> {code}"); r = f(code); print(f"  log: <- {r}"); return r
    return w
def retry(f):
    @functools.wraps(f)
    def w(code):
        for attempt in (1, 2, 3):
            try: return f(code)
            except ConnectionError as e: print(f"  retry: attempt {attempt} failed ({e})")
        raise ConnectionError("gave up")
    return w
def cached(f):
    memo = {}
    @functools.wraps(f)
    def w(code):
        if code in memo: print("  cache: hit"); return memo[code]
        memo[code] = f(code); return memo[code]
    return w
def make_fetch():                       # fictional rate server: 1st call times out
    calls = [0]
    def fetch_rate(code):
        calls[0] += 1
        if calls[0] == 1: raise ConnectionError("timeout")
        return {"USD": 122}[code]
    return fetch_rate, calls

for order in [(cached, logged, retry), (logged, retry, cached)]:
    f, calls = make_fetch()
    for deco in reversed(order): f = deco(f)   # same as @order[0] @order[1] @order[2]
    print(" > ".join(d.__name__ for d in order), "| name:", f.__name__)
    f("USD"); f("USD")
    print(f"  real calls = {calls[0]}")

আউটপুট:

cached > logged > retry | name: fetch_rate
  log: -> USD
  retry: attempt 1 failed (timeout)
  log: <- 122
  cache: hit
  real calls = 2
logged > retry > cached | name: fetch_rate
  log: -> USD
  retry: attempt 1 failed (timeout)
  log: <- 122
  log: -> USD
  cache: hit
  log: <- 122
  real calls = 2

দুই ক্ষেত্রেই সার্ভারে আসল ডাক ২টি (প্রথমটি ব্যর্থ, দ্বিতীয়টি সফল), আর দ্বিতীয় প্রশ্নের উত্তর cache থেকে। কিন্তু log আলাদা। প্রথম ক্রমে cache সবার বাইরে, তাই দ্বিতীয় প্রশ্ন cache-এই থেমে যায় — log সেটি দেখেই না। Log যদি “কতজন ব্যবহারকারী হার দেখলেন” গোনার জন্য হয়, এই ক্রমে সংখ্যাটি কম আসবে। দ্বিতীয় ক্রমে log সবার বাইরে, তাই প্রতিটি অনুরোধ log হয়, cache hit-সহ। কোনটি “ঠিক”, তা নির্ভর করে log-এর উদ্দেশ্যের ওপর — কিন্তু ক্রম যে অর্থ বদলায়, সেটি নিশ্চিত। functools.wraps-এর জন্য মোড়ানো function-এর নামও fetch_rate-ই থাকল; না দিলে নাম হতো w.

একটি ডাক তিন স্তরের মোড়ক ভেদ করে ভেতরে যায়, আর ফল একই পথে বাইরে ফেরে। দ্বিতীয় ক্রমের (logged(retry(cached(fetch)))) প্রথম ডাকটি ধাপে ধাপে দেখুন:

1logged:  print "-> USD" … print "<- r"2  retry:  for attempt in 1..3: try3    cached: memo-তে আছে? না হলে নিচে4      fetch_rate: আসল সার্ভার
অবস্থা (Watch)
দিক
attempt
memo
real calls
  1. সবার বাইরের মোড়ক — logged. ডাক f("USD") প্রথমে logging স্তরে আসে। সে “log: -> USD” লিখে ডাকটি ভেতরে পাঠায়। সে জানে না ভেতরে আর কয়টি স্তর আছে — শুধু জানে ভেতরেরটিও একই interface-এর।
  2. দ্বিতীয় স্তর — retry, attempt ১। Retry একটি try খুলে ডাকটি আরও ভেতরে পাঠায়। এখনো কিছু ছাপা হয়নি।
  3. তৃতীয় স্তর — cached. Memo খালি, তাই miss. Cache নিজে উত্তর দিতে পারল না, ডাকটি আসল function-এর কাছে গেল।
  4. আসল সার্ভার ব্যর্থ। fetch_rate চলল (real calls = ১), কিন্তু প্রথম ডাক বলে ConnectionError("timeout") ছুড়ল। Exception উপরের দিকে উঠছে।
  5. Exception cached পেরিয়ে retry-তে ধরা পড়ল। Cache কিছুই জমায়নি — ব্যর্থতা জমানো হয় না, এটি গুরুত্বপূর্ণ। Retry exception ধরে লিখল “retry: attempt 1 failed (timeout)”. Logging স্তর exception দেখলই না, কারণ সেটি retry পর্যন্তই উঠেছে।
  6. Attempt ২ — আবার ভেতরে। Retry একই ডাক আবার পাঠাল। Cache-এ এখনো কিছু নেই, আবার miss.
  7. এবার সার্ভার সফল। fetch_rate দ্বিতীয়বার চলল (real calls = ২) আর 122 ফেরত দিল।
  8. ফেরার পথে cached জমাল। Cache ফলটি memo-তে রাখল: {USD: 122}, তারপর উপরে পাঠাল। এর পরের যেকোনো "USD" প্রশ্ন এখানেই থামবে।
  9. Retry সফল ফল ফেরত দিল। দ্বিতীয় চেষ্টায় সফল, তাই লুপ শেষ, কিছু ছাপা হলো না।
  10. Logged ফল লিখে বেরিয়ে গেল। “log: <- 122” — ঢোকার সময় প্রথম লাইন, বেরোনোর সময় শেষ লাইন একই স্তরের। দ্বিতীয় ডাকে পথটি ছোট: logged → retry → cached-এ hit → ফিরে এসে আবার log. প্রতিটি স্তর ঢোকার পথে একবার আর বেরোনোর পথে একবার কাজ করার সুযোগ পায় — পেঁয়াজের খোসার মতো।

Python-এর @decorator — সম্পর্কিত, কিন্তু হুবহু এক নয়

Python-এ @cached লিখে function-এর ওপর বসালে সেটি আসলে এই লাইনের সংক্ষিপ্ত রূপ: fetch = cached(fetch). একাধিক হলে নিচ থেকে উপরে প্রয়োগ হয় — নিচের লেখাটি সবচেয়ে ভেতরের মোড়ক:

@logged          # 3rd: outermost
@retry           # 2nd
@cached          # 1st: closest to the function
def fetch_rate(code): ...
# same as: fetch_rate = logged(retry(cached(fetch_rate)))

এই Python decorator GoF Decorator-এর আত্মীয়, কিন্তু পার্থক্যগুলো জানা দরকার:

দিকGoF Decorator patternPython @decorator syntax
কী মোড়ানো হয়একটি object, চলার সময়একটি function বা class-এর সংজ্ঞা, সংজ্ঞা লেখার মুহূর্তে (একবারই)
Interfaceএকই interface রাখা বাধ্যতামূলক — এটিই patternকোনো বাধ্যবাধকতা নেই; decorator যেকোনো কিছু ফেরত দিতে পারে, এমনকি function নয় এমন জিনিস
উদাহরণJava-র new BufferedReader(new FileReader(f))@functools.cache (মোড়ক, pattern-এর কাছাকাছি); @property, @dataclass (মোড়ক নয়, রূপান্তর)
প্রতি object আলাদা?হ্যাঁ — দুটি object-কে দুই রকম মোড়কে মুড়তে পারেননা — সংজ্ঞা একবার বদলায়, ওই function-এর সব ব্যবহারকারী একই মোড়ক পান

উপরের কোডে আমরা for deco in reversed(order): f = deco(f) লিখে চলার সময় মোড়ক বসিয়েছি — এটি GoF-এর চেতনার কাছাকাছি। @ syntax-এর খুঁটিনাটি (functools.wraps, argument-সহ decorator) জানতে Python কোর্সের decorator পর্বটি দেখুন। PEP 318 (Python 2.4, ২০০৪) নিজেই উল্লেখ করেছে যে “decorator” নামটি GoF-এর pattern-এর সাথে পুরো মেলে না বলে সমালোচনা হয়েছিল।

ফলাফল আর ফাঁদ

  • লাভ: প্রতিটি দায়িত্ব একটি ছোট class-এ (পর্ব ২৭-এর single responsibility), নতুন দায়িত্ব যোগে পুরোনো কোড বদলায় না (open-closed), সমন্বয় চলার সময়।
  • ক্রমের ভুল: উপরের log-এর উদাহরণ; বা retry-কে cache-এর ভেতরে না রেখে বাইরে রাখলে কী হয় — ভেবে দেখুন। মোড়ক কে, কোন ক্রমে বসাবে, তা এক জায়গায় রাখুন (পর্ব ৩৭-এ composition root).
  • Identity হারায়: মোড়ানো object আর আসল object আলাদা; === বা is দিয়ে তুলনা, বা instanceof RateFetcher যাচাই ভেঙে যায়।
  • অনেক ছোট object আর লম্বা stack trace: debug করার সময় পাঁচ স্তরের মোড়ক পেরিয়ে আসল কোডে পৌঁছাতে হয়।
  • Interface মোটা হলে কষ্ট: interface-এ ২০টি method থাকলে প্রতিটি decorator-কে ২০টিই ভেতরে পাঠাতে হয় — পর্ব ২৮-এর interface segregation এখানে সরাসরি সাহায্য করে।
  • Strategy-র সাথে গুলিয়ে ফেলা: GoF-এর ভাষায় decorator object-এর “চামড়া” বদলায় (বাইরে থেকে মোড়ে), আর strategy বদলায় “ভেতরের অঙ্গ” (ভেতরের algorithm). Strategy পর্ব ৩৫-এ (Behavioral pattern ১)।

৪ · Facade — জটিল subsystem-এর একটি সহজ দরজা

উদ্দেশ্য: একটি subsystem-এর অনেকগুলো interface-এর সামনে একটি একক, উঁচু স্তরের interface দেওয়া, যাতে subsystem ব্যবহার সহজ হয়। Facade (ফরাসি শব্দ, বাড়ির সামনের দিক) নতুন কোনো ক্ষমতা যোগ করে না; শুধু সাধারণ কাজগুলোকে একটি পরিষ্কার পথে সাজায়।

বিয়েবাড়ির আয়োজন মানে ডেকোরেটর, বাবুর্চি, প্যান্ডেল, আলো, গাড়ি — আলাদা আলাদা লোক, আলাদা ফোন নম্বর, আলাদা শর্ত। অনেক পরিবার তাই একজন ইভেন্ট ম্যানেজারকে বলেন, “শুক্রবার ৩০০ জন, বাকিটা আপনি দেখুন।” ম্যানেজার বাকিদের নিষেধ করেননি — চাইলে আপনি নিজেই বাবুর্চির সাথে মেনু ঠিক করতে পারেন। Facade ঠিক এমন: সহজ পথ দেয়, কিন্তু ভেতরের দরজাগুলো বন্ধ করে না।

সমস্যা: কাল্পনিক অনলাইন বাজার অ্যাপে একটি অর্ডার নিতে stock সংরক্ষণ, payment, ডেলিভারির ঠিকানা যাচাই, ডেলিভারি বুকিং আর SMS — পাঁচটি subsystem পরপর ডাকতে হয়, নির্দিষ্ট ক্রমে, আর মাঝপথে ব্যর্থ হলে আগের কাজ ফিরিয়ে নিতে হয়। এই ক্রম যদি mobile app-এর controller, web-এর controller আর admin panel-এ তিন জায়গায় লেখা থাকে, তিনটি আলাদা হয়ে যাবেই। Facade এই ক্রমকে এক জায়গায় তোলে:

class OrderFacade {
  constructor(stock, payments, delivery, sms) { Object.assign(this, { stock, payments, delivery, sms }); }
  placeOrder(cart, customer) {                 // the one simple entry point
    const hold = this.stock.reserve(cart.items);
    const paid = this.payments.pay(cart.total);   // e.g. an adapter from section 2
    if (!paid.ok) { this.stock.release(hold); return { ok: false, reason: "payment" }; }
    const slot = this.delivery.book(customer.address, hold);
    this.sms.send(customer.phone, "Order confirmed: " + paid.id);
    return { ok: true, paymentId: paid.id, slot };
  }
}

এটি একটি নকশার খসড়া (চালানোর জন্য নয়)। খেয়াল করুন, payments এখানে আগের অংশের adapter-ই হতে পারে — pattern-গুলো একসাথে কাজ করে।

ফলাফল আর ফাঁদ

  • লাভ: client-এর coupling কমে — পাঁচটির বদলে একটি জিনিস চেনে (পর্ব ২৫-এর coupling, পর্ব ২৬-এর Law of Demeter). ক্রম আর ভুল সামলানোর নিয়ম এক জায়গায়।
  • সব লুকানো নয়: GoF স্পষ্টই বলে, facade থাকলেও যাঁদের দরকার, তাঁরা subsystem-এর class সরাসরি ব্যবহার করতে পারেন। Facade মানে “একমাত্র পথ” নয়, “সহজ পথ”।
  • ফাঁদ — god class: সব নতুন কাজ facade-এ গুঁজে দিলে সেটি শত শত method-এর একটি দানব হয়। সমাধান: কাজের ধরন অনুযায়ী একাধিক ছোট facade (অর্ডার, রিপোর্ট, প্রশাসন)।
  • ফাঁদ — ব্যবসার যুক্তি জমা হওয়া: facade সমন্বয় করে; ছাড়ের হিসাব বা stock-এর নিয়মের মতো domain-এর সিদ্ধান্ত subsystem-এ থাকবে।

Network-এর স্তরে একই চিন্তার রূপ হলো API gateway — অনেক service-এর সামনে একটি দরজা; এর বিস্তারিত System Design কোর্সের reverse proxy, API gateway আর BFF পর্বে।

৫ · Proxy — একই interface-এর পেছনে প্রবেশ নিয়ন্ত্রণ

উদ্দেশ্য: আরেকটি object-এর কাছে যাওয়া নিয়ন্ত্রণ করতে তার একটি প্রতিনিধি (surrogate) রাখা। Proxy আসল object-এর হুবহু একই interface মানে, তাই client টেরই পায় না যে সে প্রতিনিধির সাথে কথা বলছে।

অফিস ভবনের দারোয়ানের কথা ভাবুন। দর্শনার্থী “ম্যানেজার সাহেবের সাথে দেখা করব” বললে দারোয়ান খাতায় নাম লেখেন, অনুমতি আছে কি না ফোনে জেনে নেন, আর ম্যানেজার সভায় থাকলে বার্তা রেখে দেন। দর্শনার্থীর দিক থেকে কাজটা একই — “দেখা করা” — কিন্তু মাঝখানে একজন নিয়ন্ত্রক আছেন। দারোয়ান ম্যানেজারের কাজ করেন না, শুধু তাঁর কাছে পৌঁছানো নিয়ন্ত্রণ করেন।

চার রকম প্রচলিত proxy

⏳ Virtual (lazy) proxy

ব্যয়বহুল object — বড় ছবি, ভারী report, database-এর সম্পর্কিত row — প্রথম দরকারের মুহূর্তে তৈরি বা লোড করে। ORM-গুলো (যেমন Hibernate) সম্পর্কিত object-এর lazy loading-এ এমন proxy ব্যবহার করে।

virtual proxy

🛡️ Protection proxy

প্রতিটি ডাকের আগে অনুমতি যাচাই: শুধু পরীক্ষা-নিয়ন্ত্রক ফলাফল প্রকাশ করতে পারবেন, অন্যরা শুধু দেখতে পারবেন। Least privilege-এর একটি কোড-রূপ।

protection proxy

🌐 Remote proxy

অন্য process বা অন্য যন্ত্রের object-এর স্থানীয় প্রতিনিধি: method ডাককে network বার্তায় বদলায়। Java RMI-র stub, gRPC-র generated client — সবই এই ধারণা।

remote proxy

🗃️ Caching proxy

আসলটির ফল জমিয়ে রেখে একই প্রশ্নে জমানো ফল দেয়। Logging বা গণনা রাখা (smart reference) proxy-ও প্রচলিত।

caching proxy

এগুলোকে বলে virtual proxy, protection proxy, remote proxy আর caching proxy.

JavaScript-এর Proxy — pattern যখন ভাষার অংশ

ES2015 থেকে JavaScript-এ Proxy নামে একটি ভাষাগত সুবিধা আছে: new Proxy(target, handler). Handler-এর get, set, has, apply ইত্যাদি method — যাদের বলে trap — target-এর ওপর প্রতিটি মৌলিক কাজ (property পড়া, লেখা, function ডাকা) মাঝপথে ধরে ফেলে। প্রতিটি trap-এর সমনামী একটি default আচরণ Reflect-এ পাওয়া যায়। এর ফলে proxy-র জন্য প্রতিটি method হাতে হাতে লিখতে হয় না। নিচে একটি caching proxy, যা আসল ডাক গুনে দেখায়:

// A fictional slow rate service (pretend each call goes over the network)
let realCalls = 0;
const rateService = {
  getRate(code) { realCalls++; return { USD: 122, EUR: 131, INR: 1.45 }[code]; },
  name: "demo-rates",
};

// Caching proxy built with the language's Proxy object
function withCache(target) {
  const memo = new Map();
  return new Proxy(target, {
    get(obj, prop, receiver) {
      const value = Reflect.get(obj, prop, receiver);
      if (typeof value !== "function") return value;      // plain fields pass through
      return (...args) => {
        const key = prop + ":" + JSON.stringify(args);
        if (!memo.has(key)) memo.set(key, value.apply(obj, args));
        return memo.get(key);
      };
    },
  });
}

const rates = withCache(rateService);       // same interface as rateService
const asked = ["USD", "EUR", "USD", "USD", "INR", "EUR"];
for (const c of asked) console.log(c, rates.getRate(c));
console.log("asked:", asked.length, "| real calls:", realCalls, "| name:", rates.name);
console.log("is rates === rateService?", rates === rateService);

আউটপুট:

USD 122
EUR 131
USD 122
USD 122
INR 1.45
EUR 131
asked: 6 | real calls: 3 | name: demo-rates
is rates === rateService? false

ছয়টি প্রশ্নের মধ্যে আলাদা মুদ্রা মাত্র তিনটি (USD, EUR, INR), তাই আসল service-এ ডাক গেছে ৩ বার — বাকি ৩টি উত্তর এসেছে memo থেকে। rates.name সাধারণ field, get trap সেটি না বদলে পার করে দিয়েছে। Client-এর কোড (rates.getRate(c)) আসল service-এর সাথে যা লিখত, হুবহু তাই। শেষ লাইনটি সতর্কবার্তা: proxy আর আসল object আলাদা object, তাই identity-নির্ভর তুলনা কাজ করবে না।

বাস্তবে Vue 3-এর reactivity-ব্যবস্থা এই ভাষাগত Proxy-র ওপর দাঁড়িয়ে (property পড়া আর লেখা ধরে UI হালনাগাদ করে)। Java-তে একই ধারণা java.lang.reflect.Proxy (JDK 1.3) — Spring-এর মতো framework লেনদেন বা নিরাপত্তার নিয়ম proxy দিয়েই জোড়ে। ভাষাগত Proxy-র আরও trap আর Reflect-এর বিস্তারিত JavaScript কোর্সের Symbol, Proxy আর Reflect পর্বে।

ফলাফল আর ফাঁদ

  • লাভ: প্রবেশ-নিয়ন্ত্রণের কাজ (অনুমতি, lazy load, cache) আসল class থেকে আলাদা থাকে; আসল class নিজের কাজেই মন দেয়।
  • লুকানো latency: remote proxy একটি network ডাককে স্থানীয় method-এর মতো দেখায়। লুপের ভেতরে order.customer.name লিখলে প্রতিবার একটি database query বা network ডাক যেতে পারে (ORM-এর কুখ্যাত “N+1 query” সমস্যা)। Network ব্যর্থ হতে পারে, ধীর হতে পারে — স্থানীয় ডাক তা হয় না।
  • বাসি cache: caching proxy-র ফল কতক্ষণ বৈধ? আসল হার বদলালে memo পুরোনো হয়ে যায়। মেয়াদ (TTL) আর বাতিল করার নিয়ম ছাড়া cache একটি bug-কারখানা।
  • Decorator-এর সাথে পার্থক্য: কাঠামো প্রায় এক। পার্থক্য উদ্দেশ্যে আর সম্পর্কের মালিকানায়: proxy সাধারণত নিজেই ঠিক করে আসল object কখন তৈরি হবে আর কাকে ঢুকতে দেবে; decorator বাইরে থেকে দেওয়া object-কে মুড়ে নতুন আচরণ যোগ করে আর মোড়ক জমানো যায়। উপরের caching proxy-কে decorator বললেও খুব ভুল হবে না — নাম বেছে নিন পাঠককে কী জানাতে চান, সেই অনুযায়ী।

৬ · Composite — অংশ আর পুরোকে একই চোখে দেখা

উদ্দেশ্য: object-গুলোকে গাছের কাঠামোয় সাজিয়ে part-whole hierarchy বোঝানো, যাতে client একটি একক জিনিস আর জিনিসের দলকে একইভাবে ব্যবহার করতে পারে। Composite-এ দুই রকম অংশ: leaf (নিচে আর কিছু নেই, যেমন একটি পণ্য) আর composite (ভেতরে আরও অংশ, যেমন একটি বান্ডিল) — দুটিরই একই interface.

ঈদের উপহারের একটি বক্স: ভেতরে একটি পাঞ্জাবি, একটি আতর আর একটি ছোট মিষ্টির বাক্স — যার ভেতরে আবার রসগোল্লা আর সন্দেশ। “এই বক্সের দাম কত?” প্রশ্নের উত্তর দিতে দোকানি প্রতিটি জিনিসের দাম যোগ করেন; মিষ্টির বাক্সে এসে ভেতরের দুটির দাম যোগ করে বাক্সের ছাড় বাদ দেন, তারপর বড় বক্সের যোগফলে যোগ করেন। ভেতরের বাক্সটিও তাঁর কাছে “একটি জিনিস”, যার দাম আছে। কম্পিউটারের folder-এর ভেতরে folder, অ্যাপের মেনুর ভেতরে sub-menu, browser-এর DOM-এর element-এর ভেতরে element — সবই এই গাছ।

// Composite: a fictional shop's order — items and bundles share one interface
class Item {                               // leaf
  constructor(name, price) { this.name = name; this.price = price; }
  total() { return this.price; }
  print(indent = "") { console.log(`${indent}- ${this.name}: ${this.total()}`); }
}
class Bundle {                             // composite: holds children
  constructor(name, discountPct = 0) { this.name = name; this.discountPct = discountPct; this.children = []; }
  add(...parts) { this.children.push(...parts); return this; }
  total() {
    const sum = this.children.reduce((s, c) => s + c.total(), 0);  // recursion
    return Math.round(sum * (100 - this.discountPct) / 100);
  }
  print(indent = "") {
    const off = this.discountPct ? ` (-${this.discountPct}%)` : "";
    console.log(`${indent}+ ${this.name}${off}: ${this.total()}`);
    this.children.forEach(c => c.print(indent + "    "));
  }
}

const sweets = new Bundle("Sweets box", 10).add(new Item("Roshogolla 1kg", 400), new Item("Sondesh 500g", 300));
const gift = new Bundle("Eid gift box").add(new Item("Panjabi", 1500), sweets, new Item("Attar", 250));
const order = new Bundle("Order #42").add(gift, new Item("Delivery", 60));

order.print();
// The client treats a leaf and a whole tree the same way:
for (const part of [new Item("Pen", 20), order]) console.log(part.constructor.name, "total =", part.total());

আউটপুট:

+ Order #42: 2440
    + Eid gift box: 2380
        - Panjabi: 1500
        + Sweets box (-10%): 630
            - Roshogolla 1kg: 400
            - Sondesh 500g: 300
        - Attar: 250
    - Delivery: 60
Item total = 20
Bundle total = 2440

মিষ্টির বাক্স: (৪০০ + ৩০০) × ৯০% = ৬৩০. উপহার বক্স: ১৫০০ + ৬৩০ + ২৫০ = ২৩৮০. পুরো অর্ডার: ২৩৮০ + ৬০ = ২৪৪০. শেষ লাইন দুটিতে client একটি কলম (leaf) আর পুরো অর্ডার (গাছ) — দুটিকেই একই total() ডেকেছে, কোনো if ছাড়া। Bundle.total() জানে না তার সন্তান leaf না আরেকটি bundle; সে শুধু প্রত্যেককে total() জিজ্ঞেস করে — এটিই recursion-এর সৌন্দর্য।

1+ Order #422    + Eid gift box3        - Panjabi 15004        + Sweets box (-10%)5            - Roshogolla 4006            - Sondesh 3007        - Attar 2508    - Delivery 60
হিসাব (Watch)
call stack
Sweets-এর যোগ
Gift-এর যোগ
Order-এর যোগ
  1. Client একবার ডাকে। order.total(). Order একটি bundle, তাই সে নিজের দুটি সন্তানকে পরপর জিজ্ঞেস করবে। তার যোগফল এখন ০।
  2. প্রথম সন্তান — উপহার বক্স। এটিও একটি bundle, তাই আরেক স্তর নিচে নামা হলো (call stack: Order › Gift). Gift-এর যোগফল শুরু ০ থেকে।
  3. Leaf সরাসরি উত্তর দেয়। পাঞ্জাবি একটি item; তার total() সঙ্গে সঙ্গে ১৫০০ ফেরত দেয়। Gift-এর যোগ এখন ১৫০০।
  4. আরেকটি bundle — মিষ্টির বাক্স। আরও এক স্তর নিচে (stack-এ চারটি নাম)। প্রথম সন্তান রসগোল্লা ৪০০ ফেরত দিল; Sweets-এর যোগ ৪০০।
  5. সন্দেশ ৩০০। Sweets-এর যোগ ৪০০ + ৩০০ = ৭০০. এই bundle-এর সব সন্তান শেষ।
  6. ছাড় বাদ দিয়ে Sweets ফিরল। ৭০০ × (১০০ − ১০) / ১০০ = ৬৩০. এই মান Gift-এর কাছে ফিরল — Gift-এর চোখে মিষ্টির বাক্স শুধুই “৬৩০ টাকার একটি জিনিস”। Gift-এর যোগ ১৫০০ + ৬৩০ = ২১৩০।
  7. আতর, তারপর Gift ফিরল। ২১৩০ + ২৫০ = ২৩৮০; Gift-এ ছাড় নেই, তাই ২৩৮০ Order-এর কাছে ফিরল। Order-এর যোগ ২৩৮০।
  8. ডেলিভারি আর চূড়ান্ত উত্তর। ডেলিভারি একটি leaf, ৬০. Order-এর যোগ ২৩৮০ + ৬০ = ২৪৪০, ছাড় নেই, ফেরত গেল ২৪৪০. গাছের প্রতিটি অংশ ঠিক একবার জিজ্ঞাসিত হয়েছে — n টি node-এ কাজ O(n).

নকশার একটি বাছাই — স্বচ্ছতা বনাম নিরাপত্তা

add() method কোথায় থাকবে? GoF দুটি পথ আলোচনা করে। স্বচ্ছতা (transparency): সাধারণ interface-এ add() রাখুন, যাতে client কখনো leaf আর composite আলাদা করতে না হয় — কিন্তু তখন leaf-এ add() ডাকা যায়, যা অর্থহীন, তাই সেখানে error দিতে হয় (পর্ব ২৮-এর Liskov substitution-এর সাথে টানাপোড়েন)। নিরাপত্তা (safety): add() শুধু composite-এ, যেমন উপরের কোডে — ভুল ডাক compile-এর সময় বা পড়ার সময়ই ধরা পড়ে, কিন্তু গাছ বানানোর সময় client-কে জানতে হয় কোনটি composite. আধুনিক কোডে সাধারণত দ্বিতীয় পথই বেশি দেখা যায়।

  • ফাঁদ — চক্র: একটি bundle-কে ভুল করে নিজের ভেতরেই যোগ করলে total() অসীম recursion-এ পড়ে। add()-এ চক্র যাচাই রাখুন।
  • ফাঁদ — খুব গভীর গাছ: হাজার স্তরের গাছে recursion stack-এর সীমা ছুঁতে পারে; তখন নিজের stack দিয়ে লুপ।
  • ফাঁদ — instanceof দিয়ে শাখা: client যদি “leaf হলে এটা, bundle হলে ওটা” লেখে, pattern-এর মূল লাভটাই হারায়। গাছের ওপর নতুন নতুন কাজ (দাম, ওজন, কর) লাগলে visitor pattern (পর্ব ৩৬, Behavioral pattern ২) সাহায্য করে; গাছ ঘুরে দেখার জন্য iterator-ও সেখানে।

৭ · Bridge আর Flyweight — সংক্ষেপে

Bridge — দুই দিকের বৈচিত্র্য আলাদা রাখা

Bridge-এর উদ্দেশ্য: abstraction আর তার implementation-কে আলাদা করা, যাতে দুটি স্বাধীনভাবে বদলাতে পারে। একটি কাল্পনিক অঙ্কন-অ্যাপে আকৃতি তিনটি (বৃত্ত, বর্গ, ত্রিভুজ), আর আঁকার পদ্ধতি দুটি (পর্দায় SVG, ছাপার জন্য PDF). Inheritance দিয়ে করলে SvgCircle, PdfCircle, SvgSquare… — ৩ × ২ = ৬টি class. নতুন একটি আকৃতি মানে আরও ২টি, নতুন একটি renderer মানে আরও ৩টি। Bridge-এ Shape-এর ভেতরে একটি Renderer-এর reference থাকে: new Circle(new SvgRenderer()). Class লাগে ৩ + ২ = ৫টি, আর ৫টি আকৃতি × ৩টি renderer হলে ১৫-এর বদলে ৮টি।

Adapter-এর সাথে পার্থক্য: adapter বানানো হয় পরে, দুটি আগে থেকে থাকা অমিল জিনিস জুড়তে; bridge পরিকল্পনা করা হয় আগে, যাতে দুই দিক আলাদাভাবে বাড়তে পারে। Database driver-এর নকশা (JDBC-র মতো) — application-এর API একদিকে, প্রতিটি database-এর driver আরেক দিকে — bridge-এর চিন্তার কাছাকাছি।

Flyweight — অভিন্ন অংশ ভাগাভাগি

Flyweight-এর উদ্দেশ্য: ভাগাভাগি করে বিপুলসংখ্যক ছোট object দক্ষতার সাথে রাখা। কৌশলটি হলো object-এর অবস্থাকে দুই ভাগ করা: intrinsic state — যা সব জায়গায় একই (একটি অক্ষরের আকৃতি, একটি map tile-এর ছবি), একবার রেখে ভাগ করা হয়; আর extrinsic state — যা প্রতিটি ব্যবহারে আলাদা (পাতায় অক্ষরের অবস্থান, map-এ tile-এর স্থানাঙ্ক), ডাকের সময় বাইরে থেকে দেওয়া হয়। GoF-এর নিজের উদাহরণ ছিল একটি document editor-এর অক্ষর (glyph).

একটি হিসাব (ধরে নেওয়া সংখ্যায়): একটি document-এ ১,০০,০০০টি অক্ষর, আলাদা অক্ষর-আকৃতি মাত্র ৮০টি। ধরা যাক প্রতিটি আকৃতির object ২ KB. Flyweight ছাড়া প্রতিটি অক্ষরের নিজের object: ১,০০,০০০ × ২ KB = ২,০০,০০০ KB, প্রায় ২০০ MB. Flyweight-এ: ৮০ × ২ KB = ১৬০ KB shared আকৃতি, আর প্রতিটি অক্ষরের জন্য শুধু অবস্থান ও আকৃতির reference, ধরা যাক ১৬ byte: ১,০০,০০০ × ১৬ = ১৬,০০,০০০ byte, প্রায় ১.৬ MB. মোট প্রায় ১.৭৬ MB — শতগুণেরও বেশি কম। আসল সংখ্যা ভাষা ও runtime-ভেদে আলাদা; অনুপাতটিই শিক্ষা।

Flyweight-এর শর্ত: ভাগ করা অংশ অবশ্যই immutable হতে হবে — একজন বদলালে সবার বদলে যাবে। বাস্তবে Java-র Integer.valueOf −১২৮ থেকে ১২৭-এর মান cache করে, আর অনেক ভাষা একই string literal ভাগ করে (string interning) — দুটিই flyweight-এর রূপ। ভাবনার প্রশ্ন ৫-এ এর একটি বিখ্যাত ফাঁদ দেখবেন।

৮ · পাশাপাশি তুলনা — Adapter, Facade, Proxy আর Decorator

চারটি pattern-ই “একটি জিনিস আরেকটিকে মুড়ে রাখে”, তাই কোড দেখে প্রায়ই একই রকম লাগে। আলাদা করার দুটি প্রশ্ন: (১) বাইরের interface কি ভেতরেরটির মতোই? (২) মোড়কটি কেন আছে? নিচের ছবিতে তিনটির UML কাঠামো পাশাপাশি:

Adapter, Decorator আর Proxy-র UML class diagram

Realization (dashed, hollow triangle), generalization (solid, hollow triangle), association (open arrow), aggregation (hollow diamond)

Adapter, Decorator আর Proxy-র কাঠামো ১ · Adapter (object adapter) Checkout PaymentGateway TakaPayAdapter TakaPaySdk «interface» + pay(amount) + pay(amount) + makePayment() sdk1 Target realize করে, ভেতরে Adaptee ধরে রাখে; interface বদলায় ২ · Decorator «interface» DataSource RateFetcher Decorator {abstract} LoggingRetryCaching + fetch(code) + fetch(code) inner1 একই interface realize করে, আরেকটি DataSource-কে মোড়ে ৩ · Proxy «interface» ReportPage RateService RealRateService CachingProxy + getRate(code) + getRate(code) + getRate(code) real0..1 একই interface realize করে, আসলটির কাছে যাওয়া নিয়ন্ত্রণ করে
ছবি ৩৪.১ — Adapter-এর বাইরের interface (PaymentGateway) আর ভেতরের জিনিসের interface (TakaPaySdk) আলাদা। Decorator আর Proxy যে interface realize করে, ভেতরেও সেই interface-এরই জিনিস রাখে। Decorator-এর multiplicity ১ (সবসময় একটি ভেতরের জিনিস); Proxy-র ০..১ — lazy proxy শুরুতে আসলটি ছাড়াই থাকতে পারে।
Patternবাইরের interfaceমূল উদ্দেশ্যকয়টি জিনিস মোড়েচেনার প্রশ্ন
Adapterভিন্ন — client-এর চাওয়া interfaceঅমিল interface মেলানোসাধারণত একটি adaptee“এটি না থাকলে কি দুটি অংশ জোড়াই যেত না?”
Facadeনতুন, সরল, উঁচু স্তরেরজটিল subsystem সহজে ব্যবহারঅনেকগুলো“এটি কি অনেক ডাককে একটি কাজে সাজাচ্ছে?”
Proxyএকইপ্রবেশ নিয়ন্ত্রণ — lazy, অনুমতি, দূরত্ব, cacheএকটি আসল subject“এটি কি ঠিক করছে কখন বা কে আসলটির কাছে যাবে?”
Decoratorএকইনতুন আচরণ যোগ, স্তরে স্তরেএকটি, কিন্তু মোড়ক জমানো যায়“এটি কি আগে-পরে নতুন কাজ জুড়ছে, আর একাধিক মোড়ক বসানো যায়?”

একটি মনে রাখার কৌশল: Adapter — কথা অনুবাদ করে; Facade — কথা সংক্ষেপ করে; Proxy — কথা পাহারা দেয়; Decorator — কথার সাথে কিছু যোগ করে। কাঠামো এক হলেও নাম আলাদা রাখার কারণ এটাই: নামটি পরের পাঠককে বলে দেয় মোড়কটি কেন আছে। Composite আলাদা জাতের — সে একটির বদলে অনেকটিকে মুড়ে একটির মতো দেখায়; আর chain of responsibility-ও (পর্ব ৩৬) decorator-এর মতো শিকল, তবে সেখানে যে-কোনো স্তর ডাকটি থামিয়ে দিতে পারে।

আরও গভীরে: মূল উৎস Gamma, Helm, Johnson ও Vlissides-এর Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, ১৯৯৪)-এর structural অধ্যায়; প্রতিটি pattern-এর “Related Patterns” অংশে এই পর্বের তুলনাগুলো লেখকদের নিজের ভাষায় আছে। Java-র java.io package (decorator), java.awt.Container (composite) আর java.lang.reflect.Proxy হলো standard library-তে pattern-এর সবচেয়ে পড়া উদাহরণ। বড় পরিসরে adapter-এর ধারণা Alistair Cockburn-এর hexagonal architecture (“ports and adapters”, ২০০৫) আর Evans-এর anti-corruption layer-এ পৌঁছেছে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Adapter আর Decorator-এর পার্থক্য কী? — Adapter interface বদলায় (অমিল মেলাতে); Decorator interface একই রেখে আচরণ যোগ করে, আর স্তরে স্তরে বসানো যায়।
  • Proxy আর Decorator-এর কাঠামো একই হলে পার্থক্য কোথায়? — উদ্দেশ্যে: proxy প্রবেশ নিয়ন্ত্রণ করে আর প্রায়ই আসল object-এর জীবনচক্র নিজে সামলায়; decorator বাইরে থেকে দেওয়া object-এ দায়িত্ব যোগ করে।
  • Object adapter আর class adapter? — object adapter composition দিয়ে adaptee ধরে রাখে; class adapter adaptee-কে inherit করে (Java-তে extends Adaptee implements Target).
  • Composite pattern-এর একটি বাস্তব উদাহরণ দিন। — file system-এর file আর folder, UI-র component tree, বা অর্ডারের পণ্য আর বান্ডিল।
  • Flyweight-এ intrinsic আর extrinsic state কী? — intrinsic: ভাগ করা, অপরিবর্তনীয় অংশ (অক্ষরের আকৃতি); extrinsic: প্রতিটি ব্যবহারে আলাদা, বাইরে থেকে দেওয়া (অবস্থান)।

৯ · AI-এর যুগে structural pattern

AI কোড-সহকারী pattern-এর নাম খুব ভালোবাসে। “একটি adapter বানাও” বললে সে প্রায় নিখুঁত দেখতে কোড দেয়। সমস্যা হয় সূক্ষ্ম জায়গায় — এককের রূপান্তর, error-এর অনুবাদ, মোড়কের ক্রম আর UML-এর তীর।

AI-এর সাধারণ ভুলকীভাবে ধরবেন
দুই লাইনের কাজে Facade + Adapter + Factory — pattern-এর অতি-প্রয়োগপ্রশ্ন করুন: আজ কয়টি বাস্তবায়ন আছে? একটি হলে সরল function-ই যথেষ্ট (পর্ব ২৬-এর YAGNI)
Adapter-এ টাকা-পয়সা বা সেকেন্ড-millisecond রূপান্তর বাদ, বা উল্টোএকক-রূপান্তরের জন্য আলাদা test; নকল SDK দিয়ে পাঠানো মান দেখুন
SDK-র exception আর type adapter পেরিয়ে client-এ ফাঁসAdapter-এর public method-এর signature আর ছোড়া error-এর তালিকা review করুন
Retry আর cache-এর ক্রম উল্টো, বা ব্যর্থতাও cache করাপ্রথম ডাক ব্যর্থ হয় এমন নকল service দিয়ে test চালান, আসল ডাক গুনুন
Idempotent নয় এমন payment ডাকে অন্ধ retry — দুবার টাকা কাটাIdempotency key আছে কি না দেখুন (পর্ব ৩১)
ভুল UML: decorator-এর aggregation-এর হীরা ভুল প্রান্তে, realization-এর বদলে টানা রেখাপর্ব ২৯-এর নোটেশন মিলিয়ে নিজে আঁকুন; হীরা থাকে “মালিক” প্রান্তে
ভুল দাবি: “Python decorator আর GoF decorator একই জিনিস”, বা নেই এমন Proxy trap-এর নামPython documentation, MDN-এর Proxy পাতা আর GoF বইয়ে মিলিয়ে নিন

একটি বাইরের SDK মানিয়ে নিতে AI-কে এভাবে কাজ দিন। SDK-র আসল API key, গ্রাহকের তথ্য বা কোম্পানির গোপন কোড prompt-এ দেবেন না; key-র জায়গায় নকল মান লিখুন:

আমার কাল্পনিক অনলাইন দোকানের checkout শুধু এই interface চেনে: pay(amountTaka) → { ok, id, error }. নিচে একটি payment SDK-র method-গুলোর বর্ণনা দিলাম (key-গুলো নকল)। ১. একটি object adapter লেখো (composition, inheritance নয়)। SDK-র কোনো type বা exception adapter-এর বাইরে যাবে না; SDK-র প্রতিটি error code কোন error-এ বদলাবে, একটি সারণি দাও। ২. এককের রূপান্তর (টাকা থেকে SDK-র একক) স্পষ্ট নামের variable-এ করো, আর floating point-এর ঝুঁকি কীভাবে এড়ালে বলো। ৩. Retry যোগ করতে চাইলে সেটি adapter-এর ভেতরে না রেখে আলাদা decorator হিসেবে দাও, আর বলো payment ডাক idempotent না হলে retry কেন বিপজ্জনক। ৪. একটি নকল SDK দিয়ে adapter-এর test লেখো, যার একটি test ইচ্ছা করে ভুল একক পাঠানো adapter-কে ধরতে পারে। ৫. যে SDK method-এর অস্তিত্ব আমার বর্ণনায় নেই, সেটি নিজে বানিয়ে নেবে না; অনিশ্চিত হলে "নিশ্চিত নই" লেখো।

AI-এর খসড়া হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Java-র java.io decorator pattern-এর সবচেয়ে বিখ্যাত উদাহরণ — আবার সবচেয়ে বেশি সমালোচিতও। কেন?

JDK 1.0-তে (১৯৯৬) InputStream-এর পরিবার আর JDK 1.1-এ (১৯৯৭) Reader/Writer-এর পরিবার এসেছিল decorator-এর চিন্তায়। একটি মৌলিক উৎস (FileInputStream), তার ওপর byte থেকে অক্ষরে রূপান্তর (InputStreamReader), তার ওপর buffer (BufferedReader) — প্রতিটি একটি করে মোড়ক। একটি UTF-8 ফাইল লাইন ধরে পড়তে লিখতে হতো new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8")).

নমনীয়তার দিক থেকে এটি সফল: buffer, compression (GZIPInputStream), encryption, checksum — যেকোনোটি যেকোনো উৎসের ওপর, যেকোনো ক্রমে। নতুন উৎস (network socket) এলেও সব মোড়ক কাজ করে। Inheritance দিয়ে করলে প্রতিটি সমন্বয়ের class লাগত।

ব্যবহারের দিক থেকে সমালোচনা: সবচেয়ে সাধারণ কাজের জন্য তিন স্তরের মোড়ক মনে রাখতে হয়; buffer বসাতে ভুলে গেলে প্রোগ্রাম কোনো error ছাড়াই অনেক ধীর হয়; encoding না দিলে platform-এর default encoding চুপচাপ ব্যবহৃত হতো — Windows আর Linux-এ একই ফাইল ভিন্নভাবে পড়া যেত। অর্থাৎ ভুল সমন্বয়টাই সবচেয়ে সহজে লেখা যেত।

পরে কী হলো: Java 7-এ (২০১১) Files.newBufferedReader(path, charset)-এর মতো সহজ পথ এল — কার্যত একটি ছোট facade, যা ভেতরে সেই মোড়কগুলোই সঠিক ক্রমে বসায়। আর JDK 18 থেকে default charset UTF-8 করা হয়েছে (JEP 400).

মূল উপলব্ধি: Decorator নির্মাতাকে নমনীয়তা দেয়, কিন্তু ব্যবহারকারীর ঘাড়ে সমন্বয়ের দায় চাপায়। ভালো library দুটোই দেয় — ভেতরে ছোট ছোট মোড়ক, আর সাধারণ কাজের জন্য সামনে একটি সহজ facade, যার default নিরাপদ।

প্র ০২Remote proxy একটি network ডাককে স্থানীয় method-এর মতো দেখায়। এটি কি সুবিধা, না বিপদ?

১৯৯০-এর দশকে distributed object-এর স্বপ্ন ছিল ঠিক এটাই: CORBA (OMG, প্রথম সংস্করণ ১৯৯১), পরে Java RMI (JDK 1.1, ১৯৯৭) — অন্য যন্ত্রের object-এর জন্য একটি proxy (stub) তৈরি হয়, আর আপনি account.getBalance() লেখেন যেন object আপনার হাতেই আছে। Remote proxy-র সংজ্ঞা অনুযায়ী এটি নিখুঁত — একই interface, অবস্থান লুকানো।

সমস্যা ধরিয়ে দেয় ১৯৯৪ সালে Sun Microsystems Laboratories-এর Jim Waldo ও সহকর্মীদের প্রতিবেদন “A Note on Distributed Computing”. তাঁদের যুক্তি: স্থানীয় আর দূরের ডাক মৌলিকভাবে আলাদা — latency হাজার থেকে লক্ষগুণ বেশি হতে পারে, memory ভাগ করা যায় না, আর সবচেয়ে বড় কথা আংশিক ব্যর্থতা — ডাকটি পৌঁছেছে কি না, কাজ হয়েছে কি না, জানাই সম্ভব নাও হতে পারে। এই পার্থক্য interface-এ লুকিয়ে ফেললে প্রোগ্রামার সেগুলো সামলানোর কোড লেখেন না। একই সময়ের “fallacies of distributed computing”-এর তালিকা (L Peter Deutsch-এর নামে প্রচলিত) — “network নির্ভরযোগ্য”, “latency শূন্য” — এই ভুল ধারণাগুলোরই নাম।

আজকের ব্যবস্থাগুলো তাই proxy রাখে, কিন্তু দূরত্ব লুকায় না। gRPC-র generated client-এ deadline দেওয়া যায় আর error-এ network-এর অবস্থা আলাদা code-এ আসে; অনেক ভাষায় দূরের ডাক async/await দিয়ে চোখে পড়ার মতো আলাদা। ORM-এর lazy proxy-র “N+1 query” সমস্যাও একই শিক্ষার ছোট রূপ: একটি নিরীহ-দেখতে property পড়া আসলে একটি database ডাক।

মূল উপলব্ধি: Proxy ঠিক কী লুকাচ্ছে, সেটি নকশার সিদ্ধান্ত। দূরত্ব, ব্যর্থতা আর সময় — এগুলো client-এর জানা দরকার; interface-এ সেগুলো স্পষ্ট রাখুন (timeout, error type, async). লুকানো উচিত শুধু সেই খুঁটিনাটি, যার কারণে client-এর সিদ্ধান্ত বদলায় না।

প্র ০৩Python-এর decorator syntax-এর নাম নিয়ে কেন বিতর্ক হয়েছিল, আর এটি GoF Decorator সম্পর্কে কী শেখায়?

@ syntax এসেছে PEP 318 থেকে, Python 2.4-এ (২০০৪)। তার আগে লিখতে হতো def f(...): ... আর ফাংশনের শেষে f = staticmethod(f) — বড় function-এ মোড়কটি চোখেই পড়ত না। নতুন syntax মোড়কটিকে সংজ্ঞার মাথায় নিয়ে এল। PEP 318 নিজেই লিখেছে যে “decorator” নামটি নিয়ে আপত্তি উঠেছিল, কারণ GoF বইয়ের Decorator-এর সাথে এর ব্যবহার পুরো মেলে না; তবু নামটি টিকে যায়। পরে PEP 3129 class-এর জন্যও decorator আনে (Python 3.0-এ)।

কোথায় মেলে: functools.cache (Python 3.9) বা lru_cache (3.2), একটি timing বা logging মোড়ক — এগুলো একই ডাকের আকার রেখে আগে-পরে কাজ জোড়ে, অর্থাৎ GoF-এর চেতনা। কোথায় মেলে না: @property একটি method-কে descriptor-এ বদলায়; @dataclass class-এ নতুন method লিখে দেয়; @app.route("/") ধরনের decorator function-টি কোথাও নথিভুক্ত করে আর প্রায়ই অপরিবর্তিত ফেরত দেয়। এগুলো “সংজ্ঞাকে রূপান্তর” — interface একই রাখার কোনো শর্ত নেই। তা ছাড়া Python decorator বসে সংজ্ঞায়, একবার; GoF decorator বসে প্রতিটি object-এ, চলার সময় — একটি RateFetcher-কে cache দিয়ে আর আরেকটিকে ছাড়া রাখা যায়।

এই বিতর্ক pattern-এর নাম সম্পর্কে একটি সাধারণ সত্য দেখায়: একই শব্দ ভিন্ন সম্প্রদায়ে ভিন্ন অর্থ পায় (JavaScript আর TypeScript-এর decorator প্রস্তাবও আলাদা জিনিস)। পর্ব ৩২-এ দেখেছেন, pattern-এর বড় মূল্য তার শব্দভান্ডারে; শব্দ ঝাপসা হলে সেই মূল্য কমে।

মূল উপলব্ধি: কোনো সহকর্মী “decorator” বললে জিজ্ঞেস করুন — ভাষার syntax, না pattern? Pattern চেনা হয় উদ্দেশ্য আর কাঠামো দিয়ে (একই interface, ভেতরে একই interface-এর জিনিস), শব্দ দিয়ে নয়।

প্র ০৪পুরোনো একটি system ধীরে ধীরে বদলাতে adapter কীভাবে সাহায্য করে — আর কখন adapter নিজেই নতুন legacy হয়ে যায়?

Martin Fowler ২০০৪ সালে “strangler fig” রূপকটি লেখেন: অস্ট্রেলিয়ার একটি লতা গাছকে জড়িয়ে বড় হয়, একসময় পুরোনো গাছ ভেতরে মরে যায় আর লতাই গাছ হয়ে দাঁড়ায়। পুরোনো সফটওয়্যার একবারে নতুন করে লেখার বদলে তার চারপাশে নতুন অংশ বানিয়ে ধীরে ধীরে কাজ সরিয়ে নেওয়া। এর প্রতিটি সীমানায় একটি adapter বা facade থাকে: নতুন কোড পুরোনো system-এর অদ্ভুত API-কে নিজের পরিষ্কার interface-এর পেছনে রাখে।

Eric Evans-এর Domain-Driven Design (২০০৩) এই adapter-স্তরকে বলে anti-corruption layer. নামটির অর্থ: পুরোনো system-এর ধারণা — যেমন যেখানে “customer” মানে আসলে “billing account” — যেন নতুন domain-এর ভাষায় চুইয়ে না ঢোকে। অনুবাদ এক জায়গায় হয়, বাকি কোড পরিষ্কার থাকে। একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালে বিশ বছরের পুরোনো একটি পরীক্ষা-শাখার database থাকতে পারে, যার টেবিলের নাম আর কোড অদ্ভুত; নতুন পোর্টাল সরাসরি ওই টেবিল না পড়ে একটি adapter-এর মাধ্যমে নিজের Result object পায়।

বিপদ কোথায়: “সাময়িক” adapter বছরের পর বছর থেকে যায়। একের পর এক বিশেষ নিয়ম (“এই বিভাগের পুরোনো কোড হলে…”) জমে adapter নিজেই একটি জটিল, test-বিহীন, কেউ-বোঝে-না অংশ হয়ে ওঠে — অর্থাৎ নতুন legacy. আরও খারাপ, adapter-এর ওপর adapter: পুরোনো API → adapter → “নতুন” API → আরেকটি adapter.

প্রতিরোধ: adapter-এর উদ্দেশ্য আর মেয়াদ লিখে রাখুন (ADR, পর্ব ৩৮-এ আসবে); adapter-এ শুধু অনুবাদ, কোনো ব্যবসার নিয়ম নয়; উভয় দিকের contract test (পর্ব ৫১); আর পুরোনো অংশ সরে গেলে adapter মুছে ফেলার কাজটিকে backlog-এ আসল কাজ হিসেবে রাখুন। Technical debt আর legacy কোড সামলানো পর্ব ৪৩-এ।

মূল উপলব্ধি: Adapter হলো সীমান্তের অনুবাদক। ভালো অনুবাদক নিজের মত যোগ করেন না, আর সীমান্ত উঠে গেলে তাঁর চাকরিও শেষ হয় — নকশায় সেই শেষটাও পরিকল্পনা করুন।

প্র ০৫Java-তে Integer a = 127, b = 127; হলে a == b সত্য, কিন্তু 128 হলে মিথ্যা। Flyweight এখানে কী শেখায়?

Java-তে int একটি primitive, কিন্তু collection-এ রাখতে লাগে object — Integer. স্বয়ংক্রিয় রূপান্তর (autoboxing) Integer.valueOf ডাকে, আর Java Language Specification (§5.1.7) নির্দিষ্ট করে যে −১২৮ থেকে ১২৭-এর মানের boxing সবসময় একই object দেবে। অর্থাৎ এই পরিসরের ছোট সংখ্যাগুলো flyweight — একবার তৈরি, সবার মধ্যে ভাগ করা। কারণ সহজ: ছোট সংখ্যা প্রোগ্রামে খুব বেশি আসে, প্রতিবার নতুন object বানানো memory আর সময়ের অপচয়। এই পরিসরের বাইরে নতুন object তৈরি হতে পারে (JVM-এর setting-ভেদে cache বড়ও হতে পারে)।

ফাঁদ: == object-এ identity তুলনা করে, মান নয়। তাই ১২৭-এ দুটি reference একই object-কে দেখায় — সত্য; ১২৮-এ সাধারণত দুটি আলাদা object — মিথ্যা, যদিও মান এক। এই bug test-এ ধরা পড়ে না যদি test-এর সংখ্যাগুলো ছোট হয়, আর production-এ বড় id আসা মাত্র হঠাৎ দেখা দেয়। সঠিক পথ a.equals(b). String-এর ক্ষেত্রেও একই গল্প: literal-গুলো intern হয়ে ভাগ হয়, কিন্তু চলার সময় বানানো string নয়।

Flyweight-এর শিক্ষা: ভাগাভাগি তখনই নিরাপদ যখন (১) ভাগ করা object immutable — Integer তা-ই, কেউ ১২৭-কে ১২৮ বানাতে পারে না; আর (২) client identity-র ওপর নির্ভর করে না। দ্বিতীয় শর্তটি client-এর দায়, আর সেখানেই bug. একই কারণে Python-এ is দিয়ে সংখ্যা বা string তুলনা ভুল — CPython ছোট int cache করে, কিন্তু ভাষা তা প্রতিশ্রুতি দেয় না; Python ৩.৮ থেকে literal-এর সাথে is তুলনায় (যেমন x is 128) SyntaxWarning দেয়।

মূল উপলব্ধি: Flyweight memory বাঁচানোর জন্য identity-কে বাস্তবায়নের খুঁটিনাটি বানিয়ে ফেলে। তাই flyweight ব্যবহার করা কোডে মান তুলনা করুন, identity নয় — আর নিজে flyweight বানালে ভাগ করা অংশ অবশ্যই অপরিবর্তনীয় রাখুন।

অনুশীলন

আগে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন।

  1. আউটপুট অনুমান করুন (decorator-এর ক্রম): নিচের Python প্রোগ্রাম চালালে ঠিক কোন ছয়টি লাইন ছাপা হবে, ক্রমসহ লিখুন।
    def twice(f):
        print("apply twice")
        return lambda: f() + " " + f()
    def exclaim(f):
        print("apply exclaim")
        return lambda: f() + "!"
    @exclaim
    @twice
    def greet(): return "salam"
    print(greet())
    @twice
    @exclaim
    def greet2(): return "salam"
    print(greet2())
    apply twice
    apply exclaim
    salam salam!
    apply exclaim
    apply twice
    salam! salam!

    দুটি শিক্ষা। (১) Decorator প্রয়োগ হয় সংজ্ঞা লেখার মুহূর্তে, নিচ থেকে উপরে: greet = exclaim(twice(greet)), তাই আগে “apply twice”, পরে “apply exclaim” — greet() ডাকার আগেই ছাপা হয়। (২) ডাকার সময় বাইরের মোড়ক শেষ কথা বলে: প্রথমটিতে বাইরে exclaim, তাই পুরো “salam salam”-এর শেষে একটি “!”. দ্বিতীয়টিতে বাইরে twice, তাই ভেতরের “salam!” দুবার। একই দুটি মোড়ক, ক্রম উল্টালে ফল আলাদা।

  2. Pattern চিনুন: প্রতিটি পরিস্থিতিতে কোন structural pattern? (ক) একটি পুরোনো XML-ভিত্তিক SMS service-কে নতুন send(phone, text) interface-এর পেছনে রাখা। (খ) ভারী PDF report শুধু ব্যবহারকারী “দেখুন” চাপলে তৈরি করা, তার আগে একটি হালকা প্রতিনিধি object. (গ) একটি Notifier-এ চলার সময় “আগে অনুবাদ”, “তারপর হার-সীমা”, “তারপর log” যোগ করা — প্রতিটি ঐচ্ছিক। (ঘ) Menu-র ভেতরে sub-menu আর item, সবার render(). (ঙ) enrollStudent() একটি ডাকে ভর্তি, ফি, হোস্টেল আর লাইব্রেরি — চারটি subsystem. (চ) একটি map অ্যাপে লক্ষ লক্ষ গাছের আইকন, কিন্তু গাছের প্রজাতি মাত্র ১২টি।
    • (ক) Adapter — আগে থেকে থাকা অমিল interface মেলানো; interface বদলাচ্ছে।
    • (খ) Proxy (virtual/lazy) — একই interface, আসল object কখন তৈরি হবে তা proxy ঠিক করে।
    • (গ) Decorator — একই interface, ঐচ্ছিক দায়িত্ব স্তরে স্তরে, আর ক্রমের অর্থ আছে (অনুবাদের আগে log করলে মূল ভাষার লেখা log হবে)।
    • (ঘ) Composite — part-whole গাছ, leaf আর composite-এর একই render().
    • (ঙ) Facade — অনেক subsystem-এর সামনে একটি সহজ, উঁচু স্তরের ডাক।
    • (চ) Flyweight — প্রজাতির ছবি (intrinsic) ১২টি shared object-এ; প্রতিটি গাছের স্থানাঙ্ক (extrinsic) আলাদা।
  3. হিসাব — inheritance বনাম composition: (ক) একটি Notifier-এর চারটি ঐচ্ছিক বৈশিষ্ট্য (log, retry, অনুবাদ, হার-সীমা)। প্রতিটি সমন্বয়ের জন্য subclass বানালে কয়টি class (মূলটিসহ) লাগবে? Decorator-এ কয়টি? (খ) একটি চার্ট লাইব্রেরিতে ৫ রকম চার্ট আর ৩ রকম output (SVG, Canvas, PDF). শুধু inheritance-এ কয়টি concrete class, Bridge-এ কয়টি? (গ) ৫০,০০০টি map tile দেখানো হয়, আলাদা ছবি ২০০টি, প্রতিটি ছবি ৪০ KB, প্রতিটি tile-এর অবস্থান ১২ byte. Flyweight ছাড়া (প্রতিটি tile-এ নিজের ছবি) আর flyweight-সহ memory কত?

    (ক) প্রতিটি বৈশিষ্ট্য থাকতে বা না থাকতে পারে: ২⁴ = ১৬টি সমন্বয়, যার একটি হলো কিছু-না-থাকা মূল class; অর্থাৎ ১৬টি class. ক্রমও আলাদা অর্থ বহন করলে সংখ্যা আরও বাড়ে। Decorator-এ: মূল class ১টি + ৪টি decorator = ৫টি (একটি সাধারণ abstract decorator রাখলে ৬টি)।

    (খ) Inheritance: ৫ × ৩ = ১৫টি concrete class. Bridge: ৫টি চার্ট + ৩টি renderer = ৮টি. আরেকটি output যোগ করলে inheritance-এ আরও ৫টি, bridge-এ ১টি।

    (গ) Flyweight ছাড়া: ৫০,০০০ × ৪০ KB = ২০,০০,০০০ KB, প্রায় ২ GB (প্লাস অবস্থান)। Flyweight-সহ: ছবি ২০০ × ৪০ KB = ৮,০০০ KB (প্রায় ৮ MB), আর অবস্থান ৫০,০০০ × ১২ = ৬,০০,০০০ byte (প্রায় ০.৬ MB); মোট প্রায় ৮.৬ MB. (বাস্তবে tile-এর ছবি একই হলে browser-ও cache করে; হিসাবটি ধারণা বোঝানোর জন্য।)

  4. নকশা ও সমালোচনা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টাল দুটি SMS provider ব্যবহার করে (একটির API sendSms(to, body), আরেকটির post({msisdn, message, unicode: true}))। দরকার: provider বদলানো যাবে; ব্যর্থ হলে retry; প্রতিটি SMS log; শুধু পরীক্ষা-নিয়ন্ত্রক ফলাফল প্রকাশ করতে পারবেন; আর controller-এর জন্য একটি ডাক publishResults(examId) — যা ফল লক করে, PDF বানায়, SMS পাঠায়। কোন অংশে কোন pattern, class-গুলোর নাম আর সম্পর্ক লিখুন। একজন সহকর্মী প্রস্তাব দিলেন: “সব একটি ResultManager class-এ রাখি, ৪০০ লাইন, সহজ।” আপনার সমালোচনা কী?
    • Target interface: SmsSender — send(phone, text) → { ok, id, error }.
    • Adapter: AlphaSmsAdapter আর BetaSmsAdapter, দুটিই SmsSender realize করে, ভেতরে নিজের SDK ধরে রাখে। Beta-র জন্য phone-কে msisdn আকারে (দেশের কোডসহ) রূপান্তর আর বাংলা লেখার জন্য unicode: true — এগুলোর test.
    • Decorator: RetryingSender আর LoggingSender, দুটিই SmsSender, ভেতরে আরেকটি SmsSender. ক্রম: Logging(Retry(adapter)) হলে প্রতিটি SMS একবার log হয়, চেষ্টার সংখ্যাসহ; প্রতিটি চেষ্টা log করতে চাইলে উল্টো। একই SMS দুবার যাওয়া ঠেকাতে message id দিয়ে idempotency.
    • Protection proxy: GuardedResultService — ResultService-এর একই interface; publish()-এর আগে ব্যবহারকারীর ভূমিকা যাচাই, না মিললে error আর audit log.
    • Facade: ResultPublishingFacade.publishResults(examId) — lock → PDF → SMS-এর ক্রম আর মাঝপথে ব্যর্থ হলে কী হবে, এক জায়গায়। ভেতরের service-গুলো অন্য কাজে সরাসরি ব্যবহার করা যায়।
    • জোড়া লাগানো: সব মোড়ক এক জায়গায় তৈরি (composition root, পর্ব ৩৭)।

    সমালোচনা: ৪০০ লাইনের ResultManager আজ সহজ দেখায়, কিন্তু (১) provider বদলাতে পুরো class খুলতে হবে, (২) অনুমতি-যাচাই, retry আর log ব্যবসার যুক্তির সাথে মিশে যাবে — একটি ভুলে নিরাপত্তা ছিদ্র, (৩) SMS না পাঠিয়ে test করা কঠিন, (৪) বদলানোর কারণ অন্তত চারটি — single responsibility ভাঙে। আবার অন্য দিকও ন্যায্য: provider যদি সত্যিই একটি, আর retry-র দরকার এখনো না দেখা যায়, তাহলে সব pattern আজই বানানো YAGNI ভাঙে। ভারসাম্যপূর্ণ উত্তর: interface আর একটি adapter এখনই (বাইরের SDK-কে আটকে রাখতে), protection proxy-ও এখনই (নিরাপত্তা পরে যোগ করা কঠিন), বাকিগুলো যখন দরকার।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি বাইরের payment SDK-র জন্য adapter বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Client-এর দরকার থেকে target interface ঠিক করা — pay(amount) → { ok, id }
  2. SDK-র API, একক আর error code পড়ে রূপান্তরের সারণি বানানো
  3. Target realize করে SDK-র object ধরে রাখা adapter লেখা
  4. নকল SDK দিয়ে একক-রূপান্তর আর error-অনুবাদের test চালানো
  5. Client-কে SDK-র বদলে target interface-এর ওপর নির্ভর করানো

একটি class ভেতরে আরেকটি object রাখে আর ভিন্ন একটি interface বাইরে দেয়, যাতে পুরোনো একটি library নতুন কোডের প্রত্যাশিত চুক্তিতে কাজ করে। এটি কোন pattern?

  • Decorator
  • Adapter
  • Proxy
  • Composite

Python-এ লেখা আছে: @a, তার নিচে @b, তার নিচে def f(): .... এটি কোন লাইনের সমান?

  • f = b(a(f))
  • f() ডাকার সময় a(f) আর b(f) আলাদাভাবে চলে
  • f = a(b(f))
  • এটি SyntaxError, একাধিক decorator দেওয়া যায় না

একটি হার-সার্ভারের function প্রথম ডাকে timeout দেয়, পরে ঠিক উত্তর দেয়। দুটি মোড়ক: retry (৩ বার পর্যন্ত চেষ্টা) আর cached (সফল ফল জমায়)। গঠন cached(retry(fetch)). একই প্রশ্ন পরপর দুবার করলে আসল সার্ভারে কয়টি ডাক যাবে?

  • ২টি
  • ১টি
  • ৩টি
  • ৪টি

Proxy আর Decorator — দুটিই একই interface রাখে আর ভেতরে একটি object ধরে রাখে। নিচের কোনটি তাদের পার্থক্য সবচেয়ে ভালোভাবে বলে?

  • Proxy inheritance দিয়ে, decorator composition দিয়ে লেখা হয়
  • Proxy interface বদলায়, decorator রাখে
  • Decorator অনেকগুলো subsystem-কে একসাথে মোড়ে
  • উদ্দেশ্যে: proxy প্রবেশ নিয়ন্ত্রণ করে, decorator নতুন দায়িত্ব যোগ করে

একটি অর্ডারের গাছ: বান্ডিল A-তে আছে item ২০০ আর বান্ডিল B; B-তে আছে item ৩০০ আর item ৫০০, B-তে ১০% ছাড়। A-তে ছাড় নেই। Composite-এর recursive total()-এ A-র মোট কত?

  • ১০০০
  • ৯০০
  • ৯২০
  • ৮২০

৪টি আকৃতি আর ৩টি rendering পদ্ধতি আছে। প্রতিটি জোড়ার জন্য subclass বানানোর বদলে Bridge ব্যবহার করলে মোট কয়টি concrete class লাগবে, আর কেন?

  • ১২টি, কারণ প্রতিটি জোড়া আলাদা
  • ৭টি, কারণ আকৃতি আর renderer আলাদাভাবে বাড়ে
  • ১টি, সব শর্ত একটি class-এ
  • ৩টি, শুধু renderer-গুলো

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Structural pattern = interface + composition দিয়ে জিনিস জোড়ার নকশা; GoF-এর সাতটি: adapter, bridge, composite, decorator, facade, flyweight, proxy.
  2. Adapter interface বদলায় — বাইরের SDK-কে নিজের চুক্তিতে আটকায়; object adapter (composition) সাধারণত ভালো; এককের রূপান্তর আর error-অনুবাদের test রাখুন।
  3. Decorator একই interface রেখে স্তরে স্তরে আচরণ যোগ করে; ক্রম অর্থ বদলায়। Python-এর @ syntax সম্পর্কিত, কিন্তু সংজ্ঞাকে একবার রূপান্তর করে আর interface রাখার শর্ত নেই।
  4. Facade সহজ দরজা দেয়, ভেতরের দরজা বন্ধ করে না; Proxy একই interface-এ প্রবেশ নিয়ন্ত্রণ করে — lazy, protection, remote, caching; JavaScript-এ ভাষাগত Proxy.
  5. Composite অংশ আর পুরোকে একই interface দেয়, হিসাব recursive; Bridge m × n-কে m + n বানায়; Flyweight immutable অভিন্ন অংশ ভাগ করে memory বাঁচায় — identity-র ওপর ভরসা নয়।

পরের পর্বে: পর্ব ৩৫-এ behavioral pattern-এর প্রথম ভাগ — strategy, observer, command আর template method: object-গুলো কীভাবে দায়িত্ব ভাগ করে আর একে অপরের সাথে কথা বলে। Decorator-এর “চামড়া বদল” আর strategy-র “ভেতরের অঙ্গ বদল”-এর পার্থক্য সেখানে পুরো স্পষ্ট হবে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান