Part 01 · Low Level Design
Creational Patterns: Factory, Builder, Singleton
কোন class বানানো হচ্ছে সেটা লুকিয়ে রাখা, দশটা option ওয়ালা constructor সামলানো, আর কেন Singleton সবার শেষে ধরার pattern।
⏱ 2-3 ঘণ্টা · Station 7 / 80
চল্লিশ জায়গায় new
একটা courier company র app OTP পাঠায়। Code এর চল্লিশ জায়গায় লেখা sender = new SmsSender(apiKey, "BD", 3, true)। তারপর company corporate client দের জন্য email এ OTP আনলো, আর Grameenphone নম্বরের জন্য আলাদা একটা SMS provider। এখন চল্লিশ জায়গাতেই একটা if লাগবে, আর প্রত্যেকে constructor এর চারটা argument একটু একটু আলাদা ভাবে দেয়।
একই app parcel book করে এমন একটা constructor দিয়ে যেটা এগারোটা argument নেয়: new Parcel("P-1", "Dhaka", "Sylhet", 2, 1500, true, false, null, 0, "", false)। কোন true টা fragile, কেউ মনে রাখে না। ৪৫ kg এর একটা parcel তৈরি হয়ে যায়, van এ ওঠে, তারপর hub এ গিয়ে বাতিল হয়।
দুইটা bug এর জন্ম একই জায়গায়: object কীভাবে বানানো হয়। Creational pattern হলো এই প্রশ্নের কয়েকটা চেনা উত্তর।
সমস্যা: object বানানোর code সব জায়গায় ছড়িয়ে যায়
| যা দেখা যায় | যে pattern কাজে লাগে |
|---|---|
if channel == "sms" { new X } else { new Y } পুরো code জুড়ে বারবার | Factory |
অনেক optional argument এর একটা constructor, ডাকা হয় null, 0, false দিয়ে | Builder |
| Object অর্ধেক ভরা, ভুল অবস্থায় থাকতে পারে | Builder (Build এ একবার check) |
| "আমাদের ঠিক একটা config / একটা connection pool লাগবে" | সাধারণত dependency injection, কখনো কখনো Singleton |
| এমন object এর পরিবার যাদের মিলতে হবে (বাংলা UI widget এর সাথে বাংলা font) | Abstract Factory |
চায়ের দোকান আর দর্জির দোকানে order দেওয়ার কথা ভাবো
চায়ের দোকানে গিয়ে বলো "এক কাপ দুধ চা"। কোন কেতলি, কোন কাপ, কোন supplier এর দুধ, দোকানদারকে বলো না। Board এ যা যা আছে, প্রতিটা কীভাবে বানাতে হয় দোকান জানে। এটা factory: তুমি বলো কী চাও; সে ঠিক করে কোন আসল জিনিসটা বানাবে।
দর্জির কাছে একটা শার্টের কিছু জিনিস required (মাপ, কাপড়), আর অনেক কিছু optional (পকেট, কলারের ধরন, cuff, নামের অক্ষর)। দর্জি তোমার পছন্দ গুলো একটা একটা করে slip এ লেখে, তারপর কাপড় কাটার আগে দেখে নেয় সব মিলছে কিনা ("হাফ হাতা শার্টে French cuff হয় না")। এটা builder: setting একটা একটা করে জড়ো করো, একবারে check করো, তারপর পুরো জিনিসটা বানাও।
তবে একটা জায়গায় মেলে না। দর্জি কোনো সমস্যা হলে তোমাকে phone করতে পারে। Builder শুধু একটা error দিয়ে ফিরিয়ে দিতে পারে, তাই তার error message এমন পরিষ্কার হতে হবে যাতে তার code না পড়েই call টা ঠিক করা যায়।
NewSender(channel) ই একমাত্র জায়গা যেটা 'sms' বা 'email' কে object বানায়। Company WhatsApp এ OTP আনলো। OTP পাঠায় এমন কয়টা call site বদলাতে হবে?
Object বানানোর চারটা ভালো উপায়
- Factory (factory method বা simple factory): একটা function বা method কোনো input দেখে ঠিক করে কোন concrete type বানাবে। Caller রা নির্ভর করে interface আর factory এর উপর, কখনো concrete class এর উপর না। Type দেখে
switchটা থাকে ঠিকই, কিন্তু একবার, factory এর ভেতরে। - Abstract Factory: এমন সম্পর্কিত object এর একটা পরিবারের জন্য factory, যাদের একে অপরের সাথে মিলতে হবে:
BanglaThemeবানায় বাংলা button, বাংলা date picker আর বাংলা font;EnglishThemeবানায় English এর set টা। পরিবার একবারই বাছো, আর ভুল করে মিশিয়ে ফেলার উপায় থাকে না। - Builder: setting জড়ো করা আর object বানানো আলাদা করে। required field যায় constructor এ, optional গুলো নাম দেওয়া ধাপ হিসেবে, আর
Build()পুরো জিনিসটা একবার check করে। ফল: পড়া যায় এমন call (খালিtrueএর বদলে.Fragile()), আর কোনো অর্ধেক-ঠিক object নেই। Python এ default সহ keyword argument প্রায়ই একই কাজ করে; Rust এ builder সাধারণতselfকে value হিসেবে নেয়। - Singleton: একটা class এর একটাই instance নিশ্চিত করে, সাধারণত একটা global access point সহ। সত্যিকারের একটা দরকার মেটায় (একটা connection pool, একটা config), কিন্তু সত্যিকারের ভোগান্তিও আনে: লুকানো dependency, test এর মধ্যে shared state, বদলানো কঠিন এমন সহযোগী। এটা ধরো সবার শেষে।
mainএ একটা instance বানিয়ে বাইরে থেকে দিলে global ছাড়াই "একটাই" ব্যাপারটা পাওয়া যায়। - সবগুলোর উদ্দেশ্য একটাই: "কোন concrete জিনিস, কোন setting এ, ঠিক আছে কিনা" এই সিদ্ধান্ত এক জায়গায় রাখা, যাতে বাকি code শুধু object ব্যবহার করে।
একটা parcel বানানো, ধাপে ধাপে
NewParcel("P-3", Dhaka, Rajshahi).Weight(45).Build()
ধাপ 1/4 আগে required field
Constructor নেয় যা প্রতিটা parcel এর থাকতেই হবে: ID, from, to। Weight default ১ kg।
- id
- P-3
- from
- Dhaka
- to
- Rajshahi
- kg
- 1
কী বানানো হবে, সেটা কে ঠিক করে?
Object ব্যবহার করা আর বানানো, এই দুইয়ের মাঝে একটা দাগ টানো:
- তোমার বেশিরভাগ code শুধু interface দিয়ে object ব্যবহার করবে।
- অল্প কয়েকটা জায়গা এগুলো বানাবে: factory, builder, আর
main, যেটা শুরুতে সবকিছু জোড়া লাগায়।
Business logic এর গভীরে new ConcreteThing(...) দেখলে জিজ্ঞেস করো: এটা কি factory call হওয়া উচিত, নাকি বাইরে থেকে দেওয়া একটা dependency?
নিজে বলো: কোন creational pattern খাপ খায়?
Pattern টা বাছো
gateway := ??? ("BD")Code এ sender আর parcel
একটা factory যেটা channel এর নাম থেকে Sender বানায়, আর parcel এর জন্য একটা builder যেটা Build() এ পুরো parcel টা check করে।
package main
import (
"errors"
"fmt"
"strings"
)
// ---- Factory: callers ask for "a sender for this channel" and never name the concrete type.
type Sender interface {
Send(to, msg string) string
}
type SMS struct{}
func (SMS) Send(to, msg string) string { return "sms -> " + to + ": " + msg }
type Email struct{}
func (Email) Send(to, msg string) string { return "email -> " + to + ": " + msg }
// NewSender is the one place that knows which type serves which channel.
func NewSender(channel string) (Sender, error) {
switch channel {
case "sms":
return SMS{}, nil
case "email":
return Email{}, nil
}
return nil, fmt.Errorf("unknown channel %q", channel)
}
// ---- Builder: a parcel has a few required fields and several optional ones.
type Parcel struct {
ID, From, To string
Kg, COD int
Fragile bool
}
type ParcelBuilder struct{ p Parcel }
func NewParcel(id, from, to string) *ParcelBuilder {
return &ParcelBuilder{Parcel{ID: id, From: from, To: to, Kg: 1}}
}
func (b *ParcelBuilder) Weight(kg int) *ParcelBuilder { b.p.Kg = kg; return b }
func (b *ParcelBuilder) CashOnDelivery(tk int) *ParcelBuilder { b.p.COD = tk; return b }
func (b *ParcelBuilder) Fragile() *ParcelBuilder { b.p.Fragile = true; return b }
// Build checks the whole object once, so a half-valid parcel can never exist.
func (b *ParcelBuilder) Build() (Parcel, error) {
if b.p.Kg < 1 || b.p.Kg > 30 {
return Parcel{}, errors.New("weight must be 1-30 kg")
}
if b.p.From == b.p.To {
return Parcel{}, errors.New("from and to are the same city")
}
return b.p, nil
}
func (p Parcel) String() string {
parts := []string{fmt.Sprintf("%s -> %s, %d kg", p.From, p.To, p.Kg)}
if p.COD > 0 {
parts = append(parts, fmt.Sprintf("cod %d tk", p.COD))
}
if p.Fragile {
parts = append(parts, "fragile")
}
return strings.Join(parts, ", ")
}
func main() {
for _, ch := range []string{"sms", "email", "push"} {
s, err := NewSender(ch)
if err != nil {
fmt.Printf("%-6s : %v\n", ch, err)
continue
}
to := map[string]string{"sms": "01711000000", "email": "rahim@example.com"}[ch]
fmt.Println(s.Send(to, "Your OTP is 4821"))
}
builders := []*ParcelBuilder{
NewParcel("P-1", "Dhaka", "Sylhet").Weight(2).CashOnDelivery(1500).Fragile(),
NewParcel("P-2", "Dhaka", "Khulna"),
NewParcel("P-3", "Dhaka", "Rajshahi").Weight(45),
NewParcel("P-4", "Dhaka", "Dhaka").Weight(3),
}
for _, b := range builders {
p, err := b.Build()
if err != nil {
fmt.Printf("parcel %s: refused: %v\n", b.p.ID, err)
continue
}
fmt.Printf("parcel %s: %s\n", p.ID, p)
}
}from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Protocol
# ---- Factory: callers ask for "a sender for this channel" and never name the concrete class.
class Sender(Protocol):
def send(self, to: str, msg: str) -> str: ...
class SMS:
def send(self, to: str, msg: str) -> str:
return f"sms -> {to}: {msg}"
class Email:
def send(self, to: str, msg: str) -> str:
return f"email -> {to}: {msg}"
def new_sender(channel: str) -> Sender:
"""The one place that knows which class serves which channel."""
senders: dict[str, type[Sender]] = {"sms": SMS, "email": Email}
if channel not in senders:
raise ValueError(f'unknown channel "{channel}"')
return senders[channel]()
# ---- Builder: a parcel has a few required fields and several optional ones.
@dataclass(frozen=True)
class Parcel:
id: str
frm: str
to: str
kg: int = 1
cod: int = 0
fragile: bool = False
def __str__(self) -> str:
parts = [f"{self.frm} -> {self.to}, {self.kg} kg"]
if self.cod:
parts.append(f"cod {self.cod} tk")
if self.fragile:
parts.append("fragile")
return ", ".join(parts)
class ParcelBuilder:
def __init__(self, id: str, frm: str, to: str):
self.p = Parcel(id, frm, to)
def weight(self, kg: int) -> ParcelBuilder:
self.p = replace(self.p, kg=kg)
return self
def cash_on_delivery(self, tk: int) -> ParcelBuilder:
self.p = replace(self.p, cod=tk)
return self
def fragile(self) -> ParcelBuilder:
self.p = replace(self.p, fragile=True)
return self
def build(self) -> Parcel:
"""Checks the whole object once, so a half-valid parcel can never exist."""
if not 1 <= self.p.kg <= 30:
raise ValueError("weight must be 1-30 kg")
if self.p.frm == self.p.to:
raise ValueError("from and to are the same city")
return self.p
addresses = {"sms": "01711000000", "email": "rahim@example.com"}
for ch in ("sms", "email", "push"):
try:
print(new_sender(ch).send(addresses.get(ch, ""), "Your OTP is 4821"))
except ValueError as e:
print(f"{ch:<6} : {e}")
builders = [
ParcelBuilder("P-1", "Dhaka", "Sylhet").weight(2).cash_on_delivery(1500).fragile(),
ParcelBuilder("P-2", "Dhaka", "Khulna"),
ParcelBuilder("P-3", "Dhaka", "Rajshahi").weight(45),
ParcelBuilder("P-4", "Dhaka", "Dhaka").weight(3),
]
for b in builders:
try:
p = b.build()
print(f"parcel {p.id}: {p}")
except ValueError as e:
print(f"parcel {b.p.id}: refused: {e}")// ---- Factory: callers ask for "a sender for this channel" and never name the concrete class.
interface Sender {
send(to: string, msg: string): string;
}
class SMS implements Sender {
send(to: string, msg: string): string {
return `sms -> ${to}: ${msg}`;
}
}
class Email implements Sender {
send(to: string, msg: string): string {
return `email -> ${to}: ${msg}`;
}
}
// The one place that knows which class serves which channel.
function newSender(channel: string): Sender {
const senders: Record<string, () => Sender> = { sms: () => new SMS(), email: () => new Email() };
const make = senders[channel];
if (!make) throw new Error(`unknown channel "${channel}"`);
return make();
}
// ---- Builder: a parcel has a few required fields and several optional ones.
interface Parcel {
readonly id: string;
readonly from: string;
readonly to: string;
readonly kg: number;
readonly cod: number;
readonly fragile: boolean;
}
class ParcelBuilder {
readonly p: { -readonly [K in keyof Parcel]: Parcel[K] };
constructor(id: string, from: string, to: string) {
this.p = { id, from, to, kg: 1, cod: 0, fragile: false };
}
weight(kg: number): this {
this.p.kg = kg;
return this;
}
cashOnDelivery(tk: number): this {
this.p.cod = tk;
return this;
}
fragile(): this {
this.p.fragile = true;
return this;
}
// Checks the whole object once, so a half-valid parcel can never exist.
build(): Parcel {
if (this.p.kg < 1 || this.p.kg > 30) throw new Error("weight must be 1-30 kg");
if (this.p.from === this.p.to) throw new Error("from and to are the same city");
return Object.freeze({ ...this.p });
}
}
function describe(p: Parcel): string {
const parts = [`${p.from} -> ${p.to}, ${p.kg} kg`];
if (p.cod > 0) parts.push(`cod ${p.cod} tk`);
if (p.fragile) parts.push("fragile");
return parts.join(", ");
}
const addresses: Record<string, string> = { sms: "01711000000", email: "rahim@example.com" };
for (const ch of ["sms", "email", "push"]) {
try {
console.log(newSender(ch).send(addresses[ch] ?? "", "Your OTP is 4821"));
} catch (e) {
console.log(`${ch.padEnd(6)} : ${(e as Error).message}`);
}
}
const builders = [
new ParcelBuilder("P-1", "Dhaka", "Sylhet").weight(2).cashOnDelivery(1500).fragile(),
new ParcelBuilder("P-2", "Dhaka", "Khulna"),
new ParcelBuilder("P-3", "Dhaka", "Rajshahi").weight(45),
new ParcelBuilder("P-4", "Dhaka", "Dhaka").weight(3),
];
for (const b of builders) {
try {
const p = b.build();
console.log(`parcel ${p.id}: ${describe(p)}`);
} catch (e) {
console.log(`parcel ${b.p.id}: refused: ${(e as Error).message}`);
}
}use std::fmt;
// ---- Factory: callers ask for "a sender for this channel" and never name the concrete type.
trait Sender { fn send(&self, to: &str, msg: &str) -> String; }
struct Sms;
impl Sender for Sms { fn send(&self, to: &str, msg: &str) -> String { format!("sms -> {to}: {msg}") } }
struct Email;
impl Sender for Email { fn send(&self, to: &str, msg: &str) -> String { format!("email -> {to}: {msg}") } }
// The one place that knows which type serves which channel.
fn new_sender(channel: &str) -> Result<Box<dyn Sender>, String> {
match channel {
"sms" => Ok(Box::new(Sms)),
"email" => Ok(Box::new(Email)),
_ => Err(format!("unknown channel \"{channel}\"")),
}
}
// ---- Builder: a parcel has a few required fields and several optional ones.
#[derive(Clone)]
struct Parcel { id: String, from: String, to: String, kg: u32, cod: u32, fragile: bool }
impl fmt::Display for Parcel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} -> {}, {} kg", self.from, self.to, self.kg)?;
if self.cod > 0 { write!(f, ", cod {} tk", self.cod)?; }
if self.fragile { write!(f, ", fragile")?; }
Ok(())
}
}
// Rust builders usually take `self` by value, so each step moves the builder along.
struct ParcelBuilder { p: Parcel }
impl ParcelBuilder {
fn new(id: &str, from: &str, to: &str) -> Self {
ParcelBuilder { p: Parcel { id: id.into(), from: from.into(), to: to.into(), kg: 1, cod: 0, fragile: false } }
}
fn weight(mut self, kg: u32) -> Self { self.p.kg = kg; self }
fn cash_on_delivery(mut self, tk: u32) -> Self { self.p.cod = tk; self }
fn fragile(mut self) -> Self { self.p.fragile = true; self }
// Checks the whole object once, so a half-valid parcel can never exist.
fn build(&self) -> Result<Parcel, String> {
if !(1..=30).contains(&self.p.kg) { return Err("weight must be 1-30 kg".into()); }
if self.p.from == self.p.to { return Err("from and to are the same city".into()); }
Ok(self.p.clone())
}
}
fn main() {
for ch in ["sms", "email", "push"] {
let to = match ch { "sms" => "01711000000", "email" => "rahim@example.com", _ => "" };
match new_sender(ch) {
Ok(s) => println!("{}", s.send(to, "Your OTP is 4821")),
Err(e) => println!("{ch:<6} : {e}"),
}
}
let builders = [
ParcelBuilder::new("P-1", "Dhaka", "Sylhet").weight(2).cash_on_delivery(1500).fragile(),
ParcelBuilder::new("P-2", "Dhaka", "Khulna"),
ParcelBuilder::new("P-3", "Dhaka", "Rajshahi").weight(45),
ParcelBuilder::new("P-4", "Dhaka", "Dhaka").weight(3),
];
for b in &builders {
match b.build() {
Ok(p) => println!("parcel {}: {p}", p.id),
Err(e) => println!("parcel {}: refused: {e}", b.p.id),
}
}
}import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public class Main {
// ---- Factory: callers ask for "a sender for this channel" and never name the concrete class.
interface Sender { String send(String to, String msg); }
static final class Sms implements Sender {
public String send(String to, String msg) { return "sms -> " + to + ": " + msg; }
}
static final class Email implements Sender {
public String send(String to, String msg) { return "email -> " + to + ": " + msg; }
}
// The one place that knows which class serves which channel.
static Sender newSender(String channel) {
Map<String, Supplier<Sender>> senders = Map.of("sms", Sms::new, "email", Email::new);
Supplier<Sender> make = senders.get(channel);
if (make == null) throw new IllegalArgumentException("unknown channel \"" + channel + "\"");
return make.get();
}
// ---- Builder: a parcel has a few required fields and several optional ones.
record Parcel(String id, String from, String to, int kg, int cod, boolean fragile) {
@Override public String toString() {
List<String> parts = new ArrayList<>(List.of(from + " -> " + to + ", " + kg + " kg"));
if (cod > 0) parts.add("cod " + cod + " tk");
if (fragile) parts.add("fragile");
return String.join(", ", parts);
}
}
static final class ParcelBuilder {
final String id, from, to;
private int kg = 1, cod = 0;
private boolean fragile = false;
ParcelBuilder(String id, String from, String to) { this.id = id; this.from = from; this.to = to; }
ParcelBuilder weight(int kg) { this.kg = kg; return this; }
ParcelBuilder cashOnDelivery(int tk) { this.cod = tk; return this; }
ParcelBuilder fragile() { this.fragile = true; return this; }
// Checks the whole object once, so a half-valid parcel can never exist.
Parcel build() {
if (kg < 1 || kg > 30) throw new IllegalArgumentException("weight must be 1-30 kg");
if (from.equals(to)) throw new IllegalArgumentException("from and to are the same city");
return new Parcel(id, from, to, kg, cod, fragile);
}
}
public static void main(String[] args) {
Map<String, String> addresses = Map.of("sms", "01711000000", "email", "rahim@example.com");
for (String ch : List.of("sms", "email", "push")) {
try {
System.out.println(newSender(ch).send(addresses.getOrDefault(ch, ""), "Your OTP is 4821"));
} catch (IllegalArgumentException e) {
System.out.printf("%-6s : %s%n", ch, e.getMessage());
}
}
List<ParcelBuilder> builders = List.of(
new ParcelBuilder("P-1", "Dhaka", "Sylhet").weight(2).cashOnDelivery(1500).fragile(),
new ParcelBuilder("P-2", "Dhaka", "Khulna"),
new ParcelBuilder("P-3", "Dhaka", "Rajshahi").weight(45),
new ParcelBuilder("P-4", "Dhaka", "Dhaka").weight(3));
for (ParcelBuilder b : builders) {
try {
Parcel p = b.build();
System.out.printf("parcel %s: %s%n", p.id(), p);
} catch (IllegalArgumentException e) {
System.out.printf("parcel %s: refused: %s%n", b.id, e.getMessage());
}
}
}
}#include <cstdio>
#include <functional>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
// ---- Factory: callers ask for "a sender for this channel" and never name the concrete class.
class Sender {
public:
virtual ~Sender() = default;
virtual std::string send(const std::string& to, const std::string& msg) const = 0;
};
class Sms : public Sender {
public:
std::string send(const std::string& to, const std::string& msg) const override {
return "sms -> " + to + ": " + msg;
}
};
class Email : public Sender {
public:
std::string send(const std::string& to, const std::string& msg) const override {
return "email -> " + to + ": " + msg;
}
};
// The one place that knows which class serves which channel.
std::unique_ptr<Sender> newSender(const std::string& channel) {
static const std::map<std::string, std::function<std::unique_ptr<Sender>()>> senders = {
{"sms", [] { return std::make_unique<Sms>(); }},
{"email", [] { return std::make_unique<Email>(); }},
};
auto it = senders.find(channel);
if (it == senders.end()) throw std::invalid_argument("unknown channel \"" + channel + "\"");
return it->second();
}
// ---- Builder: a parcel has a few required fields and several optional ones.
struct Parcel {
std::string id, from, to;
int kg = 1, cod = 0;
bool fragile = false;
std::string str() const {
std::string s = from + " -> " + to + ", " + std::to_string(kg) + " kg";
if (cod > 0) s += ", cod " + std::to_string(cod) + " tk";
if (fragile) s += ", fragile";
return s;
}
};
class ParcelBuilder {
public:
ParcelBuilder(std::string id, std::string from, std::string to) : p_{std::move(id), std::move(from), std::move(to)} {}
ParcelBuilder& weight(int kg) { p_.kg = kg; return *this; }
ParcelBuilder& cashOnDelivery(int tk) { p_.cod = tk; return *this; }
ParcelBuilder& fragile() { p_.fragile = true; return *this; }
// Checks the whole object once, so a half-valid parcel can never exist.
Parcel build() const {
if (p_.kg < 1 || p_.kg > 30) throw std::invalid_argument("weight must be 1-30 kg");
if (p_.from == p_.to) throw std::invalid_argument("from and to are the same city");
return p_;
}
const std::string& id() const { return p_.id; }
private:
Parcel p_;
};
int main() {
std::map<std::string, std::string> addresses{{"sms", "01711000000"}, {"email", "rahim@example.com"}};
for (std::string ch : {"sms", "email", "push"}) {
try {
std::printf("%s\n", newSender(ch)->send(addresses[ch], "Your OTP is 4821").c_str());
} catch (const std::invalid_argument& e) {
std::printf("%-6s : %s\n", ch.c_str(), e.what());
}
}
std::vector<ParcelBuilder> builders{
ParcelBuilder("P-1", "Dhaka", "Sylhet").weight(2).cashOnDelivery(1500).fragile(),
ParcelBuilder("P-2", "Dhaka", "Khulna"),
ParcelBuilder("P-3", "Dhaka", "Rajshahi").weight(45),
ParcelBuilder("P-4", "Dhaka", "Dhaka").weight(3),
};
for (const auto& b : builders) {
try {
Parcel p = b.build();
std::printf("parcel %s: %s\n", p.id.c_str(), p.str().c_str());
} catch (const std::invalid_argument& e) {
std::printf("parcel %s: refused: %s\n", b.id().c_str(), e.what());
}
}
}ছয়টাই হুবহু এটা print করে:
sms -> 01711000000: Your OTP is 4821
email -> rahim@example.com: Your OTP is 4821
push : unknown channel "push"
parcel P-1: Dhaka -> Sylhet, 2 kg, cod 1500 tk, fragile
parcel P-2: Dhaka -> Khulna, 1 kg
parcel P-3: refused: weight must be 1-30 kg
parcel P-4: refused: from and to are the same city
Output টা পড়ি:
- sms / email: loop টা কোথাও কোনো concrete type এর নাম নেয় না। একটা string দেয়, আর এমন কিছু পায় যে
Sendকরতে পারে। - push: অচেনা channel factory এক জায়গাতেই পরিষ্কার error দিয়ে ফিরিয়ে দেয়, প্রতিটা caller কে আলাদা করে check করতে হয় না।
- P-1: প্রতিটা optional setting call site এ নাম সহ লেখা:
.Weight(2).CashOnDelivery(1500).Fragile()। কোনো রহস্যময়trueনেই। - P-2: optional কিছুই দেওয়া হয়নি, তাই default গুলো বসেছে (১ kg, COD নেই, fragile না)।
- P-3 আর P-4: ৪৫ kg এর parcel আর যে parcel কোথাও যাচ্ছে না, দুটোকেই
Buildফিরিয়ে দেয়। ভুল কোনোParcelobject কখনো তৈরিই হয়নি। - Language ভেদে কী আলাদা: Python এর factory হলো নাম থেকে class এর একটা dict; অনেক Python team builder এর বদলে keyword argument আর
__post_init__এ একটা check ব্যবহার করতো। TypeScript এর builder method গুলোthisফেরত দেয়, তাই chain করলেও type ঠিক থাকে। Rust এর builder method গুলোmut selfনেয় আরSelfফেরত দেয়, প্রতিটা ধাপে builder টাকে সরিয়ে নিয়ে যায়। Java রMap.of("sms", Sms::new, ...)constructor এর reference রাখে, আর C++ রাখে এমন lambda যেটাunique_ptr<Sender>ফেরত দেয়, তাই factory মালিকানা caller এর হাতে তুলে দেয়।
Real project এ object বানানো
- সবকিছু জোড়া লাগাও
mainএ (composition root)। Config, DB pool, sender আর service গুলো শুরুতে একবার বানাও, তারপর বাইরে থেকে দাও। এতে বেশিরভাগ Singleton এর আর দরকার পড়ে না, আর পুরো dependency র ছবি এক function এই দেখা যায়। - রকমভেদ বাড়লে factory কে data দিয়ে চালাও। নাম থেকে constructor এর একটা map (Python আর Java version এর মতো) থাকলে এক লাইনে নতুন channel যোগ করা যায়, এমনকি plugin থেকেও register করা যায়।
- Check করো builder এ, প্রতিটা caller এ না। কোনো rule ("weight ১-৩০ kg") যদি সবসময় মানতেই হয়,
Buildই হলো সেই দরজা। - বানানো object টা immutable রাখো।
Buildসফল হওয়ার পরে parcel আর edit করা যাওয়া উচিত না; নইলে কেউ পরেKg = 45বসিয়ে check টা এড়িয়ে যাবে। - সত্যিই Singleton লাগলে (যেমন পুরো process এর একটা registry), সেটা একটা interface এর পেছনে রাখো, একবার পরিষ্কারভাবে initialise করো, আর test এর জন্য বদলানোর একটা রাস্তা রাখো।
যে design গুলো পরে problem create করে
যে creation code টা পরে ভোগাবে
s := NewSmsSender(key, "BD", 3, true)
একটু থেমে ভাবো
একজন teammate বললো: "App এর ঠিক একটা database connection pool লাগবে, তাই DB কে DB.getInstance() সহ Singleton বানাই।" তুমি কী বলবে?
উত্তর দেখো
Pool টা main এ একবার বানাও, আর যে repository গুলোর লাগে তাদের বাইরে থেকে দাও। Production এ তবুও ঠিক একটাই pool থাকে, কিন্তু এখন প্রতিটা class এর constructor দেখেই বোঝা যায় সে database ব্যবহার করে, test নকল বা test database দিতে পারে, আর দুইটা database এর জন্য দুইটা pool (ধরো একটা read replica) খুবই সহজ। "একটাই instance" এর দাবিটা program কীভাবে জোড়া লাগানো তার ব্যাপার, class কে নিজে সেটা চাপিয়ে দিতে হয় না।
ছোট্ট quiz
১. Factory কোন সমস্যা মেটায়?
উত্তর
"এই input এর জন্য কোন concrete type?" এই সিদ্ধান্ত এক জায়গায় রাখে, তাই caller রা শুধু interface এর উপর নির্ভর করে, আর নতুন রকম আসলে তাদের গায়ে হাত পড়ে না।
২. Setter method গুলোতে না, Build() এ check কেন?
উত্তর
কিছু rule কয়েকটা field একসাথে দেখে (from আর to আলাদা হতে হবে), আর ভরার সময় builder অর্ধেক বানানো থাকতেই পারে। শেষে একবার check করলে নিশ্চিত হয় যে যত object আছে, সব ঠিক।
৩. Factory আর abstract factory এর পার্থক্য কী?
উত্তর
Factory এক রকম জিনিস বানায় (একটা sender)। Abstract factory বানায় এমন সম্পর্কিত জিনিসের একটা পরিবার যাদের মিলতে হবে (একটা theme এর button, icon আর font)।
৪. Singleton সাধারণত শেষ পছন্দ কেন?
উত্তর
এটা global state: dependency লুকায়, code কে একটা concrete class এর সাথে বেঁধে দেয়, আর test এর মধ্যে state ছড়ায়। শুরুতে একটা instance বানিয়ে বাইরে থেকে দিলে এই দাম ছাড়াই "একটাই" পাওয়া যায়।
তোমার challenge: WhatsApp OTP আর insurance
- একটা WhatsApp sender যোগ করো যেটা print করবে
whatsapp -> <to>: <msg>, আর factory তে"whatsapp"নামে register করো। Channel এর list বদলে করোsms, email, whatsapp, push, WhatsApp এর address01811000000। - Builder এ একটা optional insurance ধাপ যোগ করো,
.Insured(), parcel এর লাইনের শেষেinsuredহিসেবে দেখাবে।Buildএ একটা rule যোগ করো: insured parcel এর COD কমপক্ষে ৫০০ টাকা হতে হবে। P-1 কে insured করো।
চালানোর আগে আন্দাজ করো নতুন লাইন গুলো কী আসবে।
হয়েছে বুঝবে যখন output এ যোগ হবে whatsapp -> 01811000000: Your OTP is 4821, P-1 হবে parcel P-1: Dhaka -> Sylhet, 2 kg, cod 1500 tk, fragile, insured, অন্য সব লাইন একই থাকবে, আর পাঠানোর loop এ নতুন কোনো if লাগবে না।
এক কথায়
Object বানানো রাখো অল্প কয়েকটা জায়গায়: factory ঠিক করে কোন concrete type বানাবে, builder নাম ধরে optional setting জড়ো করে আর পুরো object একবার check করে, আর
mainএকক instance গুলো জোড়া লাগায়। বেশিরভাগ code শুধু object ব্যবহার করবে; Singleton হলো শেষ উপায়, প্রথম না।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →