در برنامهنویسی شیگرا، ایجاد یک Object ساده معمولاً کار دشواری نیست:
$user = new User();
اما زمانی که یک Class دارای Propertyهای زیاد، تنظیمات اختیاری و وابستگیهای مختلف باشد، Constructor آن میتواند بسیار طولانی و سختخوان شود.
مثلاً:
$order = new Order(
1001,
25,
'pending',
'Tehran',
'1234567890',
true,
false,
'express',
'cash',
'special note'
);
در این حالت مشخص نیست هر مقدار دقیقاً مربوط به کدام Property است.
اگر تعداد پارامترها زیاد شود، احتمال اشتباه نیز بیشتر خواهد شد.
Builder Pattern برای حل چنین مسئلهای استفاده میشود.
Builder Pattern یکی از الگوهای طراحی از گروه Creational Design Patterns است که امکان ساخت مرحلهای Objectهای پیچیده را فراهم میکند.
به زبان ساده:
> Builder به ما اجازه میدهد یک Object را مرحلهبهمرحله و با خوانایی بیشتر بسازیم.
# مشکل Constructorهای طولانی
فرض کنید یک Class برای ساخت کاربر داریم:
class User
{
public function __construct(
public string $name,
public string $email,
public ?string $phone,
public ?string $address,
public bool $isAdmin,
public bool $isActive
) {
}
}
حالا برای ایجاد کاربر باید تمام پارامترها را به ترتیب ارسال کنیم:
$user = new User(
'Saeed',
'saeed@example.com',
null,
null,
false,
true
);
این کد چند مشکل دارد:
- ترتیب پارامترها باید دقیقاً رعایت شود.
- مقدار
nullخوانایی را کم میکند. - مشخص نیست هر مقدار برای کدام Property است.
- افزودن Property جدید میتواند تمام محلهای استفاده را تحت تأثیر قرار دهد.
- Constructor بیش از حد شلوغ میشود.
Builder میتواند این ساختار را خواناتر کند.
# ساخت User Builder در PHP
ابتدا Class اصلی User را میسازیم:
class User
{
public function __construct(
public string $name,
public string $email,
public ?string $phone = null,
public ?string $address = null,
public bool $isAdmin = false,
public bool $isActive = true
) {
}
}
حالا Builder:
class UserBuilder
{
private string $name;
private string $email;
private ?string $phone = null;
private ?string $address = null;
private bool $isAdmin = false;
private bool $isActive = true;
public function name(string $name): self
{
$this->name = $name;
return $this;
}
public function email(string $email): self
{
$this->email = $email;
return $this;
}
public function phone(?string $phone): self
{
$this->phone = $phone;
return $this;
}
public function address(?string $address): self
{
$this->address = $address;
return $this;
}
public function admin(bool $isAdmin = true): self
{
$this->isAdmin = $isAdmin;
return $this;
}
public function active(bool $isActive = true): self
{
$this->isActive = $isActive;
return $this;
}
public function build(): User
{
return new User(
name: $this->name,
email: $this->email,
phone: $this->phone,
address: $this->address,
isAdmin: $this->isAdmin,
isActive: $this->isActive
);
}
}
حالا ساخت User به شکل خواناتری انجام میشود:
$user = (new UserBuilder())
->name('Saeed')
->email('saeed@example.com')
->phone('09120000000')
->address('Tehran')
->active()
->build();
هر متد یک بخش از Object را تنظیم میکند و در پایان، متد build() Object نهایی را میسازد.
# Fluent Interface در Builder چیست؟
در مثال قبلی، متدها مقدار self را برمیگردانند:
return $this;
به همین دلیل میتوانیم متدها را پشت سر هم صدا بزنیم:
$builder
->name('Saeed')
->email('saeed@example.com')
->active()
->build();
به این سبک استفاده از متدها Fluent Interface گفته میشود.
برای داشتن Fluent Interface، متد معمولاً به جای void، خود Object یا Builder را برمیگرداند.
مثال:
public function name(string $name): self
{
$this->name = $name;
return $this;
}
اگر return $this وجود نداشته باشد، امکان زنجیره کردن متدها را نخواهیم داشت.
# چرا متد build مهم است؟
متد build() مسئول ایجاد Object نهایی است.
مثلاً:
public function build(): User
{
return new User(
name: $this->name,
email: $this->email
);
}
بهتر است منطق ایجاد Object نهایی در یک محل متمرکز باشد.
همچنین میتوانیم در build() اعتبارسنجی انجام دهیم.
مثلاً:
public function build(): User
{
if (!isset($this->name)) {
throw new LogicException(
'User name is required'
);
}
if (!isset($this->email)) {
throw new LogicException(
'User email is required'
);
}
return new User(
name: $this->name,
email: $this->email,
phone: $this->phone,
address: $this->address,
isAdmin: $this->isAdmin,
isActive: $this->isActive
);
}
در این حالت اگر Propertyهای ضروری تنظیم نشده باشند، ساخت Object متوقف میشود.
# ساخت یک Request Builder
یکی از مثالهای کاربردی Builder، ساخت یک درخواست HTTP است.
فرض کنید میخواهیم درخواست زیر را بسازیم:
- URL
- Method
- Headers
- Query Parameters
- Body
- Timeout
به جای Constructor طولانی، میتوانیم Builder داشته باشیم:
class HttpRequest
{
public function __construct(
public string $url,
public string $method,
public array $headers,
public array $query,
public ?array $body,
public int $timeout
) {
}
}
Builder:
class HttpRequestBuilder
{
private string $url;
private string $method = 'GET';
private array $headers = [];
private array $query = [];
private ?array $body = null;
private int $timeout = 30;
public function url(string $url): self
{
$this->url = $url;
return $this;
}
public function method(string $method): self
{
$this->method = strtoupper($method);
return $this;
}
public function header(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
public function query(string $name, mixed $value): self
{
$this->query[$name] = $value;
return $this;
}
public function body(array $body): self
{
$this->body = $body;
return $this;
}
public function timeout(int $seconds): self
{
$this->timeout = $seconds;
return $this;
}
public function build(): HttpRequest
{
if (!isset($this->url)) {
throw new LogicException(
'Request URL is required'
);
}
return new HttpRequest(
url: $this->url,
method: $this->method,
headers: $this->headers,
query: $this->query,
body: $this->body,
timeout: $this->timeout
);
}
}
استفاده:
$request = (new HttpRequestBuilder())
->url('https://example.com/api/users')
->method('POST')
->header('Accept', 'application/json')
->header('Content-Type', 'application/json')
->body([
'name' => 'Saeed',
'email' => 'saeed@example.com',
])
->timeout(15)
->build();
این کد از Constructor طولانی خواناتر است.
# Builder برای ساخت Query
Builder Pattern در ساخت Query نیز کاربرد زیادی دارد.
مثلاً یک Query ساده:
$query = (new QueryBuilder())
->table('users')
->where('active', true)
->where('role', 'admin')
->orderBy('created_at', 'desc')
->limit(20)
->get();
در این حالت هر متد بخشی از Query را تنظیم میکند.
نکته مهم این است که Query Builderهای واقعی معمولاً پیادهسازی پیچیدهتری دارند و فقط یک مثال ساده از مفهوم Builder هستند.
# Builder و Objectهای Immutable
در بعضی پروژهها بهتر است Object نهایی پس از ساخته شدن تغییر نکند.
به چنین Objectهایی معمولاً Immutable Object گفته میشود.
مثلاً:
final class Product
{
public function __construct(
public readonly string $name,
public readonly int $price,
public readonly ?string $description = null
) {
}
}
در این حالت Propertyها پس از ایجاد Object قابل تغییر نیستند.
Builder میتواند مقادیر را مرحلهبهمرحله جمعآوری کند و در پایان یک Object Immutable بسازد:
$product = (new ProductBuilder())
->name('Laptop')
->price(50000000)
->description('Business laptop')
->build();
این ساختار برای Value Objectها و Objectهای تنظیماتی بسیار مناسب است.
# Builder با Constructor چه تفاوتی دارد؟
| ویژگی | Constructor | Builder |
|---|---|---|
| ساخت Object | مستقیم | مرحلهای |
| مناسب Object ساده | بسیار مناسب | معمولاً غیرضروری |
| مناسب Object پیچیده | ممکن است شلوغ شود | مناسب |
| خوانایی پارامترها | وابسته به Named Arguments | معمولاً بیشتر |
| تنظیمات اختیاری | ممکن است دشوار شود | سادهتر |
| تعداد Classها | کمتر | بیشتر |
| امکان اعتبارسنجی در پایان | دارد | دارد |
برای Objectهای ساده، Constructor معمولاً بهترین انتخاب است.
برای Objectهای پیچیده با تنظیمات متعدد، Builder میتواند خوانایی را بهتر کند.
# Builder و Named Arguments در PHP
در نسخههای جدید PHP میتوان از Named Arguments استفاده کرد:
$user = new User(
name: 'Saeed',
email: 'saeed@example.com',
isActive: true
);
این روش بسیاری از مشکلات Constructorهای طولانی را کاهش میدهد.
بنابراین همیشه لازم نیست برای حل مشکل خوانایی، Builder بسازیم.
اگر Named Arguments و Default Arguments نیاز پروژه را برطرف میکنند، استفاده از Constructor سادهتر ممکن است بهتر باشد.
Builder زمانی ارزش بیشتری دارد که:
- مراحل ساخت زیاد هستند.
- اعتبارسنجی ساختارمند لازم است.
- Object گزینههای فراوان دارد.
- ساخت Object باید مرحلهای باشد.
- منطق ساخت Object پیچیده است.
# تفاوت Builder و Factory
Factory و Builder هر دو در دسته Creational قرار میگیرند، اما هدفشان متفاوت است.
Factory
Factory بیشتر به این سؤال پاسخ میدهد:
> کدام Object را بسازم؟
مثلاً:
$gateway = PaymentGatewayFactory::make('zarinpal');
Builder
Builder بیشتر به این سؤال پاسخ میدهد:
> این Object را چگونه مرحلهبهمرحله بسازم؟
مثلاً:
$request = (new HttpRequestBuilder())
->url('https://example.com')
->method('POST')
->body(['name' => 'Saeed'])
->build();
مقایسه:
| ویژگی | Factory | Builder |
|---|---|---|
| تمرکز | انتخاب نوع Object | ساخت مرحلهای Object |
| خروجی | معمولاً یک Implementation | یک Object با تنظیمات مختلف |
| مناسب چند نوع Class | بله | معمولاً خیر |
| مناسب پارامترهای زیاد | گاهی | بله |
| استفاده از Fluent Interface | الزامی نیست | رایج است |
# ترکیب Builder و Factory
در پروژههای بزرگ میتوان Builder و Factory را با هم استفاده کرد.
مثلاً Factory نوع Builder را انتخاب کند:
class ReportBuilderFactory
{
public static function make(string $format): ReportBuilder
{
return match ($format) {
'pdf' => new PdfReportBuilder(),
'excel' => new ExcelReportBuilder(),
default => throw new InvalidArgumentException(
'Unsupported report format'
),
};
}
}
سپس:
$report = ReportBuilderFactory::make('pdf')
->title('Monthly Report')
->author('CMS Nevis')
->build();
در اینجا:
- Factory نوع Builder را انتخاب میکند.
- Builder Object نهایی را مرحلهبهمرحله میسازد.
# Builder در Laravel
در Laravel نمونههای زیادی از سبک Builder مشاهده میشود.
یکی از شناختهشدهترین نمونهها، Query Builder است:
$users = DB::table('users')
->where('active', true)
->where('role', 'admin')
->orderBy('created_at', 'desc')
->get();
همچنین Eloquent نیز API زنجیرهای مشابهی دارد:
$products = Product::query()
->where('active', true)
->where('price', '>', 100000)
->latest()
->paginate(20);
این کدها از نظر تجربه استفاده، شبیه Builder هستند؛ یعنی Query به صورت مرحلهای ساخته میشود.
البته نباید هر API زنجیرهای را بدون بررسی، دقیقاً یک پیادهسازی کلاسیک Builder Pattern در نظر بگیریم. اما از نظر مفهوم، ساخت مرحلهای یک Object یا Query را نشان میدهد.
# ساخت Order Builder در Laravel
فرض کنید در یک پروژه فروشگاهی میخواهیم سفارش بسازیم.
یک Builder ساده:
class OrderBuilder
{
private array $items = [];
private ?int $userId = null;
private ?string $shippingAddress = null;
private string $paymentMethod = 'online';
public function user(int $userId): self
{
$this->userId = $userId;
return $this;
}
public function addItem(
int $productId,
int $quantity
): self {
$this->items[] = [
'product_id' => $productId,
'quantity' => $quantity,
];
return $this;
}
public function shippingAddress(string $address): self
{
$this->shippingAddress = $address;
return $this;
}
public function paymentMethod(string $method): self
{
$this->paymentMethod = $method;
return $this;
}
public function build(): array
{
if ($this->userId === null) {
throw new LogicException(
'User is required'
);
}
if ($this->items === []) {
throw new LogicException(
'At least one item is required'
);
}
return [
'user_id' => $this->userId,
'items' => $this->items,
'shipping_address' => $this->shippingAddress,
'payment_method' => $this->paymentMethod,
];
}
}
استفاده:
$order = (new OrderBuilder())
->user(10)
->addItem(100, 2)
->addItem(200, 1)
->shippingAddress('Tehran')
->paymentMethod('online')
->build();
در پروژه واقعی، بهتر است ذخیره سفارش و آیتمهای آن در یک Service یا Action جدا انجام شود و Builder فقط مسئول آمادهسازی داده یا Object باشد.
# آیا Builder باید همیشه یک Class جدا باشد؟
خیر.
در پروژههای کوچک میتوان با Constructor، Named Arguments یا یک متد Static ساده نیاز را برطرف کرد.
مثلاً:
final class User
{
public function __construct(
public string $name,
public string $email,
public bool $active = true
) {
}
public static function create(
string $name,
string $email
): self {
return new self(
name: $name,
email: $email
);
}
}
استفاده:
$user = User::create(
'Saeed',
'saeed@example.com'
);
این روش از نظر ساختار، Builder کامل نیست، اما برای Objectهای ساده ممکن است انتخاب مناسبتری باشد.
# اشتباهات رایج در Builder Pattern
1. ساخت Builder برای Objectهای ساده
اگر یک Class فقط دو یا سه Property دارد، Builder ممکن است پیچیدگی اضافه ایجاد کند.
2. نداشتن اعتبارسنجی در build
اگر Propertyهای ضروری بررسی نشوند، ممکن است Object ناقص ساخته شود.
3. ترکیب مسئولیت ساخت و ذخیرهسازی
Builder بهتر است Object را بسازد، نه اینکه مستقیماً آن را در دیتابیس ذخیره کند.
4. ساخت Builder بسیار بزرگ
اگر Builder صدها متد داشته باشد، باید طراحی آن را دوباره بررسی کنیم.
5. تغییر Object نهایی بدون کنترل
اگر هدف ساخت Immutable Object است، بهتر است Object نهایی پس از ایجاد قابل تغییر نباشد.
6. استفاده از Builder به جای Service
Builder برای ساخت Object مناسب است؛ اما منطق سنگین Business Logic باید در Service یا لایه مناسب قرار بگیرد.
# مزایای Builder Pattern
استفاده درست از Builder میتواند مزایای زیر را داشته باشد:
- خواناتر شدن ساخت Object
- مدیریت بهتر پارامترهای اختیاری
- کاهش Constructorهای طولانی
- امکان ساخت مرحلهای Object
- امکان اعتبارسنجی در پایان
- مناسب برای Objectهای پیچیده
- پشتیبانی از Fluent Interface
- کمک به ساخت Objectهای Immutable
# معایب Builder Pattern
Builder نیز مانند هر Pattern دیگری معایبی دارد:
- تعداد Classها را افزایش میدهد.
- برای Objectهای ساده بیش از حد پیچیده است.
- ممکن است کد تکراری ایجاد کند.
- نگهداری Builder و Class اصلی باید هماهنگ باشد.
- اگر درست طراحی نشود، تعداد متدهای آن بسیار زیاد میشود.
پس استفاده از Builder باید بر اساس پیچیدگی واقعی Object انجام شود.
# چه زمانی از Builder استفاده کنیم؟
Builder معمولاً در شرایط زیر مناسب است:
- Object پارامترهای زیادی دارد.
- تعداد زیادی از پارامترها اختیاری هستند.
- ترتیب ساخت اهمیت دارد.
- ساخت Object چند مرحله دارد.
- ساخت Object نیاز به اعتبارسنجی دارد.
- میخواهیم کد ساخت خواناتر باشد.
- Object نهایی باید Immutable باشد.
- ساخت Object شامل تنظیمات مختلف است.
# چه زمانی از Builder استفاده نکنیم؟
اگر Object ساده است:
$user = new User(
name: 'Saeed',
email: 'saeed@example.com'
);
احتمالاً نیازی به Builder نداریم.
همچنین اگر Named Arguments مشکل خوانایی را حل کردهاند، ساخت Builder ممکن است فقط تعداد Classها را افزایش دهد.
# جمعبندی
Builder Pattern در PHP یکی از الگوهای طراحی Creational است که برای ساخت مرحلهای Objectهای پیچیده استفاده میشود.
این Pattern زمانی مفید است که:
- Constructor طولانی شده باشد.
- Propertyهای اختیاری زیادی داشته باشیم.
- ساخت Object چند مرحلهای باشد.
- نیاز به اعتبارسنجی قبل از ایجاد Object داشته باشیم.
- بخواهیم کد ساخت خواناتر شود.
ساختار معمول Builder شامل متدهایی برای تنظیم Propertyها و یک متد نهایی مانند build() است:
$object = (new ExampleBuilder())
->firstStep()
->secondStep()
->thirdStep()
->build();
Builder با Factory تفاوت دارد.
Factory معمولاً مشخص میکند چه Objectی ساخته شود، اما Builder مشخص میکند Object چگونه ساخته شود.
همچنین نباید برای هر Class از Builder استفاده کنیم. برای Objectهای ساده، Constructor و Named Arguments معمولاً انتخابهای بهتر و سادهتری هستند.
# سوالات متداول Builder Pattern در PHP
Builder Pattern در PHP چیست؟
Builder Pattern یک الگوی طراحی برای ساخت مرحلهای Objectهای پیچیده است. این Pattern بهخصوص زمانی مفید است که Object پارامترهای زیاد یا تنظیمات اختیاری داشته باشد.
متد build در Builder چه کاری انجام میدهد؟
متد build() معمولاً پس از تنظیم تمام Propertyها، Object نهایی را ایجاد و برمیگرداند. میتوان اعتبارسنجیهای لازم را نیز در این متد انجام داد.
تفاوت Builder و Factory چیست؟
Factory معمولاً برای انتخاب و ایجاد نوع مناسب Object استفاده میشود، اما Builder برای ساخت مرحلهای یک Object با تنظیمات مختلف کاربرد دارد.
آیا Builder برای همه Classها لازم است؟
خیر. اگر Class ساده باشد، استفاده از Constructor یا Named Arguments معمولاً مناسبتر است.
Fluent Interface در Builder چیست؟
Fluent Interface سبکی است که در آن متدها معمولاً خود Builder را برمیگردانند تا بتوان چند متد را پشت سر هم فراخوانی کرد.
آیا Builder در Laravel استفاده میشود؟
بله. Query Builder و APIهای زنجیرهای Eloquent از نظر سبک استفاده، نمونههایی از ساخت مرحلهای Query هستند.
آیا Builder میتواند Object Immutable بسازد؟
بله. Builder میتواند Propertyها را جمعآوری کند و در پایان Objectی با Propertyهای readonly یا ساختار Immutable ایجاد کند.
آیا Builder میتواند اطلاعات را در دیتابیس ذخیره کند؟
از نظر فنی ممکن است، اما بهتر است Builder مسئول ساخت Object یا داده باشد و عملیات ذخیرهسازی در Service، Repository یا لایه مناسب دیگری انجام شود.





