Part 01 · Low Level Design

Structural Patterns: Adapter, Decorator, Proxy

AdapterDecoratorProxyFacadeComposite

বাইরের API কে নিজের মতো করে নেওয়া, subclass ছাড়াই behaviour যোগ করা, সামনে একটা check বা cache করা stand-in বসানো, জটিল subsystem এ একটাই দরজা, আর পুরো tree কে একটা leaf এর মতো ব্যবহার করা।

⏱ 2-3 ঘণ্টা · Station 9 / 80

Vendor বদলালো, সাথে সবকিছুই

একটা e-commerce app একটা SMS company র SDK দিয়ে OTP পাঠায়। SDK এর method হলো Dispatch(numbers []string, text, senderID) int, আর এই call টা সরাসরি ব্যবহার হয় ১২ জায়গায়।

তারপর company আরেকটা সস্তা SMS vendor এ চলে গেল, যার SDK আলাদা: send_message(to, body, options) -> Response। ১২ জায়গাই বদলাতে হলো। এক মাস পর কেউ একজন দুর্বল network এর জন্য retry যোগ করলো, ৯ জায়গায় একটা for loop copy করে। তারপর কেউ খেয়াল করলো currency rate এর API প্রতিটা page view এ ডাকা হচ্ছে আর প্রতি call এ টাকা লাগছে, আর তিন রকম ভাবে cache বসালো।

App এর আসলে নতুন আচরণ যতটা দরকার ছিল, তার চেয়ে বেশি দরকার ছিল ভালো মোড়ক: এমন একটা উপায়, যাতে কোনো object কে খাপ খাওয়ানো, বাড়ানো বা পাহারা দেওয়া যায়, যে code সেটা ব্যবহার করে তাতে হাত না দিয়ে। Structural pattern ঠিক এটা নিয়েই।

সমস্যা: object গুলো কীভাবে জোড়া লাগে, সেটা বদলানো

পরিস্থিতিPattern
একটা third-party API তোমার code এর সাথে খাপ খায় নাAdapter
কিছু একটার চারপাশে retry, logging বা সময় মাপা যোগ করতে চাও, ওটা edit না করেDecorator
দামি কিছুকে cache করতে, ঢোকার অনুমতি নিয়ন্ত্রণ করতে, বা দেরিতে বানাতে চাওProxy
একটা subsystem জটিল, আর caller দের একটা সহজ দরজা লাগেFacade
একটা জিনিস আর জিনিসের একটা group কে একইভাবে ব্যবহার করতে হবেComposite

Plug, phone cover আর receptionist এর কথা ভাবো

তোমার laptop charger এর UK plug, আর দেয়ালে বাংলাদেশি socket। তুমি laptop নতুন করে wiring করো না; একটা travel adapter লাগাও (Adapter)।

Phone cover grip আর সুরক্ষা যোগ করে, অথচ ভেতরের phone ঠিক আগের মতোই চলে। Cover এর উপর card holder ও লাগানো যায়। এটা decorator: বাইরের আকার একই, আচরণ বেশি, আর একটার উপর আরেকটা বসানো যায়।

ব্যস্ত office এ receptionist সাধারণ প্রশ্নের উত্তর নিজেই দিয়ে দেন, আর দরকার হলেই শুধু manager কে ডাকেন। Caller বুঝতেও পারে না manager এর সাথে কথা হলো কিনা। এটা proxy।

checkoutSend(to, msg)RetrydecoratorBulkSMSAdapteradaptervendor SDKDispatch([]num, ...)try 1timeouttry 2200 OKeach wrapper has the same Send() shape, so checkout never knows they're there
Play চাপো। Checkout Send ডাকে। Call টা Retry decorator আর adapter পার হয়ে vendor এর SDK তে যায়, যার আকার আলাদা। প্রথম চেষ্টায় timeout; Retry আবার পাঠায়, এবার পৌঁছায়। Checkout জানতেও পারে না মোড়ক গুলো আছে।

তবে একটা জায়গায় মেলে না। আসল adapter এর খরচ প্রায় শূন্য। Software এ প্রতিটা মোড়ক পড়া আর debug করার জন্য আরেকটা layer যোগ করে, তাই মোড়ক দাও যখন সেটা সত্যিকারের duplication সরায় বা সত্যিকারের একটা dependency আলাদা করে, অভ্যাসে না।

আগে অনুমান করো

Notification যাচ্ছে Retry{inner: BulkSMSAdapter} দিয়ে। Company আবার SMS vendor বদলালো। কী বদলাবে?

Object জোড়া লাগানোর পাঁচটা উপায়

  1. Adapter: ভুল interface ওয়ালা একটা object কে মুড়ে দেয়, আর তোমার code যে interface চায় সেটা দেখায়। Call (Send(to, msg) → Dispatch([]string{to}, msg, senderID)) আর error অনুবাদ করে। যে code তোমার না, তার প্রতিটা সীমানায় একটা বসাও: vendor SDK, পুরনো module, অন্য team এর API।
  2. Decorator: একই interface ওয়ালা একটা object কে মোড়ে, আর কাজটা ভেতরে পাঠানোর আগে বা পরে আচরণ যোগ করে: retry, logging, metric, caching, rate limiting। Decorator একটার উপর আরেকটা বসানো যায় (Logging{Retry{Adapter}}), আর ভেতরের object বদলায় না।
  3. Proxy: এরও interface একই, কিন্তু এর কাজ হলো আসল object এ ঢোকা নিয়ন্ত্রণ করা: ফল cache করা, অনুমতি check করা, আসল object টা দরকার হলে তবেই বানানো, বা দূরের একটা object এর হয়ে দাঁড়ানো। Proxy আর decorator এর পার্থক্য উদ্দেশ্যে: decorator feature যোগ করে; proxy পাহারা দেয় বা বদলি হয়।
  4. Facade: একটা জটিল subsystem এর সামনে একটা সহজ object। Checkout.PlaceOrder(cart) stock check, payment, invoice বানানো আর notification লুকিয়ে রাখে। Caller রা পায় একটা দরজা; পেছনের subsystem বদলাতে পারে।
  5. Composite: একটা tree, যেখানে একক জিনিস আর group দুটোরই interface এক। একটা Item এর দাম আছে; item (আর অন্য combo) দিয়ে বানানো একটা Combo রও দাম আছে, যেটা সে নিজের অংশ গুলোকে জিজ্ঞেস করে বের করে। Caller এর হাতে কোনটা আছে, তাতে কিছু যায় আসে না। File system, org chart আর menu হলো composite।
  6. পাঁচটাই interface আর composition এর উপর দাঁড়িয়ে। কোনোটারই inheritance লাগে না; প্রত্যেকে অন্য object কে মোড়ে বা ধরে রাখে।

ধীর call বাঁচানো একটা proxy

ধাপে ধাপে দেখো

rates.Rate(USD) দুইবার, তারপর Rate(EUR)

CallerCachedRates proxyCacheSlowRates

ধাপ 1/5 Caller একটা RateService কে জিজ্ঞেস করে

তার হাতে একটা RateService। এটা যে proxy, সে জানে না।

ask
USD

বাইরে একই, ভেতরে আলাদা

এই chapter এর সব মোড়কই বাইরে থেকে একরকম দেখায়, তাই জিজ্ঞেস করো মোড়কটা কী কাজে:

  • বাইরের আকার ভেতরের চেয়ে আলাদা? → Adapter।
  • আকার একই, বাড়তি আচরণ? → Decorator।
  • আকার একই, ঢোকা পাহারা দেয় বা বদলি হয়? → Proxy।
  • অনেকগুলো object এর উপর একটা সহজ আকার? → Facade।
  • একটা জিনিস আর জিনিসের group এর আকার একই? → Composite।

নিজে বলো: কোন structural pattern?

প্রতিটা case চালিয়ে দেখো

Pattern এর নাম বলো

type SSLCommerzAdapter struct{ sdk *sslc.Client }
কাজ করে: Adapter। Vendor এর আকারকে তোমার আকারে অনুবাদ করে।

Code এ adapter, decorator, proxy আর combo

একটা vendor SDK এর জন্য adapter, যেকোনো notifier এর চারপাশে একটা retry decorator, একটা ধীর rate service এর সামনে caching proxy, আর একটা composite menu যেখানে combo র ভেতরে item আর অন্য combo থাকে।

package main

import (
	"errors"
	"fmt"
)

// Our own interface: what the rest of the app expects from any notifier.
type Notifier interface {
	Send(to, msg string) error
}

// ---- Adapter: a third-party SMS API with a different shape.

type BulkSMS struct{} // pretend this comes from a vendor's SDK

func (BulkSMS) Dispatch(numbers []string, text, senderID string) int {
	fmt.Printf("adapter: bulksms dispatched to %s from %s\n", numbers[0], senderID)
	return 200
}

// BulkSMSAdapter makes BulkSMS look like a Notifier, so nothing else has to change.
type BulkSMSAdapter struct{ api BulkSMS }

func (a BulkSMSAdapter) Send(to, msg string) error {
	if code := a.api.Dispatch([]string{to}, msg, "CodingFoding"); code != 200 {
		return fmt.Errorf("bulksms code %d", code)
	}
	return nil
}

// ---- Decorator: adds retries around ANY Notifier, without changing it.

type Retry struct {
	Inner    Notifier
	Attempts int
}

func (r Retry) Send(to, msg string) error {
	var err error
	for i := 1; i <= r.Attempts; i++ {
		if err = r.Inner.Send(to, msg); err == nil {
			fmt.Printf("decorator: attempt %d ok\n", i)
			return nil
		}
		fmt.Printf("decorator: attempt %d failed: %v\n", i, err)
	}
	return err
}

// flaky fails the first time, then works (to show the retry).
type flaky struct{ calls *int }

func (f flaky) Send(to, msg string) error {
	*f.calls++
	if *f.calls == 1 {
		return errors.New("timeout")
	}
	return nil
}

// ---- Proxy: same interface as the real service, but remembers answers.

type RateService interface{ Rate(currency string) int }

type SlowRates struct{ Calls int }

func (s *SlowRates) Rate(c string) int {
	s.Calls++ // imagine a 2-second network call here
	return map[string]int{"USD": 122, "EUR": 131}[c]
}

type CachedRates struct {
	real  RateService
	cache map[string]int
}

func (p *CachedRates) Rate(c string) int {
	if r, ok := p.cache[c]; ok {
		return r
	}
	r := p.real.Rate(c)
	p.cache[c] = r
	return r
}

// ---- Composite: a single item and a combo of items share one interface.

type MenuPart interface{ Price() int }

type Item struct {
	Name string
	Tk   int
}

func (i Item) Price() int { return i.Tk }

type Combo struct {
	Name  string
	Parts []MenuPart // items or other combos
}

func (c Combo) Price() int {
	total := 0
	for _, p := range c.Parts {
		total += p.Price()
	}
	return total
}

func main() {
	var n Notifier = BulkSMSAdapter{}
	_ = n.Send("01711000000", "Your OTP is 4821")

	calls := 0
	_ = Retry{Inner: flaky{&calls}, Attempts: 3}.Send("01711000000", "Your order is on the way")

	slow := &SlowRates{}
	var rates RateService = &CachedRates{real: slow, cache: map[string]int{}}
	for _, c := range []string{"USD", "USD", "EUR"} {
		r := rates.Rate(c)
		fmt.Printf("proxy: %s rate %d (calls to slow service: %d)\n", c, r, slow.Calls)
	}

	drinks := Combo{"Drinks", []MenuPart{Item{"Coke", 60}, Item{"Coke", 60}}}
	family := Combo{"Family Combo", []MenuPart{Item{"Biryani", 450}, Item{"Biryani", 450}, drinks, Item{"Firni", 150}}}
	for _, c := range []Combo{family, drinks} {
		fmt.Printf("composite: %s = %d tk\n", c.Name, c.Price())
	}
}

ছয়টাই হুবহু এটা print করে:

adapter: bulksms dispatched to 01711000000 from CodingFoding
decorator: attempt 1 failed: timeout
decorator: attempt 2 ok
proxy: USD rate 122 (calls to slow service: 1)
proxy: USD rate 122 (calls to slow service: 1)
proxy: EUR rate 131 (calls to slow service: 2)
composite: Family Combo = 1170 tk
composite: Drinks = 120 tk

Output টা পড়ি:

  • adapter: caller ব্যবহার করেছে Notifier.Send; adapter সেটাকে vendor এর Dispatch বানিয়েছে, নম্বরের একটা slice আর একটা sender ID সহ।
  • decorator: প্রথম চেষ্টায় timeout, Retry আবার চেষ্টা করলো। দুর্বল sender টার নিজের কোনো retry code নেই, আর Retry চলতো adapter, একটা mock, বা Notifier মানে এমন যেকোনো কিছুর চারপাশেই।
  • proxy: USD দুইবার চাওয়া হলেও ধীর service ডাকা হয়েছে একবার, আর EUR এর জন্য একবার। Caller জানেই না একটা cache আছে।
  • composite: Family Combo = ৪৫০ + ৪৫০ + (৬০ + ৬০) + ১৫০ = ১১৭০। এর ভেতরের Drinks combo শুধু আরেকটা MenuPart।
  • Language ভেদে কী আলাদা: Rust এর decorator আর proxy ভেতরের type এর উপর generic (Retry<N: Notifier>), তাই কোনো virtual call নেই; Flaky shared reference দিয়ে call গুনতে Cell ব্যবহার করে। Java র computeIfAbsent(c, real::rate) এক লাইনেই একটা caching proxy। C++ combo র অংশ share করে shared_ptr দিয়ে, কারণ একই Drinks combo Family Combo র ভেতরে আবার আলাদা করেও আছে। Python আর TypeScript এ decorator এর জন্য কোনো interface ঘোষণা লাগে না: send থাকলেই খাপ খায়।

Real code এ মোড়ক

  • সীমানায় একবারই adapt করো। প্রতিটা vendor SDK, payment gateway বা পুরনো API এর একটা করে adapter, যেটা তোমার interface এ কথা বলে। Vendor এর type কখনো business code এ ঢোকে না, তাই vendor বদলানো এক file এর কাজ।
  • Decorator ছোট রাখো, আর ক্রম ইচ্ছা করে ঠিক করো। Metrics{Retry{Timeout{Adapter}}} মানে: সবকিছু মাপো, সময় ধরা পুরো call টা retry করো, প্রতিটা চেষ্টার timeout দাও। ক্রম বদলালে আচরণ বদলায়; এক জায়গায় লেখো (সাধারণত main এ)।
  • Caching proxy র জন্য invalidation এর একটা plan লাগে। TTL, size এর সীমা, আর আসল service down থাকলে কী হবে। যে cache কখনো মেয়াদ শেষ হয় না, সে চিরকাল গতকালের exchange rate দেখায়।
  • Facade নতুনদের বোঝাতে আর test এ দারুণ, কিন্তু business rule ও রাখতে রাখতে god object হয়ে যেতে দিও না। এর কাজ সমন্বয় করা, সিদ্ধান্ত নেওয়া না।
  • গভীরতার দিকে খেয়াল রাখো। একটা call এর চারপাশে পাঁচটা মোড়ক থাকলে stack trace লম্বা হয় আর debug ধীর হয়। মোড়ক দাও যেখানে লাভ আছে।

যে design গুলো পরে problem create করে

প্রতিটা case চালিয়ে দেখো

যে মোড়কটা পরে ভোগাবে

func Notify(resp *bulksms.Response) ...
ভাঙে: কোনো adapter এর সীমানা নেই: পরের বার vendor বদলালে bulksms লেখা প্রতিটা file এ হাত পড়বে। Adapter এর ভেতরেই নিজের type এ বদলে নাও।

একটু থেমে ভাবো

SMS adapter এর চারপাশে তুমি logging আর retry দুটোই চাও। Logger কি retry কে মুড়বে, নাকি retry logger কে?

উত্তর দেখো

নির্ভর করে তুমি কী দেখতে চাও তার উপর। Logging{Retry{Adapter}} প্রতি send এ এক লাইন log করে, শেষ ফল সহ: business log এর জন্য ভালো। Retry{Logging{Adapter}} প্রতি চেষ্টায় এক লাইন log করে, failure গুলো সহ: দুর্বল vendor debug করার জন্য ভালো। দুইটাই ঠিক design; আসল কথা হলো decorator থাকলে wiring এর এক লাইন বদলেই এটা বেছে নেওয়া যায়, adapter, retry বা logger কোনোটাতে হাত না দিয়ে।

ছোট্ট quiz

১. Adapter আর Decorator এর পার্থক্য কী?

উত্তর

Adapter interface বদলায় (ভুল আকার ঢোকে, ঠিক আকার বের হয়)। Decorator interface একই রাখে আর call এর চারপাশে আচরণ যোগ করে।

২. প্রতিটা third-party SDK এর চারপাশে adapter কেন?

উত্তর

যাতে vendor এর type আর অদ্ভুত আচরণ গুলো এক file এ থাকে। Business code তোমার interface এর উপর নির্ভর করে, আর vendor বদলালে শুধু adapter বদলায়।

৩. দুজনেরই interface একই হলে Proxy আর Decorator এর পার্থক্য কোথায়?

উত্তর

উদ্দেশ্যে: decorator feature যোগ করে (logging, retry); proxy আসল object এ ঢোকা নিয়ন্ত্রণ করে (caching, অনুমতি check, দেরিতে বানানো, দূরের object এর বদলি)।

৪. Menu বা file system এর জন্য Composite কাজে লাগে কেন?

উত্তর

একক জিনিস আর group এর interface এক, তাই caller যেকোনো node কে তার দাম বা size জিজ্ঞেস করতে পারে, সেটা leaf না group check না করেই; group শুধু নিজের অংশ গুলোকে জিজ্ঞেস করে।

তোমার challenge: একটা logging decorator আর বড় একটা combo

  • একটা Logging decorator লেখো, যেটা ভেতরে পাঠানোর আগে print করবে log: send to <to>। এটাকে retry র বাইরে মোড়াও: Logging{Retry{flaky}}।
  • একটা Party Pack combo যোগ করো, যার ভেতরে দুইটা Family Combo।

চালানোর আগে আন্দাজ করো নতুন লাইন গুলো কী হবে।

হয়েছে বুঝবে যখন retry র অংশ শুরু হবে log: send to 01711000000 দিয়ে (দুইটা চেষ্টার আগে একবারই print), নতুন একটা লাইন আসবে composite: Party Pack = 2340 tk, আর Retry, Flaky বা Combo কোনোটাই edit করতে হবে না।

এক কথায়

নতুন করে না লিখে মুড়ে দাও: adapter একটা বাইরের object কে তোমার interface দেয়, decorator একই interface রেখে আচরণ যোগ করে আর একটার উপর আরেকটা বসে, proxy আসল জিনিসে ঢোকা পাহারা দেয় বা cache করে, facade একটা জটিল subsystem কে একটা দরজা দেয়, আর composite একটা জিনিস আর জিনিসের একটা group কে একই প্রশ্নের উত্তর দিতে দেয়।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →