Part 01 · Low Level Design
Creational Patterns: Factory, Builder, Singleton
Hide which class gets built, tame constructors with ten options, and why Singleton is the pattern you reach for last.
⏱ 2-3 hours · Station 7 of 80
new in forty places
A courier company's app sends OTPs. Forty places in the code do sender = new SmsSender(apiKey, "BD", 3, true). Then the company adds email OTPs for corporate clients, and a different SMS provider for Grameenphone numbers. Forty places now need an if, and each one gets the four constructor arguments slightly differently.
The same app books parcels with a constructor that takes eleven arguments: new Parcel("P-1", "Dhaka", "Sylhet", 2, 1500, true, false, null, 0, "", false). Nobody remembers which true means fragile. A parcel with 45 kg gets created, sent to a van, and rejected at the hub.
Both bugs come from the same place: how objects get made. Creational patterns are a few standard answers to that question.
The problem: creation spreads everywhere
| Symptom | Pattern that helps |
|---|---|
if channel == "sms" { new X } else { new Y } repeated across the code | Factory |
A constructor with many optional arguments, called with null, 0, false | Builder |
| Objects that can exist in an invalid, half-filled state | Builder (validate once in Build) |
| "We need exactly one config / one connection pool" | Usually dependency injection, sometimes Singleton |
| Families of objects that must match (Bangla UI widgets with Bangla fonts) | Abstract Factory |
Think of ordering at a tea stall and a tailor
At a tea stall you say "one milk tea". You don't tell the stall owner which kettle, which cup or which supplier's milk. The stall knows how to make each item on the board. That's a factory: you name what you want; it decides which concrete thing to make.
At a tailor, a shirt has a few required things (size, cloth) and many optional ones (pocket, collar type, cuffs, monogram). The tailor writes your choices on a slip one by one, then checks it all makes sense ("no French cuffs on a half-sleeve") before cutting cloth. That's a builder: collect settings step by step, validate once, then produce the finished object.
Where the comparison breaks down. A tailor can phone you about a problem. A builder can only refuse with an error, so its error messages must be clear enough to fix the call without reading its code.
NewSender(channel) is the only place that turns 'sms' or 'email' into an object. The company adds WhatsApp OTPs. How many call sites that send OTPs need to change?
Four ways to create objects well
- Factory (factory method or simple factory): one function or method decides which concrete type to create, from some input. Callers depend on the interface and the factory, never on concrete classes. The
switchon type still exists, but once, in the factory. - Abstract Factory: a factory for a family of related objects that must match:
BanglaThememakes Bangla buttons, Bangla date pickers and a Bangla font;EnglishThememakes the English set. You pick the family once and can't accidentally mix them. - Builder: separates collecting settings from making the object. Required fields go in the constructor, optional ones as named steps, and
Build()validates the whole thing once. The result: readable calls (.Fragile()instead of a baretrue) and no half-valid objects. In Python, keyword arguments with defaults often do the same job; in Rust, builders usually takeselfby value. - Singleton: guarantees one instance of a class, usually with a global access point. It solves a real need (one connection pool, one config) but causes real pain: hidden dependencies, shared state between tests, hard-to-replace collaborators. Reach for it last. Creating one instance in
mainand passing it in gives the "only one" property without the global. - The goal of all of them is the same: keep the decision "which concrete thing, with which settings, is it valid?" in one place, so the rest of the code just uses objects.
Building a parcel, step by step
NewParcel("P-3", Dhaka, Rajshahi).Weight(45).Build()
Step 1/4 Required fields first
The constructor takes what every parcel must have: ID, from, to. Weight defaults to 1 kg.
- id
- P-3
- from
- Dhaka
- to
- Rajshahi
- kg
- 1
Who decides what gets made?
Draw a line between using objects and making them:
- Most of your code should only use objects through interfaces.
- A few small places make them: factories, builders, and
main, which wires everything together at start-up.
If you see new ConcreteThing(...) deep inside business logic, ask: should this be a factory call, or a dependency passed in?
Try it: which creational pattern fits?
Pick the pattern
gateway := ??? ("BD")Senders and parcels in code
A factory that turns a channel name into a Sender, and a builder for parcels that validates the whole parcel in Build().
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());
}
}
}All six print exactly this:
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
Reading the output:
- sms / email: the loop never names a concrete type. It passes a string and gets something that can
Send. - push: the factory refuses unknown channels with a clear error, in one place, instead of each caller checking.
- P-1: every optional setting is named at the call site:
.Weight(2).CashOnDelivery(1500).Fragile(). No mysterytrue. - P-2: nothing optional set, so the defaults apply (1 kg, no COD, not fragile).
- P-3 and P-4:
Buildrefuses a 45 kg parcel and a parcel going nowhere. No invalidParcelobject was ever created. - What each language changed: Python's factory is a dict from name to class; many Python teams would replace the builder with keyword arguments plus a check in
__post_init__. TypeScript's builder methods returnthis, so chaining keeps the type. Rust's builder methods takemut selfand returnSelf, moving the builder through each step. Java'sMap.of("sms", Sms::new, ...)stores constructor references, and C++ stores lambdas that returnunique_ptr<Sender>, so the factory hands ownership to the caller.
Creating objects in real projects
- Wire everything in
main(the composition root). Create the config, the DB pool, the senders and the services once at start-up and pass them in. This replaces most Singletons and makes the dependency graph visible in one function. - Make factories data-driven when variants grow. A map from name to constructor (as in the Python and Java versions) lets you add a channel with one line, and even register channels from plugins.
- Validate in the builder, not in every caller. If a rule ("weight 1-30 kg") must always hold,
Buildis the gate. - Prefer immutability for the built object. Once
Buildsucceeds, the parcel shouldn't be editable; otherwise someone can setKg = 45afterwards and skip the check. - If you truly need a Singleton (a process-wide registry, for example), keep it behind an interface, initialise it once explicitly, and give tests a way to replace it.
Designs that cause problems later
Pick the creation code that will hurt
s := NewSmsSender(key, "BD", 3, true)
Pause and think
A teammate says: "The app needs exactly one database connection pool, so let's make DB a Singleton with DB.getInstance()." What would you suggest?
Reveal
Create the pool once in main and pass it to the repositories that need it. You still get exactly one pool in production, but now every class shows in its constructor that it uses the database, tests can pass a fake or a test database, and two pools for two databases (say, a read replica) are trivial. The "one instance" requirement is about how the program is wired, not something the class itself has to enforce.
Quick quiz
1. What problem does a factory solve?
Answer
It keeps the decision "which concrete type for this input?" in one place, so callers depend only on the interface and new variants don't touch them.
2. Why validate in Build() instead of in the setter methods?
Answer
Some rules involve several fields together (from must differ from to), and a builder is allowed to be incomplete while it's being filled. Checking once at the end guarantees that every object that exists is valid.
3. What's the difference between a factory and an abstract factory?
Answer
A factory creates one kind of product (a sender). An abstract factory creates a family of related products that must match (a theme's buttons, icons and fonts).
4. Why is Singleton usually the last choice?
Answer
It's global state: it hides dependencies, ties code to one concrete class, and leaks state between tests. Creating one instance at start-up and passing it in gives the same "only one" without those costs.
Your challenge: WhatsApp OTP and insurance
- Add a WhatsApp sender that prints
whatsapp -> <to>: <msg>and register it in the factory under"whatsapp". Change the channel list tosms, email, whatsapp, push, with01811000000as the WhatsApp address. - Add an optional insurance step to the builder,
.Insured(), shown asinsuredat the end of the parcel line. Add a rule toBuild: an insured parcel must declare COD of at least 500 taka. Make P-1 insured.
Before you run it, predict the new lines.
You've succeeded when the output gains whatsapp -> 01811000000: Your OTP is 4821, P-1 becomes parcel P-1: Dhaka -> Sylhet, 2 kg, cod 1500 tk, fragile, insured, every other line is unchanged, and the sending loop has no new if.
In one line
Keep creation in a few places: a factory decides which concrete type to make, a builder collects optional settings by name and validates the whole object once, and
mainwires the single instances together. Most code should only use objects; Singleton is the last resort, not the first.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →