Builder Pattern: একটু একটু করে বানাও, ওস্তাদ দর্জির মতো
ঢাকার একটা দর্জির দোকানের গল্পের মাধ্যমে Builder design pattern শেখো — fluent TypeScript ও C# কোড, telescoping constructor এর সমস্যা, আর practice টাস্কসহ।
ইকবাল চাচার দর্জির দোকানের গল্প
চলো, ঢাকার একটা ছোট দর্জির দোকানে ঘুরে আসি।
ইকবাল চাচা একজন ওস্তাদ দর্জি। তার দোকান, নূর টেইলার্স, পাঞ্জাবির জন্য বিখ্যাত। তার নাতি তারিক, কলেজের ছাত্র, সপ্তাহান্তে কাউন্টারে সাহায্য করে। মজার বিষয় হলো: কোনো দুই কাস্টমার একই পাঞ্জাবি চায় না। রহিম ভাই চায় প্রতিদিন পরার জন্য সাদামাটা সাদা সুতির পাঞ্জাবি। জামাল সাহেব চায় তার ছেলের বিয়ের জন্য চিকনকারি কাজ করা সিল্কের পাঞ্জাবি। কাছের স্কুলের প্রধান শিক্ষিকা নাসরিন ম্যাডাম অর্ডার দেন দুইশো ইউনিফর্ম পাঞ্জাবি, সব একইরকম।
ইকবাল চাচা কীভাবে অর্ডার নেন? তিনি কি কাস্টমারকে বলেন একনিঃশ্বাসে নির্দিষ্ট ক্রমে সব বলতে — "সুতি-সাদা-সাইজ৪০-ব্যান্ডকলার-ফুলস্লিভ-দুইপকেট-কাঠেরবোতাম-কোনোকাজনেই!"? অবশ্যই না। এভাবে অর্ডার দেওয়া অসম্ভব, আর এরকম অর্ডারে ভুল ধরাও অসম্ভব।
বরং তিনি একটা সহজ অর্ডার স্লিপ রাখেন। বন্ধুত্বপূর্ণ কথোপকথনে একটু একটু করে পূরণ করেন:
- "কোন কাপড়?" — সুতি।
- "রং?" — সাদা।
- "সাইজ?" — ৪০।
- "কলার?" — ব্যান্ড কলার।
- "পকেট?" — একটা, বাম দিকে।
- "কাজ?" — না, সাদামাটা।
কিছু প্রশ্ন বাদ দেওয়া যায় — তখন চাচা নিজেই sensible default ব্যবহার করেন (full sleeve, standard বোতাম)। যেকোনো ক্রমে অর্ডার দেওয়া যায়। আর একদম শেষে, যখন কাস্টমার বলেন "এইটুকুই", তখন চাচা স্লিপটা একবার পড়েন, চেক করেন কোনো গুরুত্বপূর্ণ কিছু miss হয়নি তো — কাপড় আর সাইজ অবশ্যই লাগবে! — তারপর final পাঞ্জাবি সেলাই করেন। একবার সেলাই হয়ে গেলে পাঞ্জাবি আর বদলায় না। কিছু আলাদা চাইলে নতুন অর্ডার দিতে হবে।
আরও একটা চালাক ব্যাপার: নাসরিন ম্যাডামের ইউনিফর্ম অর্ডারের জন্য চাচা দুইশো বার প্রশ্ন করেন না। তার কাছে একটা রেডি-মেড রেসিপি কার্ড আছে: "স্কুল ইউনিফর্ম = সাদা সুতি, ব্যান্ড কলার, হাফ স্লিভ, এক পকেট।" তিনি শুধু কার্ডটা দুইশো বার follow করেন।
অর্ডার স্লিপ হলো Builder। রেসিপি কার্ড হলো Director। সেলাই করা পাঞ্জাবি হলো Product। পুরো দোকানটাই হলো Builder pattern। চাচা, তারিক, রহিম ভাই, আর নাসরিন ম্যাডামকে মাথায় রাখো — এরা প্রতিটা section-এই ফিরে আসবে। এবার pattern-টা ঠিকঠাক শিখি।
Builder pattern কী?
Builder হলো একটা creational design pattern যা জটিল object গুলোকে ধাপে ধাপে তৈরি করে — একটা বিশাল constructor call-এ সব কিছু ঢুকিয়ে দেওয়ার বদলে।
সহজ কথায় বলি। কিছু object-এর অনেকগুলো অংশ থাকে: কিছু required, অনেকগুলো optional। Builder pattern নির্মাণের কাজটাকে product class থেকে সরিয়ে একটা আলাদা builder object-এ নিয়ে যায়। তুমি ছোট, সুনামযুক্ত step method গুলো call করো — withFabric(), withSize(), addPocket() — শুধু যে অংশগুলো দরকার সেগুলোর জন্য। শেষে একটা final method call করো, সাধারণত build()। সেই method এতক্ষণে সংগ্রহ করা সব কিছু validate করে আর finished product return করে, প্রায়ই একটা immutable object হিসেবে (যেটা তৈরির পর আর পরিবর্তন হয় না)। ঠিক চাচার পাঞ্জাবির মতো: যেকোনো ক্রমে প্রশ্ন, স্লিপ একবার যাচাই, সেলাই করা পাঞ্জাবি চিরস্থায়ী।
প্যাটার্নটা Gang of Four বইতে (১৯৯৪) আছে, কিন্তু এর সবচেয়ে জনপ্রিয় আধুনিক রূপ হলো fluent builder: প্রতিটি step method builder-কেই return করে (return this), তাই call গুলো সুন্দরভাবে chain হয়:
const kurta = new KurtaBuilder()
.withFabric("cotton")
.withColor("white")
.withSize(40)
.build();জোরে পড়ো। প্রায় একটা ইংরেজি বাক্যের মতো। এই পাঠযোগ্যতাই pattern-এর সবচেয়ে বড় শক্তি।
এক লাইনে সারাংশ: Builder = নামযুক্ত method দিয়ে অর্ডার স্লিপ ধাপে ধাপে পূরণ করো, তারপর build() একবার call করো আর তৈরি, যাচাই করা, অপরিবর্তনীয় product পাও।
কলেজ কর্নার: লক্ষ্য করো mutable construction state আর immutable result-এর মধ্যে পার্থক্যটা। Builder ইচ্ছাকৃতভাবে mutable আর স্বল্পস্থায়ী; product immutable আর দীর্ঘস্থায়ী। এই বিভাজন product-এর দিক থেকে বিনা খরচে thread-safety দেয় — immutable object গুলো lock ছাড়াই thread-এ share করা যায়। অনেক functional language এটা built-in করে রাখে। যেমন Rust-এর ownership system "builder consume করো, product emit করো" handoff-কে explicit করে।
এটা কোন সমস্যার সমাধান করে
আগে ব্যথাটা অনুভব করি। ধরো তারিক, এক semester প্রোগ্রামিং শিখে, builder ছাড়াই দোকান code করল। অনেক option সহ একটা পাঞ্জাবি তাকে বিখ্যাত telescoping constructor anti-pattern-এর দিকে নিয়ে যায় — ক্রমশ লম্বা হতে থাকা constructor-এর মই:
// BAD CODE: the telescoping constructor anti-pattern
class Kurta {
constructor(
fabric: string,
size: number,
color?: string,
collar?: string,
sleeves?: string,
pockets?: number,
buttons?: string,
embroidery?: string | null,
giftWrap?: boolean
) {
// ...store everything...
}
}
// And now look at the call site. Can YOU read this?
const order = new Kurta(
"silk", 42, "maroon", undefined, "full", 0, "fancy", "chikankari", true
);সৎভাবে বলো — ওই 0 মানে কী? মাঝখানে undefined কেন? শেষে true কী জিনিস? তারিক নিজেও এক সপ্তাহে ভুলে যাবে। জামাল সাহেব যখন বিয়ের অর্ডার confirm করতে ফোন করেন, দোকানের কেউ তাকে পড়ে শোনাতে পারবে না। ঝামেলা জমতে থাকে:
- অপাঠযোগ্য call site। কোনো নাম ছাড়া শুধু positional value-এর দেয়াল। একটা ভুল position — ধরো pockets আর size উল্টো করে দিলে — size-0 পাঞ্জাবি পাবে ৪২টা পকেটসহ। Compiler হয়তো ধরবেও না।
- Combinatorial explosion। "সাহায্য" করতে গিয়ে লোকজন অনেক constructor overload লেখে: (fabric, size), (fabric, size, color), (fabric, size, color, collar)... প্রতিটি নতুন optional field variations গুণ করে।
- অর্ধেক তৈরি object। অন্য পথ — সব কিছু public setter করা — আরও খারাপ। এখন object, fabric সেট করে কিন্তু size ছাড়াই exist করতে পারে, আর কিছু code হয়তো ওই ভাঙা, অর্ধেক সেলাই করা অবস্থায় ব্যবহার করে ফেলবে। ভাবো চাচা এক হাতা ছাড়া পাঞ্জাবি delivery দিচ্ছেন।
- সম্পূর্ণ object validation-এর জায়গা নেই। "কাজের জন্য সিল্ক বা সুতি লাগবে, polyester না" — এরকম নিয়মের কোনো স্বাভাবিক জায়গা নেই যখন বাইরে থেকে field-এ একটু একটু করে সেট করা হয়।
এই ঝামেলাগুলোর মধ্যে কোনটা team-কে সবচেয়ে বেশি কামড় দেয়? যে কাউকে জিজ্ঞেস করো যে আট-argument constructor ভরা codebase maintain করেছে:
আমরা চাই construction যেটা পাঠযোগ্য (নামযুক্ত step), নমনীয় (যেকোনো ক্রম, optional skip করা যাবে), নিরাপদ (required field একবার, এক জায়গায় চেক), আর immutable final object তৈরি করতে পারে। Builder pattern চারটাই দেয় — এটাই চাচার অর্ডার স্লিপ, type করা।
Builder কীভাবে কাজ করে, ধাপে ধাপে
নূর টেইলার্স ঠিকঠাক সাজাই। এই ধাপগুলো follow করো:
- Product-এর field গুলো তালিকা করো। লিখে ফেলো একটা পাঞ্জাবিতে কী কী থাকতে পারে। কোনগুলো required (fabric, size) আর কোনগুলো optional with defaults (color = white, sleeves = full, pockets = 0) সেটা চিহ্নিত করো। এটা চাচার সিদ্ধান্তের মতো — কোন প্রশ্নগুলো বাধ্যতামূলক।
- Builder class তৈরি করো। এটা mutable working state হিসেবে product-এর মতো একই field রাখে — এটাই অর্ডার স্লিপ। Optional field গুলোতে default value দিয়ে শুরু করো।
- প্রতিটা logical অংশের জন্য একটা step method লেখো।
withFabric(),withColor(),withCollar(),addPocket(),withEmbroidery()। Fluent style-এ, প্রতিটা step শেষ হয়return thisদিয়ে যাতে call গুলো chain করা যায়। build()লেখো। এটা required field চেক করে, cross-field rules enforce করে ("সম্পূর্ণ object validation"), তারপর finalKurtaতৈরি করে return করে। Product-এর constructor লুকিয়ে রাখা যায় যাতে builder-ই একমাত্র প্রবেশদ্বার হয়।- (ঐচ্ছিক) Director যোগ করো। যদি একই step sequence বারবার repeat হয় — school uniform, wedding special — প্রতিটা sequence-কে একটা ছোট recipes class-এর নামযুক্ত method-এ wrap করো। এগুলোই চাচার recipe card।
আর এখানে রহিম ভাইয়ের অর্ডার সময়ের সাথে একটা কথোপকথন হিসেবে। দেখো কীভাবে প্রতিটা step চুপচাপ builder-কেই return করে, আর পাঞ্জাবি কেবল একদম শেষেই আসে:
Classic GoF version-এ একটা Builder interface আর বেশ কয়েকটা concrete builder থাকে, যাতে একই recipe সম্পূর্ণ ভিন্ন representation তৈরি করতে পারে। যেমন, একটা builder আসল Car তৈরি করে, অন্যটা গাড়ির instruction Manual তৈরি করে। আধুনিক দৈনন্দিন code-এ, এখানে দেখানো single fluent builder-ই সবচেয়ে প্রচলিত রূপ।
কলেজ কর্নার: GoF multi-builder form নিয়ে এক মিনিট ভাবার মতো। একটা single Director recipe — "চাকা যোগ করো, ইঞ্জিন যোগ করো, আসন যোগ করো" — CarBuilder-এর বিপরীতে replay করলে একটা car পাবে, আর ManualBuilder-এর বিপরীতে একই step বর্ণনা করা documentation পাবে। Construction process আর final representation স্বাধীন variation-এর axis হয়ে যায়। আধুনিক compiler ঠিক এই split ব্যবহার করে: একটা parsing process, বিভিন্ন builder AST, bytecode, বা pretty-printed source তৈরি করে।
বাস্তব কোডের উদাহরণ
নূর টেইলার্স TypeScript-এ, section by section code করার সময় এসেছে।
Section 1: Product। পাঞ্জাবি immutable — সব field readonly, constructor-এ একবারই সেট হয়।
// ---------- PRODUCT ----------
// Immutable: once stitched, a kurta never changes.
class Kurta {
constructor(
public readonly fabric: string,
public readonly color: string,
public readonly size: number,
public readonly collar: string,
public readonly sleeves: string,
public readonly pockets: number,
public readonly buttons: string,
public readonly embroidery: string | null
) {}
describe(): string {
const extra = this.embroidery
? ` with ${this.embroidery} embroidery`
: "";
return (
`A size-${this.size} ${this.color} ${this.fabric} kurta, ` +
`${this.collar} collar, ${this.sleeves} sleeves, ` +
`${this.pockets} pocket(s), ${this.buttons} buttons${extra}.`
);
}
}হ্যাঁ, এই constructor-টা লম্বা — কিন্তু লক্ষ্য করো শুধুমাত্র builder এটা call করবে। কোনো মানুষকে সরাসরি ওই call পড়তে বা লিখতে হবে না। ঝামেলাটা একটা ঘরে বন্ধ — ঠিক যেমন চাচার দোকানের পেছনের cutting table, যেখানে কাস্টমাররা কখনো যায় না।
Section 2: Builder। এটা ইকবাল চাচার অর্ডার স্লিপ।
// ---------- BUILDER ----------
class KurtaBuilder {
// Required fields: no defaults; must be set before build().
private fabric: string | null = null;
private size: number | null = null;
// Optional fields: sensible defaults, like Chacha assumes.
private color = "white";
private collar = "band";
private sleeves = "full";
private pockets = 0;
private buttons = "standard";
private embroidery: string | null = null;
withFabric(fabric: string): KurtaBuilder {
this.fabric = fabric;
return this; // fluent: enables chaining
}
withSize(size: number): KurtaBuilder {
this.size = size;
return this;
}
withColor(color: string): KurtaBuilder {
this.color = color;
return this;
}
withCollar(collar: string): KurtaBuilder {
this.collar = collar;
return this;
}
withSleeves(sleeves: "full" | "half"): KurtaBuilder {
this.sleeves = sleeves;
return this;
}
addPocket(): KurtaBuilder {
this.pockets += 1;
return this;
}
withButtons(buttons: string): KurtaBuilder {
this.buttons = buttons;
return this;
}
withEmbroidery(style: string): KurtaBuilder {
this.embroidery = style;
return this;
}
// The final step: validate everything, then stitch.
build(): Kurta {
if (this.fabric === null) {
throw new Error("Order incomplete: fabric is required.");
}
if (this.size === null) {
throw new Error("Order incomplete: size is required.");
}
// A whole-object rule, checked in ONE place:
if (this.embroidery !== null && this.fabric === "polyester") {
throw new Error("Embroidery needs cotton or silk, not polyester.");
}
return new Kurta(
this.fabric,
this.color,
this.size,
this.collar,
this.sleeves,
this.pockets,
this.buttons,
this.embroidery
);
}
}তিনটা বিষয় মনোযোগ দিয়ে দেখো:
- Required বনাম optional একদম স্পষ্ট। Required field গুলো
nullদিয়ে শুরু; optional field গুলো default দিয়ে শুরু। Class-এর structure নিয়মগুলো document করে — ঠিক চাচার স্লিপের বাধ্যতামূলক বক্সের মতো। - প্রতিটা step
thisreturn করে। ওই একটা line সাধারণ method গুলোকে chainable, বাক্যের মতো API-তে পরিণত করে। build()হলো gatekeeper। Field check আর cross-field rules একসাথে এক জায়গায় থাকে, আর একটা অবৈধ পাঞ্জাবি কখনো program-এ প্রবেশ করতে পারে না। হয় তুমি একটা সম্পূর্ণ, বৈধ, immutable পাঞ্জাবি পাবে — অথবা কাউন্টারেই একটা পরিষ্কার error পাবে।
Section 3: Director (ঐচ্ছিক কিন্তু সুন্দর)। চাচার recipe card।
// ---------- DIRECTOR ----------
// Named recipes for orders that repeat again and again.
class TailorRecipes {
schoolUniform(size: number): Kurta {
return new KurtaBuilder()
.withFabric("cotton")
.withColor("white")
.withSize(size)
.withSleeves("half")
.addPocket()
.build();
}
weddingSpecial(size: number): Kurta {
return new KurtaBuilder()
.withFabric("silk")
.withColor("maroon")
.withSize(size)
.withCollar("sherwani")
.withButtons("fancy")
.withEmbroidery("chikankari")
.build();
}
}Section 4: কাস্টমাররা।
// ---------- CLIENT ----------
// Farhan bhai's custom order, built step by step, in any order:
const dailyWear = new KurtaBuilder()
.withSize(38)
.withFabric("cotton")
.addPocket()
.addPocket()
.build();
console.log(dailyWear.describe());
// Mrs. D'Souza's uniforms and Mr. Joshi's wedding kurta use recipe cards:
const recipes = new TailorRecipes();
const uniform = recipes.schoolUniform(36);
const groomKurta = recipes.weddingSpecial(42);
console.log(uniform.describe());
console.log(groomKurta.describe());
// And a careless order is caught at the counter, not at the wedding:
try {
new KurtaBuilder().withFabric("polyester").withSize(40)
.withEmbroidery("chikankari").build();
} catch (e) {
console.log("Rejected: " + (e as Error).message);
}Output:
A size-38 white cotton kurta, band collar, full sleeves, 2 pocket(s), standard buttons.
A size-36 white cotton kurta, band collar, half sleeves, 1 pocket(s), standard buttons.
A size-42 maroon silk kurta, sherwani collar, full sleeves, 0 pocket(s), fancy buttons with chikankari embroidery.
Rejected: Embroidery needs cotton or silk, not polyester.দেখো আমরা কী পেলাম। রহিম ভাইয়ের অর্ডার ভিন্ন ক্রমে step সেট করেছে (fabric-এর আগে size) — কোনো সমস্যা নেই। ইউনিফর্ম আর বিয়ের অর্ডার recipe পুনরায় ব্যবহার করেছে — কোনো copy-paste নেই। আর খারাপ polyester অর্ডার build time-এই আটকে গেছে, পরিষ্কার message সহ, program-এ কোনো invalid পাঞ্জাবি ঢোকার আগেই। কাউন্টারে ভুল ধরো, বিয়েতে না।
C# তে একই ধারণা
C# developers প্রতিদিন builder ব্যবহার করেন। এখানে আমাদের দর্জির দোকান সংক্ষিপ্তভাবে:
public sealed record Kurta(
string Fabric, string Color, int Size,
string Collar, int Pockets, string? Embroidery);
public class KurtaBuilder
{
private string? _fabric;
private int? _size;
private string _color = "white";
private string _collar = "band";
private int _pockets = 0;
private string? _embroidery;
public KurtaBuilder WithFabric(string fabric)
{
_fabric = fabric;
return this;
}
public KurtaBuilder WithSize(int size)
{
_size = size;
return this;
}
public KurtaBuilder WithColor(string color)
{
_color = color;
return this;
}
public KurtaBuilder AddPocket()
{
_pockets++;
return this;
}
public KurtaBuilder WithEmbroidery(string style)
{
_embroidery = style;
return this;
}
public Kurta Build()
{
if (_fabric is null) throw new InvalidOperationException("Fabric is required.");
if (_size is null) throw new InvalidOperationException("Size is required.");
return new Kurta(_fabric, _color, _size.Value, _collar, _pockets, _embroidery);
}
}
// Usage — reads like a sentence:
var kurta = new KurtaBuilder()
.WithFabric("silk")
.WithColor("maroon")
.WithSize(42)
.WithEmbroidery("chikankari")
.Build();record এমনিতেই immutability বিনামূল্যে দেয়। একই দোকান, একই ধারণা, ভিন্ন language।
আর একটা ছোট Python variation যেটা tester-রা পছন্দ করেন — test data builder। তারিক তার কলেজ project-এ এই trick ব্যবহার করে: test-এর অনেক সামান্য ভিন্ন পাঞ্জাবি দরকার, আর builder প্রতিটা test-কে একটা পাঠযোগ্য শুরুর বিন্দু দেয় যেখানে সব কিছুর default আছে:
class KurtaBuilder:
def __init__(self):
self._fabric = "cotton" # test default: every field pre-filled
self._size = 40
self._color = "white"
self._pockets = 0
def with_fabric(self, fabric):
self._fabric = fabric
return self
def with_size(self, size):
self._size = size
return self
def add_pocket(self):
self._pockets += 1
return self
def build(self):
return Kurta(self._fabric, self._color, self._size, self._pockets)
def a_kurta():
return KurtaBuilder() # tiny readable entry point
# In tests: each test names ONLY what it cares about.
tiny = a_kurta().with_size(28).build()
silk = a_kurta().with_fabric("silk").build()
pocketed = a_kurta().add_pocket().add_pocket().build()পার্থক্যটা লক্ষ্য করো: test builder-এ, এমনকি "required" field গুলোও default পায়, কারণ পকেট নিয়ে test-কে fabric নিয়ে কথা বলতে বাধ্য করা উচিত না। Pattern তার উদ্দেশ্য অনুযায়ী নমনীয় হয়।
বাস্তব software-এ কোথায় দেখা যায়
Builder pattern হয়তো দৈনন্দিন programming-এ সবচেয়ে বেশি ব্যবহৃত GoF pattern। তুমি সম্ভবত এর নাম না জেনেই এটা ব্যবহার করেছ:
StringBuilderC# আর Java উভয়তেই সবচেয়ে বিখ্যাত builder। তুমি বারবারappend()call করো — ধাপে ধাপে — আর শেষেToString()/toString()call করে finished string পাও। Final call-টাই হলোbuild()step।- Java-এর
HttpRequest.newBuilder()(Java 11 থেকে) fluent ভাবে HTTP request তৈরি করে: URI সেট করো, header যোগ করো, method বেছে নাও, তারপর.build()। Return হওয়া request immutable — ঠিক আমাদের সেলাই করা পাঞ্জাবির মতো। - Lombok-এর
@Builderannotation Java-তে যেকোনো class-এর জন্য স্বয়ংক্রিয়ভাবে একটা সম্পূর্ণ fluent builder তৈরি করে, কারণ pattern-টা এতটাই সাধারণ যে Java developers এটাকে এক-শব্দের annotation হিসেবে চাইলেন। দেখো Project Lombok docs। - ASP.NET Core প্রায় পুরোটাই builder দিয়ে তৈরি:
WebApplication.CreateBuilder(args)services, configuration, আর logging ধাপে ধাপে সংগ্রহ করেbuilder.Build()দিয়ে application তৈরির আগে।ConfigurationBuilderআরHostBuilderএকই shape follow করে। - Joshua Bloch-এর Effective Java, Item 2 — "অনেক constructor parameter থাকলে builder বিবেচনা করো" — এই pattern-এর পক্ষে সবচেয়ে বিখ্যাত লিখিত যুক্তি। এটা আজকে Java-তে সর্বত্র ব্যবহৃত fluent, static-inner-class builder style জনপ্রিয় করেছে।
- Test data builder একটা প্রিয় testing trick, যেমন Python snippet-এ দেখলে:
a_kurta().with_size(28).build()পাঠযোগ্য test object তৈরি করে যেখানে যা নিয়ে চিন্তা নেই সব কিছুর default আছে।
পড়াশোনার উপকরণের জন্য দেখো iluwatar/java-design-patterns-এ worked builder examples, faif/python-patterns-এ compact version, আর Refactoring Guru-এ illustrated chapter।
কখন ব্যবহার করবে আর কখন করবে না
প্রথমে মানচিত্র দেখো। নূর টেইলার্স দৃঢ়ভাবে উপরে-ডানে বসে: অনেক optional field, আর validation যাকে পুরো অর্ডার দেখতে হবে।
তারপর table দিয়ে নিশ্চিত করো:
| পরিস্থিতি | Builder ব্যবহার করবে? | কেন |
|---|---|---|
| Constructor-এ অনেক parameter, বেশ কয়েকটা optional | হ্যাঁ | নামযুক্ত step অপাঠযোগ্য positional argument প্রতিস্থাপন করে |
| Final object immutable রাখতে চাও কিন্তু configurable | হ্যাঁ | Mutable builder value সংগ্রহ করে; build() freeze করে |
| Validation-কে একটা field-এর বদলে পুরো object দেখতে হয় | হ্যাঁ | build() একমাত্র স্বাভাবিক checkpoint |
| একই construction sequence অনেক জায়গায় repeat হয় | হ্যাঁ | Director recipe method-এ সরিয়ে দাও |
| Tree-shaped বা composite structure ধাপে ধাপে তৈরি করছ | হ্যাঁ | প্রতিটা step একটা অংশ যোগ করতে পারে |
| Object-এ মাত্র দুই-তিনটা সহজ field | না | Plain constructor বা named/default argument ছোট হবে |
| তোমার language-এ default সহ named parameter আছে আর আর কিছু দরকার নেই | না | TypeScript object parameter বা Python keyword argument হয়তো সমাধান করে |
| আসলে ভিন্ন type-এর object বেছে নেওয়া দরকার | না | সেটা factory-এর কাজ, builder-এর না |
Builder-এর খরচ কী আর কী পরিশোধ করে? Builder লেখা শুরুতে অতিরিক্ত কাজ — সেই সব step method। কিন্তু পরবর্তী প্রতিটা call site ছোট, নামযুক্ত আর নিরাপদ হয়। যত বেশি জায়গায় object তৈরি হয়, তত দ্রুত builder পরিশোধ হয়:
Language এখানে গুরুত্বপূর্ণ। TypeScript আর Python প্রায়ই default সহ single options object বা keyword argument pass করতে পারে, যা readability সমস্যা সমাধান করে। Builder তখনও জেতে যখন তোমার সময়ের সাথে ধাপে ধাপে assembly (শেষ করার আগে বিভিন্ন জায়গা থেকে value সংগ্রহ), সম্পূর্ণ object validation, বা নিশ্চিত immutable product দরকার।
কলেজ কর্নার: একটা advanced trick আছে যাকে step builder (বা staged builder) বলে — এটা required field গুলোকে type system-এ encode করে। KurtaBuilder.withFabric() একটা NeedsSize type return করে, যার একমাত্র method হলো withSize(), যেটা full builder return করে। Required field ভুলে যাওয়া তখন runtime throw-এর বদলে compile-time error হয়ে যায়। এতে বেশি interface লাগে, তাই team গুলো এটা সেই object গুলোর জন্য রাখে যেখানে missing field সত্যিই বিপজ্জনক।
একটা অর্ডার স্লিপের জীবনচক্র
Builder নিজে একটা জীবন পায়। এটা খালি জন্মায়, step সংগ্রহ করে, build()-এ বিচারের মুখোমুখি হয়, তারপর তার product বেঁচে থাকে আর স্লিপ ফেলে দেওয়া হয়। Validation-এর দুটো ফলাফল দেখো:
দুটো state মনোযোগ দেওয়ার মতো। Collecting-এ self-loop হলো "যেকোনো ক্রম, যেকোনো সংখ্যক step"-এর স্বাধীনতা। আর Stitched থেকে Collecting-এ ফেরার কোনো arrow নেই — একবার পাঞ্জাবি তৈরি হলে কেউ এটা আবার খুলতে পারবে না। যদি Kurta-তে public setter থাকত, সেই arrow থাকত, আর পুরো safety গল্পটা ভেঙে পড়ত। যখন builder design করবে, এই diagram আঁকো আর চেক করো: Collecting থেকে Validating-এর মধ্য দিয়ে না গিয়ে কিছু বেরিয়ে যেতে পারে কি? যদি হ্যাঁ, তাহলে একটা ফাঁক আছে।
প্রতিবেশী pattern গুলোর সাথে তুলনা
| Pattern | যে প্রশ্নের উত্তর দেয় | নির্মাণ style | ফলাফল |
|---|---|---|---|
| Builder | "এই একটা জটিল object কীভাবে একত্রিত হয়?" | অনেক ছোট step, তারপর build() | একটা জটিল (প্রায়ই immutable) object |
| Factory Method | "এই step কোন single object তৈরি করবে?" | একটা call, subclass-এ সিদ্ধান্ত | একটা object, সাথে সাথে return |
| Abstract Factory | "কোন matching family of objects দরকার?" | প্রতিটা family member-এ একটা call | সম্পর্কিত object-এর একটা set |
| Prototype | "বিদ্যমান object copy করতে পারি?" | একটা clone() call | adjust করার জন্য একটা duplicate |
পার্থক্য মনে রাখার সবচেয়ে সহজ উপায়: factory বেছে নেয়, builder একত্রিত করে। ইকবাল চাচা মোটেও পাঞ্জাবির type বেছে নেন না — তিনি একটা পাঞ্জাবি একত্রিত করেন, ধীরে ধীরে, কথোপকথনের মাধ্যমে, আর কেবল তখনই হস্তান্তর করেন যখন এটা সম্পূর্ণ আর যাচাই করা।
এই pattern গুলো একসাথেও কাজ করে। একটা Abstract Factory preconfigured builder হস্তান্তর করতে পারে (একটা কাউন্টার যেটা তোমাকে অর্ডার স্লিপ দেয়)। একটা builder-এর build() ভেতরে parts তৈরিতে Factory Method ব্যবহার করতে পারে (চাচা একটা button-maker থেকে বোতাম সংগ্রহ করেন)। আর অনেক team telescoping constructor দিয়ে শুরু করে, ব্যথা পেলে Builder-এ refactor করে, আর recipe repeat হলে Director যোগ করে।
ছাত্রছাত্রীরা যে সাধারণ ভুল করে
ভুল ১: return this ভুলে যাওয়া। একটা step method return this ছাড়া লিখলে, পরের step-এ "cannot read property of undefined" error দিয়ে chain ভেঙে যায়। প্রতিটা step method একইভাবে শেষ হয় কিনা চেক করো।
ভুল ২: build()-এর বদলে প্রতিটা step-এ validation করা। Per-step check সম্পূর্ণ ছবি দেখতে পারে না — কাজের-জন্য-সিল্ক rule-এ দুটো field জড়িত। Simple per-field check step-এ ঠিক আছে, কিন্তু সম্পূর্ণ object নিয়ে rules build()-এ থাকা উচিত, যেখানে সব কিছু শেষমেশ জানা যায়। চাচা স্লিপ একবার চেক করেন, শেষে, প্রতিটা প্রশ্নের পরে নয়।
ভুল ৩: Reset না করে একই builder অনেক product-এর জন্য পুনরায় ব্যবহার করা। build()-এর পরে, স্লিপে এখনও পুরনো value আছে। আবার build করলে নিঃশব্দে আগের কাস্টমারের কাজ carry over হতে পারে! ভাবো রহিম ভাইয়ের সাদামাটা পাঞ্জাবি জামাল সাহেবের chikankari সহ এসেছে। হয় প্রতিটা product-এর জন্য fresh builder তৈরি করো, অথবা builder-কে একটা reset() দাও আর প্রতিটা build-এর পরে call করো।
ভুল ৪: Product-কে যাইহোক mutable রাখা। Kurta-তে public setter রাখলে, যে কেউ delivery-এর পরে এটা "পুনরায় সেলাই" করতে পারবে, আর build() validation-এর প্রতিশ্রুতি হারিয়ে যাবে। Product-এর field গুলো readonly করো আর, যেখানে language অনুমতি দেয়, constructor লুকাও যাতে builder-ই একমাত্র পথ হয়।
একই তালিকা quick-scan table হিসেবে:
| ভুল | কেমন দেখায় | সমাধান |
|---|---|---|
Missing return this | পরের step-এ chain crash করে | প্রতিটা step method builder return করে |
| Step-এ ছড়িয়ে থাকা validation | Cross-field rules অর্ধেক চেক | Whole-object rules build()-এ থাকে |
| Reused builder, no reset | আগের অর্ডারের value leak হয় | প্রতিটা product-এর জন্য fresh builder, বা reset() |
| Mutable product | Kurta-তে public setter | readonly field, hidden constructor |
দ্রুত রিভিশন বক্স
+=================================================================+
| BUILDER - QUICK REVISION |
+=================================================================+
| IDEA : Assemble a complex object with small NAMED steps; |
| build() validates and returns the final product. |
| POPULAR FORM: Fluent builder - every step returns this. |
|-----------------------------------------------------------------|
| PARTICIPANTS |
| Product -> Kurta (immutable once stitched) |
| Builder -> KurtaBuilder (the order slip) |
| Director -> TailorRecipes (optional recipe cards) |
|-----------------------------------------------------------------|
| GOLDEN RULES |
| 1. Required fields start null; optionals get defaults. |
| 2. Whole-object validation lives in build(), one place. |
| 3. Steps can run in ANY order; skip what you do not need. |
| 4. Fresh builder per product, or reset() after build(). |
|-----------------------------------------------------------------|
| KILLS : the telescoping constructor anti-pattern |
| REAL LIFE : StringBuilder, HttpRequest.newBuilder (Java 11), |
| Lombok @Builder, ASP.NET Core WebApplicationBuilder|
| REMEMBER : "Order slip first, stitching last." |
+=================================================================+প্র্যাকটিস এক্সারসাইজ
সুই-সুতো তুলে নাও! পাঁচটা কাজ:
- Kurta builder বাড়াও। একটা
withLength("short" | "knee" | "long")step যোগ করো, default"knee"সহ, আরbuild()-এ একটা নতুন নিয়ম:"short"পাঞ্জাবিতে"sherwani"কলার থাকতে পারবে না। এই নিয়মে reject হয় এমন একটা অর্ডার লেখো। - Pizza অর্ডার builder। একটা
Pizzamodel করো requiredsizeসহ, আর optionalbase(default "thin"),cheese(default normal), আরaddTopping()এর মাধ্যমে toppings-এর তালিকা।build()-এ validate করো যে pizza-তে সর্বাধিক ৫টা topping থাকতে পারে। তারপরmargherita(size)recipe সহ একটাPizzaRecipesdirector তৈরি করো। - Code-এর আগে diagram আঁকো। Exercise 2-এর জন্য নিজে চিত্র ৬ (sequence) আর চিত্র ৯ (state diagram)-এর version আঁকো। তোমার diagram-এ সেই একটা নিয়ম চেক করো যেটা গুরুত্বপূর্ণ:
Validating-এর মধ্য দিয়ে না গিয়েCollectingথেকে কিছু বেরিয়ে যেতে পারে কি না। - ভাবো আর লেখো (কোড ছাড়া)। তোমার বন্ধু বলছে, "TypeScript-এ optional object parameter আছে —
new Kurta({ fabric: 'cotton', size: 40 })— তাই Builder pattern এখানে অকেজো।" ৪-৫ বাক্যে লেখো: options-object কোন দুটো সমস্যা সমাধান করে, আর একটা real builder-এর কোন তিনটা সুবিধা (validation, immutability, সময়ের সাথে ধাপে ধাপে assembly-র কথা ভাবো) এটা এখনও miss করে। - কলেজ কর্নার চ্যালেঞ্জ। Kurta-এর জন্য step builder implement করো:
withFabric()একটা type return করে যার একমাত্র method হলোwithSize(), যেটা full builder return করে। নিশ্চিত করো size ভুলে গেলে এখন compile time-এ fail করে, তারপর দুটো বাক্যে লেখো অতিরিক্ত interface গুলো সার্থক ছিল কিনা।
শাবাশ! তুমি এখন তিনটা বড় creational pattern শেষ করেছ। ফিরে গিয়ে Factory Method আর Abstract Factory posts revise করো, আর লক্ষ্য করো তিনটাই একটা গভীর প্রশ্নের ভিন্নভাবে উত্তর দেয়: কে object তৈরি করে, আর কীভাবে?
সচরাচর জিজ্ঞাসা
- অনেক parameter দিয়ে সরাসরি constructor ব্যবহার করলেই হয় না কেন?
- new Kurta('cotton', 'white', 40, true, false, true, null) — এরকম লম্বা constructor call পড়া অসম্ভব। তৃতীয় 'true' মানে কী সেটা কেউ মনে রাখে না। Builder এই সমস্যা সমাধান করে withFabric('cotton') বা addPocket() এর মতো নামযুক্ত step দিয়ে, যাতে code নিজেই নিজেকে explain করে।
- Fluent builder কী জিনিস?
- Fluent builder হলো সেই জনপ্রিয় style যেখানে প্রতিটি step method builder-কেই return করে। এর ফলে একটার পর একটা call chain করা যায় — যেমন builder.withFabric('cotton').withSize(40).build()। পড়তে অনেকটা ইংরেজি বাক্যের মতো লাগে।
- Director class কি সবসময় লাগে?
- না, Director ঐচ্ছিক। তুমি তখনই একটা Director যোগ করবে যখন দেখবে একই build step এর sequence অনেক জায়গায় বারবার repeat হচ্ছে। Director এই sequence গুলোকে নামযুক্ত recipe হিসেবে রাখে — যেমন schoolUniform() বা weddingSpecial() — যাতে steps copy-paste করতে না হয়।
- build() কখন error throw করে?
- build() method হলো সেই সঠিক জায়গা যেখানে চেক করা হয় সব required field সেট হয়েছে কিনা আর combination ঠিক আছে কিনা। যেমন, fabric না দিলে build() সাথে সাথে error দেবে। এভাবে অর্ধেক-তৈরি বা ভুল object কখনো program-এর বাকি অংশে পৌঁছায় না।
- Builder আর Factory Method এর মধ্যে পার্থক্য কী?
- Factory একটি মাত্র call-এ object তৈরি করে — কোন type তৈরি হবে সেটার উপর focus। Builder একটি জটিল object-কে অনেকগুলো ছোট step-এ তৈরি করে, শেষে হস্তান্তর করে — object কীভাবে তৈরি হবে সেটার উপর focus। Factory উত্তর দেয় 'কোনটা?'; Builder উত্তর দেয় 'কীভাবে তৈরি হবে?'।
আরো দেখো
সম্পর্কিত পাঠ
Factory Method Pattern: Branch নিজেই ঠিক করুক কোন Vehicle
Factory Method design pattern শেখো রাহিমের টিফিন সার্ভিসের গল্পের মাধ্যমে — সহজ TypeScript আর C# code, diagram, real-world উদাহরণ, আর practice সহ।
Abstract Factory Pattern: এক অর্ডার, এক ম্যাচিং থালি
বাংলাদেশি বিয়ের ক্যাটারিং গল্পের মাধ্যমে Abstract Factory design pattern বুঝে নাও — সহজ TypeScript ও Python কোড, diagram, আর practice সহ।
Prototype Pattern: জিরো থেকে না বানিয়ে ফটোকপি করো
বিয়ের কার্ডের দোকানের গল্প দিয়ে Prototype design pattern শেখো — TypeScript আর Python-এর সহজ উদাহরণ, আর shallow vs deep copy-এর পরিষ্কার demo সহ।
Singleton Pattern: পুরো স্কুলে একজনই হেড স্যার
স্কুলের হেড স্যারের গল্প দিয়ে Singleton design pattern বোঝো — সহজ TypeScript ও C# কোড, thread safety, আর কেন অনেক সিনিয়র ডেভেলপার এটাকে anti-pattern বলেন।