Part 01 · Low Level Design
Structural Patterns: Adapter, Decorator, Proxy
বাইরের 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।
তবে একটা জায়গায় মেলে না। আসল adapter এর খরচ প্রায় শূন্য। Software এ প্রতিটা মোড়ক পড়া আর debug করার জন্য আরেকটা layer যোগ করে, তাই মোড়ক দাও যখন সেটা সত্যিকারের duplication সরায় বা সত্যিকারের একটা dependency আলাদা করে, অভ্যাসে না।
Notification যাচ্ছে Retry{inner: BulkSMSAdapter} দিয়ে। Company আবার SMS vendor বদলালো। কী বদলাবে?
Object জোড়া লাগানোর পাঁচটা উপায়
- Adapter: ভুল interface ওয়ালা একটা object কে মুড়ে দেয়, আর তোমার code যে interface চায় সেটা দেখায়। Call (
Send(to, msg)→Dispatch([]string{to}, msg, senderID)) আর error অনুবাদ করে। যে code তোমার না, তার প্রতিটা সীমানায় একটা বসাও: vendor SDK, পুরনো module, অন্য team এর API। - Decorator: একই interface ওয়ালা একটা object কে মোড়ে, আর কাজটা ভেতরে পাঠানোর আগে বা পরে আচরণ যোগ করে: retry, logging, metric, caching, rate limiting। Decorator একটার উপর আরেকটা বসানো যায় (
Logging{Retry{Adapter}}), আর ভেতরের object বদলায় না। - Proxy: এরও interface একই, কিন্তু এর কাজ হলো আসল object এ ঢোকা নিয়ন্ত্রণ করা: ফল cache করা, অনুমতি check করা, আসল object টা দরকার হলে তবেই বানানো, বা দূরের একটা object এর হয়ে দাঁড়ানো। Proxy আর decorator এর পার্থক্য উদ্দেশ্যে: decorator feature যোগ করে; proxy পাহারা দেয় বা বদলি হয়।
- Facade: একটা জটিল subsystem এর সামনে একটা সহজ object।
Checkout.PlaceOrder(cart)stock check, payment, invoice বানানো আর notification লুকিয়ে রাখে। Caller রা পায় একটা দরজা; পেছনের subsystem বদলাতে পারে। - Composite: একটা tree, যেখানে একক জিনিস আর group দুটোরই interface এক। একটা
Itemএর দাম আছে; item (আর অন্য combo) দিয়ে বানানো একটাComboরও দাম আছে, যেটা সে নিজের অংশ গুলোকে জিজ্ঞেস করে বের করে। Caller এর হাতে কোনটা আছে, তাতে কিছু যায় আসে না। File system, org chart আর menu হলো composite। - পাঁচটাই interface আর composition এর উপর দাঁড়িয়ে। কোনোটারই inheritance লাগে না; প্রত্যেকে অন্য object কে মোড়ে বা ধরে রাখে।
ধীর call বাঁচানো একটা proxy
rates.Rate(USD) দুইবার, তারপর Rate(EUR)
ধাপ 1/5 Caller একটা RateService কে জিজ্ঞেস করে
তার হাতে একটা RateService। এটা যে proxy, সে জানে না।
- ask
- USD
বাইরে একই, ভেতরে আলাদা
এই chapter এর সব মোড়কই বাইরে থেকে একরকম দেখায়, তাই জিজ্ঞেস করো মোড়কটা কী কাজে:
- বাইরের আকার ভেতরের চেয়ে আলাদা? → Adapter।
- আকার একই, বাড়তি আচরণ? → Decorator।
- আকার একই, ঢোকা পাহারা দেয় বা বদলি হয়? → Proxy।
- অনেকগুলো object এর উপর একটা সহজ আকার? → Facade।
- একটা জিনিস আর জিনিসের group এর আকার একই? → Composite।
নিজে বলো: কোন structural pattern?
Pattern এর নাম বলো
type SSLCommerzAdapter struct{ sdk *sslc.Client }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())
}
}from __future__ import annotations
from typing import Protocol
class Notifier(Protocol):
"""Our own interface: what the rest of the app expects from any notifier."""
def send(self, to: str, msg: str) -> None: ...
# ---- Adapter: a third-party SMS API with a different shape.
class BulkSMS: # pretend this comes from a vendor's SDK
def dispatch(self, numbers: list[str], text: str, sender_id: str) -> int:
print(f"adapter: bulksms dispatched to {numbers[0]} from {sender_id}")
return 200
class BulkSMSAdapter:
"""Makes BulkSMS look like a Notifier, so nothing else has to change."""
def __init__(self, api: BulkSMS):
self.api = api
def send(self, to: str, msg: str) -> None:
code = self.api.dispatch([to], msg, "CodingFoding")
if code != 200:
raise RuntimeError(f"bulksms code {code}")
# ---- Decorator: adds retries around ANY Notifier, without changing it.
class Retry:
def __init__(self, inner: Notifier, attempts: int):
self.inner, self.attempts = inner, attempts
def send(self, to: str, msg: str) -> None:
for i in range(1, self.attempts + 1):
try:
self.inner.send(to, msg)
print(f"decorator: attempt {i} ok")
return
except RuntimeError as e:
print(f"decorator: attempt {i} failed: {e}")
if i == self.attempts:
raise
class Flaky:
"""Fails the first time, then works (to show the retry)."""
def __init__(self) -> None:
self.calls = 0
def send(self, to: str, msg: str) -> None:
self.calls += 1
if self.calls == 1:
raise RuntimeError("timeout")
# ---- Proxy: same interface as the real service, but remembers answers.
class SlowRates:
def __init__(self) -> None:
self.calls = 0
def rate(self, currency: str) -> int:
self.calls += 1 # imagine a 2-second network call here
return {"USD": 122, "EUR": 131}[currency]
class CachedRates:
def __init__(self, real: SlowRates):
self.real = real
self.cache: dict[str, int] = {}
def rate(self, currency: str) -> int:
if currency not in self.cache:
self.cache[currency] = self.real.rate(currency)
return self.cache[currency]
# ---- Composite: a single item and a combo of items share one interface.
class MenuPart(Protocol):
def price(self) -> int: ...
class Item:
def __init__(self, name: str, tk: int):
self.name, self.tk = name, tk
def price(self) -> int:
return self.tk
class Combo:
def __init__(self, name: str, parts: list[MenuPart]):
self.name, self.parts = name, parts # items or other combos
def price(self) -> int:
return sum(p.price() for p in self.parts)
n: Notifier = BulkSMSAdapter(BulkSMS())
n.send("01711000000", "Your OTP is 4821")
Retry(Flaky(), 3).send("01711000000", "Your order is on the way")
slow = SlowRates()
rates = CachedRates(slow)
for c in ("USD", "USD", "EUR"):
r = rates.rate(c)
print(f"proxy: {c} rate {r} (calls to slow service: {slow.calls})")
drinks = Combo("Drinks", [Item("Coke", 60), Item("Coke", 60)])
family = Combo("Family Combo", [Item("Biryani", 450), Item("Biryani", 450), drinks, Item("Firni", 150)])
for combo in (family, drinks):
print(f"composite: {combo.name} = {combo.price()} tk")// Our own interface: what the rest of the app expects from any notifier.
interface Notifier {
send(to: string, msg: string): void;
}
// ---- Adapter: a third-party SMS API with a different shape.
class BulkSMS {
// pretend this comes from a vendor's SDK
dispatch(numbers: string[], _text: string, senderId: string): number {
console.log(`adapter: bulksms dispatched to ${numbers[0]} from ${senderId}`);
return 200;
}
}
// Makes BulkSMS look like a Notifier, so nothing else has to change.
class BulkSMSAdapter implements Notifier {
readonly api: BulkSMS;
constructor(api: BulkSMS) {
this.api = api;
}
send(to: string, msg: string): void {
const code = this.api.dispatch([to], msg, "CodingFoding");
if (code !== 200) throw new Error(`bulksms code ${code}`);
}
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
class Retry implements Notifier {
readonly inner: Notifier;
readonly attempts: number;
constructor(inner: Notifier, attempts: number) {
this.inner = inner;
this.attempts = attempts;
}
send(to: string, msg: string): void {
for (let i = 1; i <= this.attempts; i++) {
try {
this.inner.send(to, msg);
console.log(`decorator: attempt ${i} ok`);
return;
} catch (e) {
console.log(`decorator: attempt ${i} failed: ${(e as Error).message}`);
if (i === this.attempts) throw e;
}
}
}
}
// Fails the first time, then works (to show the retry).
class Flaky implements Notifier {
calls = 0;
send(): void {
this.calls++;
if (this.calls === 1) throw new Error("timeout");
}
}
// ---- Proxy: same interface as the real service, but remembers answers.
interface RateService {
rate(currency: string): number;
}
class SlowRates implements RateService {
calls = 0;
rate(currency: string): number {
this.calls++; // imagine a 2-second network call here
return ({ USD: 122, EUR: 131 } as Record<string, number>)[currency]!;
}
}
class CachedRates implements RateService {
readonly real: RateService;
readonly cache = new Map<string, number>();
constructor(real: RateService) {
this.real = real;
}
rate(currency: string): number {
if (!this.cache.has(currency)) this.cache.set(currency, this.real.rate(currency));
return this.cache.get(currency)!;
}
}
// ---- Composite: a single item and a combo of items share one interface.
interface MenuPart {
price(): number;
}
class Item implements MenuPart {
readonly name: string;
readonly tk: number;
constructor(name: string, tk: number) {
this.name = name;
this.tk = tk;
}
price(): number {
return this.tk;
}
}
class Combo implements MenuPart {
readonly name: string;
readonly parts: MenuPart[]; // items or other combos
constructor(name: string, parts: MenuPart[]) {
this.name = name;
this.parts = parts;
}
price(): number {
return this.parts.reduce((sum, p) => sum + p.price(), 0);
}
}
const n: Notifier = new BulkSMSAdapter(new BulkSMS());
n.send("01711000000", "Your OTP is 4821");
new Retry(new Flaky(), 3).send("01711000000", "Your order is on the way");
const slow = new SlowRates();
const rates: RateService = new CachedRates(slow);
for (const c of ["USD", "USD", "EUR"]) {
const r = rates.rate(c);
console.log(`proxy: ${c} rate ${r} (calls to slow service: ${slow.calls})`);
}
const drinks = new Combo("Drinks", [new Item("Coke", 60), new Item("Coke", 60)]);
const family = new Combo("Family Combo", [new Item("Biryani", 450), new Item("Biryani", 450), drinks, new Item("Firni", 150)]);
for (const combo of [family, drinks]) console.log(`composite: ${combo.name} = ${combo.price()} tk`);use std::cell::Cell;
use std::collections::HashMap;
// Our own interface: what the rest of the app expects from any notifier.
trait Notifier { fn send(&self, to: &str, msg: &str) -> Result<(), String>; }
// ---- Adapter: a third-party SMS API with a different shape.
struct BulkSms; // pretend this comes from a vendor's SDK
impl BulkSms {
fn dispatch(&self, numbers: &[&str], _text: &str, sender_id: &str) -> u16 {
println!("adapter: bulksms dispatched to {} from {sender_id}", numbers[0]);
200
}
}
// Makes BulkSms look like a Notifier, so nothing else has to change.
struct BulkSmsAdapter { api: BulkSms }
impl Notifier for BulkSmsAdapter {
fn send(&self, to: &str, msg: &str) -> Result<(), String> {
match self.api.dispatch(&[to], msg, "CodingFoding") {
200 => Ok(()),
code => Err(format!("bulksms code {code}")),
}
}
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
struct Retry<N: Notifier> { inner: N, attempts: u32 }
impl<N: Notifier> Notifier for Retry<N> {
fn send(&self, to: &str, msg: &str) -> Result<(), String> {
let mut last = Err(String::new());
for i in 1..=self.attempts {
last = self.inner.send(to, msg);
match &last {
Ok(()) => { println!("decorator: attempt {i} ok"); return Ok(()); }
Err(e) => println!("decorator: attempt {i} failed: {e}"),
}
}
last
}
}
// Fails the first time, then works (to show the retry).
struct Flaky { calls: Cell<u32> }
impl Notifier for Flaky {
fn send(&self, _: &str, _: &str) -> Result<(), String> {
self.calls.set(self.calls.get() + 1);
if self.calls.get() == 1 { Err("timeout".into()) } else { Ok(()) }
}
}
// ---- Proxy: same interface as the real service, but remembers answers.
trait RateService { fn rate(&mut self, currency: &str) -> u32; }
struct SlowRates { calls: u32 }
impl RateService for SlowRates {
fn rate(&mut self, c: &str) -> u32 {
self.calls += 1; // imagine a 2-second network call here
match c { "USD" => 122, "EUR" => 131, _ => 0 }
}
}
struct CachedRates<R: RateService> { real: R, cache: HashMap<String, u32> }
impl<R: RateService> RateService for CachedRates<R> {
fn rate(&mut self, c: &str) -> u32 {
if let Some(&r) = self.cache.get(c) { return r; }
let r = self.real.rate(c);
self.cache.insert(c.to_string(), r);
r
}
}
// ---- Composite: a single item and a combo of items share one interface.
trait MenuPart { fn price(&self) -> u32; }
struct Item { _name: &'static str, tk: u32 }
impl MenuPart for Item { fn price(&self) -> u32 { self.tk } }
struct Combo { name: &'static str, parts: Vec<Box<dyn MenuPart>> } // items or other combos
impl MenuPart for Combo { fn price(&self) -> u32 { self.parts.iter().map(|p| p.price()).sum() } }
fn main() {
let n: Box<dyn Notifier> = Box::new(BulkSmsAdapter { api: BulkSms });
let _ = n.send("01711000000", "Your OTP is 4821");
let _ = Retry { inner: Flaky { calls: Cell::new(0) }, attempts: 3 }.send("01711000000", "Your order is on the way");
let mut rates = CachedRates { real: SlowRates { calls: 0 }, cache: HashMap::new() };
for c in ["USD", "USD", "EUR"] {
let r = rates.rate(c);
println!("proxy: {c} rate {r} (calls to slow service: {})", rates.real.calls);
}
let drinks = || Combo { name: "Drinks", parts: vec![Box::new(Item { _name: "Coke", tk: 60 }), Box::new(Item { _name: "Coke", tk: 60 })] };
let family = Combo { name: "Family Combo", parts: vec![
Box::new(Item { _name: "Biryani", tk: 450 }), Box::new(Item { _name: "Biryani", tk: 450 }),
Box::new(drinks()), Box::new(Item { _name: "Firni", tk: 150 }),
] };
for c in [&family, &drinks()] { println!("composite: {} = {} tk", c.name, c.price()); }
}import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
// Our own interface: what the rest of the app expects from any notifier.
interface Notifier { void send(String to, String msg); }
// ---- Adapter: a third-party SMS API with a different shape.
static final class BulkSms { // pretend this comes from a vendor's SDK
int dispatch(List<String> numbers, String text, String senderId) {
System.out.printf("adapter: bulksms dispatched to %s from %s%n", numbers.get(0), senderId);
return 200;
}
}
// Makes BulkSms look like a Notifier, so nothing else has to change.
record BulkSmsAdapter(BulkSms api) implements Notifier {
public void send(String to, String msg) {
int code = api.dispatch(List.of(to), msg, "CodingFoding");
if (code != 200) throw new IllegalStateException("bulksms code " + code);
}
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
record Retry(Notifier inner, int attempts) implements Notifier {
public void send(String to, String msg) {
for (int i = 1; i <= attempts; i++) {
try {
inner.send(to, msg);
System.out.printf("decorator: attempt %d ok%n", i);
return;
} catch (IllegalStateException e) {
System.out.printf("decorator: attempt %d failed: %s%n", i, e.getMessage());
if (i == attempts) throw e;
}
}
}
}
// Fails the first time, then works (to show the retry).
static final class Flaky implements Notifier {
private int calls = 0;
public void send(String to, String msg) {
if (++calls == 1) throw new IllegalStateException("timeout");
}
}
// ---- Proxy: same interface as the real service, but remembers answers.
interface RateService { int rate(String currency); }
static final class SlowRates implements RateService {
int calls = 0;
public int rate(String c) {
calls++; // imagine a 2-second network call here
return Map.of("USD", 122, "EUR", 131).get(c);
}
}
static final class CachedRates implements RateService {
private final RateService real;
private final Map<String, Integer> cache = new HashMap<>();
CachedRates(RateService real) { this.real = real; }
public int rate(String c) { return cache.computeIfAbsent(c, real::rate); }
}
// ---- Composite: a single item and a combo of items share one interface.
interface MenuPart { int price(); }
record Item(String name, int tk) implements MenuPart {
public int price() { return tk; }
}
record Combo(String name, List<MenuPart> parts) implements MenuPart { // items or other combos
public int price() { return parts.stream().mapToInt(MenuPart::price).sum(); }
}
public static void main(String[] args) {
Notifier n = new BulkSmsAdapter(new BulkSms());
n.send("01711000000", "Your OTP is 4821");
new Retry(new Flaky(), 3).send("01711000000", "Your order is on the way");
SlowRates slow = new SlowRates();
RateService rates = new CachedRates(slow);
for (String c : List.of("USD", "USD", "EUR")) {
int r = rates.rate(c);
System.out.printf("proxy: %s rate %d (calls to slow service: %d)%n", c, r, slow.calls);
}
Combo drinks = new Combo("Drinks", List.of(new Item("Coke", 60), new Item("Coke", 60)));
Combo family = new Combo("Family Combo", List.of(new Item("Biryani", 450), new Item("Biryani", 450), drinks, new Item("Firni", 150)));
for (Combo c : List.of(family, drinks)) System.out.printf("composite: %s = %d tk%n", c.name(), c.price());
}
}#include <cstdio>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
// Our own interface: what the rest of the app expects from any notifier.
class Notifier {
public:
virtual ~Notifier() = default;
virtual void send(const std::string& to, const std::string& msg) = 0;
};
// ---- Adapter: a third-party SMS API with a different shape.
class BulkSms { // pretend this comes from a vendor's SDK
public:
int dispatch(const std::vector<std::string>& numbers, const std::string&, const std::string& senderId) {
std::printf("adapter: bulksms dispatched to %s from %s\n", numbers[0].c_str(), senderId.c_str());
return 200;
}
};
// Makes BulkSms look like a Notifier, so nothing else has to change.
class BulkSmsAdapter : public Notifier {
public:
void send(const std::string& to, const std::string& msg) override {
int code = api_.dispatch({to}, msg, "CodingFoding");
if (code != 200) throw std::runtime_error("bulksms code " + std::to_string(code));
}
private:
BulkSms api_;
};
// ---- Decorator: adds retries around ANY Notifier, without changing it.
class Retry : public Notifier {
public:
Retry(Notifier& inner, int attempts) : inner_(inner), attempts_(attempts) {}
void send(const std::string& to, const std::string& msg) override {
for (int i = 1; i <= attempts_; i++) {
try {
inner_.send(to, msg);
std::printf("decorator: attempt %d ok\n", i);
return;
} catch (const std::runtime_error& e) {
std::printf("decorator: attempt %d failed: %s\n", i, e.what());
if (i == attempts_) throw;
}
}
}
private:
Notifier& inner_;
int attempts_;
};
// Fails the first time, then works (to show the retry).
class Flaky : public Notifier {
public:
void send(const std::string&, const std::string&) override {
if (++calls_ == 1) throw std::runtime_error("timeout");
}
private:
int calls_ = 0;
};
// ---- Proxy: same interface as the real service, but remembers answers.
class RateService {
public:
virtual ~RateService() = default;
virtual int rate(const std::string& currency) = 0;
};
class SlowRates : public RateService {
public:
int calls = 0;
int rate(const std::string& c) override {
calls++; // imagine a 2-second network call here
return c == "USD" ? 122 : c == "EUR" ? 131 : 0;
}
};
class CachedRates : public RateService {
public:
explicit CachedRates(RateService& real) : real_(real) {}
int rate(const std::string& c) override {
auto it = cache_.find(c);
if (it != cache_.end()) return it->second;
return cache_[c] = real_.rate(c);
}
private:
RateService& real_;
std::map<std::string, int> cache_;
};
// ---- Composite: a single item and a combo of items share one interface.
class MenuPart {
public:
virtual ~MenuPart() = default;
virtual int price() const = 0;
};
class Item : public MenuPart {
public:
Item(std::string name, int tk) : name_(std::move(name)), tk_(tk) {}
int price() const override { return tk_; }
private:
std::string name_;
int tk_;
};
class Combo : public MenuPart {
public:
explicit Combo(std::string name) : name(std::move(name)) {}
Combo& add(std::shared_ptr<MenuPart> p) { parts_.push_back(std::move(p)); return *this; } // items or other combos
int price() const override {
int total = 0;
for (const auto& p : parts_) total += p->price();
return total;
}
const std::string name;
private:
std::vector<std::shared_ptr<MenuPart>> parts_;
};
int main() {
BulkSmsAdapter sms;
Notifier& n = sms;
n.send("01711000000", "Your OTP is 4821");
Flaky flaky;
Retry(flaky, 3).send("01711000000", "Your order is on the way");
SlowRates slow;
CachedRates rates(slow);
for (std::string c : {"USD", "USD", "EUR"}) {
int r = rates.rate(c);
std::printf("proxy: %s rate %d (calls to slow service: %d)\n", c.c_str(), r, slow.calls);
}
auto drinks = std::make_shared<Combo>("Drinks");
drinks->add(std::make_shared<Item>("Coke", 60)).add(std::make_shared<Item>("Coke", 60));
auto family = std::make_shared<Combo>("Family Combo");
family->add(std::make_shared<Item>("Biryani", 450)).add(std::make_shared<Item>("Biryani", 450)).add(drinks);
family->add(std::make_shared<Item>("Firni", 150));
for (const auto& c : {family, drinks}) std::printf("composite: %s = %d tk\n", c->name.c_str(), 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 নেই;Flakyshared 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 করে
যে মোড়কটা পরে ভোগাবে
func Notify(resp *bulksms.Response) ...
একটু থেমে ভাবো
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 পাও →