Part 01 · Low Level Design

Creational Patterns: Factory, Builder, Singleton

FactoryAbstract FactoryBuilderSingleton

কোন 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 করো, তারপর পুরো জিনিসটা বানাও।

factorybuildersendOTP()asks for "sms"NewSender()knows the typesSMSEmail"sms"NewParcel(id, from, to).Weight(2).CashOnDelivery(1500).Fragile()Build()checks every ruleParcel P-1finished, validbuilderonly a valid parcel leaves Build()
Play চাপো। বাম দিকে: caller শুধু বলে "sms", আর NewSender SMS type টা বেছে নেয়। ডান দিকে: optional setting গুলো একটা একটা করে যোগ হয়, আর Build পুরো parcel টা একবার check করে, তারপরই একটা শেষ, ঠিকঠাক Parcel বের হয়।

তবে একটা জায়গায় মেলে না। দর্জি কোনো সমস্যা হলে তোমাকে phone করতে পারে। Builder শুধু একটা error দিয়ে ফিরিয়ে দিতে পারে, তাই তার error message এমন পরিষ্কার হতে হবে যাতে তার code না পড়েই call টা ঠিক করা যায়।

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

NewSender(channel) ই একমাত্র জায়গা যেটা 'sms' বা 'email' কে object বানায়। Company WhatsApp এ OTP আনলো। OTP পাঠায় এমন কয়টা call site বদলাতে হবে?

Object বানানোর চারটা ভালো উপায়

  1. Factory (factory method বা simple factory): একটা function বা method কোনো input দেখে ঠিক করে কোন concrete type বানাবে। Caller রা নির্ভর করে interface আর factory এর উপর, কখনো concrete class এর উপর না। Type দেখে switch টা থাকে ঠিকই, কিন্তু একবার, factory এর ভেতরে।
  2. Abstract Factory: এমন সম্পর্কিত object এর একটা পরিবারের জন্য factory, যাদের একে অপরের সাথে মিলতে হবে: BanglaTheme বানায় বাংলা button, বাংলা date picker আর বাংলা font; EnglishTheme বানায় English এর set টা। পরিবার একবারই বাছো, আর ভুল করে মিশিয়ে ফেলার উপায় থাকে না।
  3. Builder: setting জড়ো করা আর object বানানো আলাদা করে। required field যায় constructor এ, optional গুলো নাম দেওয়া ধাপ হিসেবে, আর Build() পুরো জিনিসটা একবার check করে। ফল: পড়া যায় এমন call (খালি true এর বদলে .Fragile()), আর কোনো অর্ধেক-ঠিক object নেই। Python এ default সহ keyword argument প্রায়ই একই কাজ করে; Rust এ builder সাধারণত self কে value হিসেবে নেয়।
  4. Singleton: একটা class এর একটাই instance নিশ্চিত করে, সাধারণত একটা global access point সহ। সত্যিকারের একটা দরকার মেটায় (একটা connection pool, একটা config), কিন্তু সত্যিকারের ভোগান্তিও আনে: লুকানো dependency, test এর মধ্যে shared state, বদলানো কঠিন এমন সহযোগী। এটা ধরো সবার শেষে। main এ একটা instance বানিয়ে বাইরে থেকে দিলে global ছাড়াই "একটাই" ব্যাপারটা পাওয়া যায়।
  5. সবগুলোর উদ্দেশ্য একটাই: "কোন concrete জিনিস, কোন setting এ, ঠিক আছে কিনা" এই সিদ্ধান্ত এক জায়গায় রাখা, যাতে বাকি code শুধু object ব্যবহার করে।

একটা parcel বানানো, ধাপে ধাপে

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

NewParcel("P-3", Dhaka, Rajshahi).Weight(45).Build()

CallerParcelBuilderBuild এর checkParcel

ধাপ 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 খাপ খায়?

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

Pattern টা বাছো

gateway := ??? ("BD")
কাজ করে: Factory। Input (দেশ) ঠিক করে concrete type; caller রা Gateway interface ব্যবহার করে।

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)
	}
}

ছয়টাই হুবহু এটা 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 ফিরিয়ে দেয়। ভুল কোনো Parcel object কখনো তৈরিই হয়নি।
  • 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 করে

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

যে creation code টা পরে ভোগাবে

s := NewSmsSender(key, "BD", 3, true)
ভাঙে: প্রতিটা নতুন channel বা argument এ ৪০ জায়গায় edit। Object বানানো এক factory তে রাখো।

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

একজন 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 এর address 01811000000।
  • 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 পাও →