وقتی در یک پروژه PHP با موجودیتهایی مثل سفارش، پرداخت، تیکت، سند، اشتراک یا حساب کاربری کار میکنیم، معمولاً رفتار این موجودیتها با توجه به وضعیت فعلی آنها تغییر میکند.
برای مثال یک سفارش ممکن است در وضعیتهای زیر قرار داشته باشد:
- در انتظار پرداخت
- پرداخت شده
- ارسال شده
- لغو شده
اما مشکل زمانی شروع میشود که بخواهیم برای هر وضعیت، رفتار متفاوتی برای متدهای مختلف تعریف کنیم.
در چنین شرایطی معمولاً کدی شبیه زیر ایجاد میشود:
if ($order->status === 'pending') {
// ...
} elseif ($order->status === 'paid') {
// ...
} elseif ($order->status === 'shipped') {
// ...
}
و همین منطق بهمرور در چندین متد مختلف تکرار میشود.
نتیجه معمولاً یک کلاس بزرگ با تعداد زیادی if، else و switch است که نگهداری و توسعه آن سخت میشود.
اینجاست که State Pattern در PHP میتواند راهحل مناسبی باشد.
State یکی از الگوهای طراحی رفتاری یا Behavioral Design Patterns است. هدف اصلی آن این است که رفتار وابسته به وضعیت را در کلاسهای جداگانه قرار دهد و Context به State فعلی خود واگذار کند که عملیات را چگونه انجام دهد.
در این مقاله ابتدا مفهوم State Pattern را بررسی میکنیم و سپس آن را با یک مثال واقعی از مدیریت وضعیت سفارش در PHP پیادهسازی خواهیم کرد.
State Pattern چیست؟
State Pattern یک الگوی طراحی رفتاری است که به یک Object اجازه میدهد با تغییر وضعیت داخلی خود، رفتار متفاوتی داشته باشد.
در این الگو بهجای اینکه یک کلاس برای هر وضعیت دائماً شرطهای مختلف را بررسی کند، رفتار مربوط به هر وضعیت در یک کلاس State جداگانه قرار میگیرد.
به زبان ساده:
> بهجای اینکه بگوییم «اگر وضعیت سفارش Pending بود این کار را انجام بده، اگر Paid بود آن کار را انجام بده»، منطق هر وضعیت را داخل کلاس مربوط به همان وضعیت قرار میدهیم.
در این معماری، Object اصلی که معمولاً Context نامیده میشود، یک State فعلی دارد و عملیات خود را به آن State واگذار میکند. با تغییر State، رفتار Context نیز تغییر میکند.
برای مثال:
Order
│
└── Current State
│
├── PendingState
├── PaidState
├── ShippedState
└── CancelledState
اگر Order در وضعیت PendingState باشد، رفتار آن مطابق قوانین Pending خواهد بود.
اگر State به PaidState تغییر کند، همان Order بدون اینکه ساختار اصلی آن تغییر کند، رفتار متفاوتی خواهد داشت.
# چرا به State Pattern نیاز داریم؟
فرض کنید یک سیستم فروشگاهی داریم.
هر سفارش میتواند یکی از وضعیتهای زیر را داشته باشد:
pending
paid
shipped
cancelled
حالا فرض کنیم سفارش متدهای زیر را دارد:
$order->pay();
$order->ship();
$order->cancel();
رفتار هر متد بر اساس وضعیت سفارش تغییر میکند.
برای مثال:
سفارش Pending
میتواند:
- پرداخت شود
- لغو شود
ولی نمیتواند:
- ارسال شود
سفارش Paid
میتواند:
- ارسال شود
- در بعضی سیستمها لغو شود
ولی نمیتواند:
- دوباره پرداخت شود
سفارش Shipped
معمولاً:
- قابل ارسال مجدد نیست
- قابل پرداخت نیست
- قابل لغو نیست
سفارش Cancelled
دیگر عملیات عادی روی آن انجام نمیشود.
اگر همه این قوانین را داخل کلاس Order قرار دهیم، خیلی سریع با کدهای شرطی زیادی مواجه میشویم.
# مشکل استفاده از if و switch
یک پیادهسازی ساده ممکن است این شکلی باشد:
final class Order
{
public function __construct(
public string $status = 'pending'
) {}
public function pay(): void
{
if ($this->status === 'pending') {
$this->status = 'paid';
return;
}
if ($this->status === 'paid') {
throw new LogicException('Order is already paid.');
}
if ($this->status === 'shipped') {
throw new LogicException('Shipped order cannot be paid.');
}
if ($this->status === 'cancelled') {
throw new LogicException('Cancelled order cannot be paid.');
}
}
public function ship(): void
{
if ($this->status === 'pending') {
throw new LogicException('Order must be paid first.');
}
if ($this->status === 'paid') {
$this->status = 'shipped';
return;
}
if ($this->status === 'shipped') {
throw new LogicException('Order is already shipped.');
}
if ($this->status === 'cancelled') {
throw new LogicException('Cancelled order cannot be shipped.');
}
}
}
در نگاه اول شاید این کد مشکلی نداشته باشد.
اما تصور کنید پروژه بزرگتر شود و متدهای دیگری مثل موارد زیر اضافه شوند:
refund()
cancel()
sendEmail()
generateInvoice()
downloadInvoice()
changeAddress()
applyDiscount()
حالا تقریباً در هر متد باید وضعیت سفارش را بررسی کنیم.
مشکل اصلی فقط طولانی شدن کد نیست.
مشکل این است که قوانین مربوط به هر State در قسمتهای مختلف کلاس پخش میشوند.
اگر State جدیدی مثل:
refunded
partially_paid
on_hold
returned
اضافه شود، ممکن است مجبور شویم چندین متد را تغییر دهیم.
State Pattern دقیقاً برای حل چنین شرایطی طراحی شده است.
# ساختار State Pattern
State Pattern معمولاً از چند بخش اصلی تشکیل میشود:
1. Context
Context همان Object اصلی است که وضعیت فعلی را نگهداری میکند.
در مثال ما:
Order
نقش Context را دارد.
2. State Interface
این Interface قرارداد مشترک تمام Stateها را مشخص میکند.
مثلاً:
interface OrderState
{
public function pay(Order $order): void;
public function ship(Order $order): void;
public function cancel(Order $order): void;
}
تمام Stateهای سفارش باید این Interface را پیادهسازی کنند.
3. Concrete State
هر وضعیت واقعی یک Concrete State است.
مثلاً:
PendingState
PaidState
ShippedState
CancelledState
هر کلاس قوانین مخصوص وضعیت خودش را پیادهسازی میکند.
# پیادهسازی State Pattern در PHP
حالا یک نمونه واقعی و قابل استفاده ایجاد کنیم.
در این مثال از PHP 8.2+ استفاده میکنیم.
ابتدا Interface را ایجاد میکنیم:
interface OrderState
{
public function pay(Order $order): void;
public function ship(Order $order): void;
public function cancel(Order $order): void;
}
این Interface میگوید هر State باید بتواند درباره سه عملیات تصمیم بگیرد:
pay
ship
cancel
اما اینکه هر عملیات دقیقاً چه کاری انجام دهد، به State مربوطه بستگی دارد.
# ایجاد Context
حالا کلاس Order را ایجاد میکنیم:
final class Order
{
private OrderState $state;
public function __construct()
{
$this->state = new PendingState();
}
public function setState(OrderState $state): void
{
$this->state = $state;
}
public function pay(): void
{
$this->state->pay($this);
}
public function ship(): void
{
$this->state->ship($this);
}
public function cancel(): void
{
$this->state->cancel($this);
}
}
نکته مهم اینجاست.
کلاس Order دیگر نمیگوید:
if ($status === 'pending')
یا:
switch ($status)
بلکه فقط میگوید:
$this->state->pay($this);
این State فعلی است که تصمیم میگیرد چه اتفاقی باید بیفتد.
# ایجاد PendingState
حالا وضعیت Pending را ایجاد میکنیم:
final class PendingState implements OrderState
{
public function pay(Order $order): void
{
echo "Payment successful.\n";
$order->setState(new PaidState());
}
public function ship(Order $order): void
{
throw new LogicException(
'Order must be paid before shipping.'
);
}
public function cancel(Order $order): void
{
echo "Order cancelled.\n";
$order->setState(new CancelledState());
}
}
در این State:
pay()
مجاز است.
بعد از پرداخت، وضعیت سفارش تغییر میکند:
Pending → Paid
اما:
ship()
مجاز نیست.
و:
cancel()
باعث تغییر وضعیت به:
Pending → Cancelled
میشود.
# ایجاد PaidState
حالا وضعیت Paid را ایجاد میکنیم:
final class PaidState implements OrderState
{
public function pay(Order $order): void
{
throw new LogicException(
'Order is already paid.'
);
}
public function ship(Order $order): void
{
echo "Order shipped.\n";
$order->setState(new ShippedState());
}
public function cancel(Order $order): void
{
echo "Order cancelled after payment.\n";
$order->setState(new CancelledState());
}
}
در این وضعیت:
pay() ❌
ship() ✅
cancel() ✅
بنابراین اگر:
$order->pay();
دوباره اجرا شود، State خودش خطا ایجاد میکند.
# ایجاد ShippedState
final class ShippedState implements OrderState
{
public function pay(Order $order): void
{
throw new LogicException(
'Shipped order cannot be paid.'
);
}
public function ship(Order $order): void
{
throw new LogicException(
'Order is already shipped.'
);
}
public function cancel(Order $order): void
{
throw new LogicException(
'Shipped order cannot be cancelled.'
);
}
}
در این State هیچکدام از عملیات عادی قابل اجرا نیستند.
# ایجاد CancelledState
در نهایت:
final class CancelledState implements OrderState
{
public function pay(Order $order): void
{
throw new LogicException(
'Cancelled order cannot be paid.'
);
}
public function ship(Order $order): void
{
throw new LogicException(
'Cancelled order cannot be shipped.'
);
}
public function cancel(Order $order): void
{
throw new LogicException(
'Order is already cancelled.'
);
}
}
حالا تمام قوانین وضعیت سفارش از کلاس اصلی Order خارج شدهاند.
# استفاده از State Pattern
حالا میتوانیم از سیستم استفاده کنیم:
$order = new Order();
$order->pay();
$order->ship();
در ابتدا:
Pending
است.
بعد از:
$order->pay();
به:
Paid
تغییر میکند.
و بعد:
$order->ship();
باعث میشود به:
Shipped
برود.
یعنی:
Pending
↓ pay()
Paid
↓ ship()
Shipped
در واقع رفتار Object در طول عمر آن تغییر کرده است.
# State Transition چیست؟
یکی از مفاهیم بسیار مهم در State Pattern، State Transition یا انتقال وضعیت است.
مثلاً در سیستم سفارش:
Pending → Paid
Paid → Shipped
Pending → Cancelled
Paid → Cancelled
اما بعضی انتقالها غیرمجاز هستند:
Shipped → Paid
Cancelled → Shipped
Shipped → Pending
یکی از مزایای State Pattern این است که میتوان قوانین انتقال را نزدیک به همان State قرار داد.
مثلاً:
final class PaidState implements OrderState
{
public function ship(Order $order): void
{
$order->setState(new ShippedState());
}
}
خود PaidState میداند که بعد از ارسال باید چه اتفاقی بیفتد.
# آیا Context باید State بعدی را تعیین کند؟
یکی از اشتباهات رایج این است که State Pattern را اینگونه پیادهسازی کنیم:
public function changeState(): void
{
switch ($this->status) {
case 'pending':
$this->state = new PaidState();
break;
case 'paid':
$this->state = new ShippedState();
break;
}
}
این کار عملاً دوباره همان مشکل قبلی را ایجاد میکند.
اگر State Pattern را برای حذف شرطهای وضعیت استفاده میکنیم، بهتر است قوانین انتقال نیز تا حد امکان در خود Stateها قرار بگیرند.
# State Pattern با PHP Enum
در PHP مدرن میتوان وضعیت ذخیرهشده در دیتابیس را با Enum نیز مدیریت کرد.
مثلاً:
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
}
این کار برای نمایش و ذخیره وضعیت بسیار مفید است.
اما باید یک نکته مهم را در نظر داشته باشیم:
Enum بهتنهایی State Pattern نیست.
Enum میتواند وضعیت را مشخص کند:
$order->status === OrderStatus::Paid
اما State Pattern علاوه بر نگهداری مفهوم State، رفتار مربوط به هر State را نیز در Objectهای جداگانه قرار میدهد.
در پروژههای واقعی میتوان این دو روش را با هم استفاده کرد:
Database
↓
status = paid
↓
OrderStatus::Paid
↓
PaidState
↓
State-specific behavior
این معماری برای پروژههای Laravel نیز میتواند بسیار کاربردی باشد.
# استفاده از State Pattern در Laravel
در پروژههای Laravel معمولاً وضعیت در دیتابیس ذخیره میشود.
برای مثال جدول:
orders
ممکن است ستونی به نام:
status
داشته باشد.
مقدار آن نیز مثلاً:
pending
paid
shipped
cancelled
باشد.
یک ساختار تمیز برای Stateها میتواند اینگونه باشد:
app/
└── Domain/
└── Orders/
└── States/
├── OrderState.php
├── PendingState.php
├── PaidState.php
├── ShippedState.php
└── CancelledState.php
این ساختار باعث میشود منطق مربوط به وضعیت سفارش از Model اصلی جدا شود.
# State Resolver در Laravel
یکی از مشکلات پروژه واقعی این است که سفارش از دیتابیس با یک String دریافت میشود.
مثلاً:
$order->status
ممکن است مقدار زیر را داشته باشد:
paid
حالا باید آن را به Object مناسب تبدیل کنیم.
میتوانیم یک Resolver داشته باشیم:
final class OrderStateResolver
{
public function resolve(Order $order): OrderState
{
return match ($order->status) {
'pending' => new PendingState(),
'paid' => new PaidState(),
'shipped' => new ShippedState(),
'cancelled' => new CancelledState(),
default => throw new LogicException(
"Unknown order state: {$order->status}"
),
};
}
}
اینجا یک match داریم، اما تفاوت مهمی وجود دارد.
این Mapping فقط در یک نقطه انجام میشود.
منطق اصلی رفتار Stateها داخل کلاسهای State قرار دارد و در تمام متدهای Order تکرار نمیشود.
# آیا استفاده از State Pattern همیشه لازم است؟
خیر.
این یکی از مهمترین نکات Design Patternهاست.
اگر پروژه شما فقط دو وضعیت دارد:
active
inactive
و تنها یک متد تحت تأثیر این وضعیت است، احتمالاً State Pattern بیش از حد پیچیده است.
مثلاً:
if ($user->active) {
// ...
}
کاملاً قابل قبول است.
اما اگر با چیزی شبیه این مواجه هستید:
5 State
10 Action
20 Transition
و در بسیاری از متدها وضعیت فعلی بررسی میشود، State Pattern میتواند ارزش زیادی ایجاد کند.
# مزایای State Pattern در PHP
1. کاهش شرطهای پیچیده
بهجای:
if
elseif
else
switch
منطق هر وضعیت در کلاس مخصوص خودش قرار میگیرد.
2. خوانایی بیشتر
بهجای اینکه یک کلاس 1000 خطی داشته باشیم، منطق به چند کلاس کوچک تقسیم میشود.
3. رعایت Single Responsibility Principle
هر State مسئول رفتار یک وضعیت مشخص است.
مثلاً:
PendingState
مسئول رفتار سفارش Pending است.
4. توسعه آسانتر
اگر State جدیدی اضافه شود:
RefundedState
میتوان یک کلاس جدید ایجاد کرد، بدون اینکه مجبور باشیم تمام منطق قبلی را تغییر دهیم.
5. تستپذیری بهتر
میتوان هر State را بهصورت مستقل تست کرد.
مثلاً:
public function test_pending_order_can_be_paid(): void
{
$order = new Order();
$order->pay();
// assert paid state
}
یا:
public function test_shipped_order_cannot_be_paid(): void
{
// ...
}
# معایب State Pattern
State Pattern با وجود مزایای زیاد، همیشه انتخاب مناسبی نیست.
1. افزایش تعداد کلاسها
برای چهار وضعیت ممکن است چهار کلاس ایجاد کنیم:
PendingState
PaidState
ShippedState
CancelledState
در پروژههای کوچک شاید این ساختار بیش از نیاز باشد.
2. پیچیده شدن معماری
اگر تعداد Stateها و Transitionها بسیار زیاد شود، مدیریت ارتباط بین آنها نیز میتواند پیچیده شود.
3. احتمال Overengineering
نباید برای هر status ساده، چندین کلاس بسازیم.
Design Pattern باید برای حل یک مشکل واقعی استفاده شود، نه صرفاً برای اینکه کد از نظر معماری «پیشرفتهتر» به نظر برسد.
# تفاوت State Pattern و Strategy Pattern
State و Strategy از نظر ساختار کد بسیار شبیه یکدیگر هستند، زیرا هر دو از Interface و Objectهای قابل تعویض استفاده میکنند.
اما هدف آنها متفاوت است.
Strategy
در Strategy معمولاً میخواهیم یک الگوریتم را انتخاب کنیم.
مثلاً:
PaymentStrategy
├── StripePayment
├── ZarinpalPayment
└── PayPalPayment
انتخاب Strategy معمولاً از بیرون انجام میشود.
State
در State رفتار Object با وضعیت داخلی آن تغییر میکند.
مثلاً:
Order
├── PendingState
├── PaidState
└── ShippedState
در اینجا تغییر وضعیت بخشی از Lifecycle خود Object است.
بنابراین به شکل ساده:
Strategy = چگونه این کار را انجام بدهم؟
State = الان در چه وضعیتی هستم و در این وضعیت چه رفتاری دارم؟
این تفاوت یکی از مهمترین نکات هنگام انتخاب بین این دو Pattern است.
# تفاوت State Pattern و Command Pattern
در مقاله قبلی درباره Command Pattern صحبت کردیم.
Command برای تبدیل یک درخواست یا عملیات به یک Object استفاده میشود.
مثلاً:
CreateOrderCommand
SendEmailCommand
DeleteProductCommand
اما State برای تغییر رفتار Object بر اساس وضعیت آن استفاده میشود.
بهصورت ساده:
Command
→ یک Action را به Object تبدیل میکند.
State
→ رفتار Object را بر اساس وضعیت آن تغییر میدهد.
بنابراین:
Command = What action should be executed?
State = How should this object behave in its current state?
# تفاوت State Pattern و Observer Pattern
Observer نیز با State متفاوت است.
در Observer زمانی که اتفاقی رخ میدهد، سایر Objectها مطلع میشوند.
مثلاً:
Order Paid
↓
Observer / Listener
├── SendEmail
├── SendSMS
└── UpdateAnalytics
اما State مسئول رفتار خود Object در وضعیت فعلی است.
برای مثال:
Order
↓
PaidState
میتواند مشخص کند که سفارش Paid چگونه رفتار کند.
در یک سیستم واقعی حتی میتوان State و Observer را در کنار یکدیگر استفاده کرد.
مثلاً:
Pending
↓
Payment
↓
PaidState
↓
OrderPaid Event
↓
Listeners
# State Pattern و SOLID
State Pattern میتواند به رعایت چند اصل SOLID کمک کند.
مهمترین آنها:
Single Responsibility Principle
هر State مسئول منطق یک وضعیت است.
Open/Closed Principle
برای اضافه کردن State جدید، در بسیاری از موارد میتوان کلاس جدید اضافه کرد، بدون اینکه منطق Stateهای موجود را تغییر داد.
Dependency Inversion Principle
Context میتواند به Interface وابسته باشد:
private OrderState $state;
نه به یک کلاس Concrete خاص.
# تست State Pattern با PHPUnit
یکی از مزایای مهم این معماری تستپذیری آن است.
برای مثال میتوان تست کرد که سفارش Pending بعد از پرداخت باید Paid شود:
public function test_pending_order_can_be_paid(): void
{
$order = new Order();
$order->pay();
// Assert that order is now paid.
}
همچنین میتوان یک Transition غیرمجاز را تست کرد:
public function test_pending_order_cannot_be_shipped(): void
{
$this->expectException(LogicException::class);
$order = new Order();
$order->ship();
}
و:
public function test_shipped_order_cannot_be_cancelled(): void
{
$this->expectException(LogicException::class);
// Create shipped order...
$order->cancel();
}
این نوع تستها کمک میکنند قوانین Business Logic مربوط به Stateها بهصورت شفاف بررسی شوند.
# اشتباهات رایج در پیادهسازی State Pattern
اشتباه اول: نگه داشتن تمام منطق در Context
اگر همچنان در Order تعداد زیادی شرط داشته باشیم:
if ($this->state === ...)
احتمالاً هنوز از مزیت اصلی State Pattern استفاده نکردهایم.
اشتباه دوم: ایجاد Stateهای بیش از حد
اگر هر تغییر کوچک باعث ایجاد یک کلاس جدید شود، معماری بیش از حد پیچیده میشود.
اشتباه سوم: Stateهای بسیار بزرگ
اگر:
PendingState.php
خودش تبدیل به یک Service با 1000 خط کد شود، فقط مشکل را از یک کلاس به کلاس دیگر منتقل کردهایم.
State باید روی رفتار مرتبط با همان وضعیت تمرکز داشته باشد.
اشتباه چهارم: ترکیب وضعیت و Persistence
State نباید لزوماً مسئول تمام عملیات دیتابیس باشد.
برای مثال بهتر است State مجبور نباشد تمام Queryهای مربوط به سفارش را خودش مدیریت کند.
در پروژههای بزرگتر میتوان مسئولیتها را بین:
Domain
Service
Repository
State
Model
به شکل مشخص تقسیم کرد.
# چه زمانی از State Pattern استفاده کنیم؟
State Pattern انتخاب مناسبی است وقتی:
- رفتار Object با وضعیت آن تغییر میکند.
- وضعیتهای مختلف Business Logic متفاوتی دارند.
- شرطهای وضعیت در چندین متد تکرار شدهاند.
- تعداد Stateها در حال افزایش است.
- Transition بین Stateها اهمیت دارد.
- Lifecycle مشخصی برای یک Entity وجود دارد.
- میخواهیم منطق هر وضعیت را جداگانه تست کنیم.
نمونههای واقعی:
Order
Payment
Invoice
Ticket
Subscription
Document
User Account
Connection
Workflow
Delivery
Booking
# چه زمانی از State Pattern استفاده نکنیم؟
اگر مسئله خیلی ساده است، از Pattern استفاده نکنید.
مثلاً:
if ($user->is_active) {
// ...
}
لزومی ندارد برای چنین شرط سادهای چند کلاس State ایجاد کنیم.
همچنین اگر فقط یک متد تحت تأثیر State قرار دارد، احتمالاً یک شرط ساده یا Enum کافی است.
# State Pattern در سیستمهای Workflow
یکی از کاربردهای مهم State Pattern، سیستمهای Workflow است.
برای مثال یک مقاله ممکن است وضعیتهای زیر را داشته باشد:
Draft
↓
PendingReview
↓
Approved
↓
Published
اما ممکن است:
Draft → Published
مجاز نباشد.
در چنین شرایطی هر State میتواند قوانین Transition خودش را مشخص کند.
مثلاً:
final class DraftState implements ArticleState
{
public function submit(Article $article): void
{
$article->setState(
new PendingReviewState()
);
}
public function publish(Article $article): void
{
throw new LogicException(
'Draft article cannot be published directly.'
);
}
}
این ساختار برای CMSها، سیستمهای انتشار محتوا و Workflowهای تجاری بسیار کاربردی است.
# State Pattern در سیستم پرداخت
پرداخت نیز یکی از نمونههای مناسب برای State Pattern است.
مثلاً:
Pending
Paid
Failed
Refunded
Cancelled
هر وضعیت میتواند قوانین متفاوتی داشته باشد.
مثلاً:
Pending
↓
Paid
↓
Refunded
یا:
Pending
↓
Failed
↓
Retry
در چنین سیستمهایی نگه داشتن تمام قوانین در یک Model بزرگ میتواند بسیار سخت شود.
State Pattern کمک میکند رفتار هر مرحله از Lifecycle پرداخت جدا شود.
# State Pattern در Laravel؛ معماری پیشنهادی
برای یک پروژه Laravel متوسط یا بزرگ میتوان ساختاری مشابه زیر داشت:
app/
└── Domain/
└── Orders/
├── Models/
│ └── Order.php
│
├── States/
│ ├── OrderState.php
│ ├── PendingState.php
│ ├── PaidState.php
│ ├── ShippedState.php
│ └── CancelledState.php
│
├── Services/
│ └── OrderService.php
│
└── Exceptions/
└── InvalidOrderTransition.php
این ساختار باعث میشود Business Logic مربوط به وضعیتها از سایر قسمتهای برنامه جدا شود.
# آیا State Pattern همان State Machine است؟
خیر، ولی ارتباط نزدیکی با یکدیگر دارند.
State Pattern یک Design Pattern برای کپسولهسازی رفتار وابسته به State است.
در مقابل، Finite State Machine یا FSM یک مدل برای تعریف مجموعهای از وضعیتها و Transitionهای مجاز بین آنهاست.
میتوان State Pattern را برای پیادهسازی یک State Machine استفاده کرد. نمونههای PHP نیز State Pattern را برای مدلسازی فرآیندها و Workflowها بهکار میبرند.
بهعنوان مثال:
Pending
│
├── pay ─────→ Paid
│
└── cancel ──→ Cancelled
Paid
│
└── ship ────→ Shipped
این نمودار عملاً بخشی از State Machine سفارش را نشان میدهد.
# جمعبندی State Pattern در PHP
State Pattern یکی از الگوهای طراحی رفتاری مهم در برنامهنویسی شیگرا است.
هدف اصلی آن این است که وقتی رفتار یک Object بر اساس وضعیت فعلی آن تغییر میکند، منطق مربوط به هر وضعیت را در کلاس مخصوص همان State قرار دهیم.
در روش سنتی ممکن است با چنین ساختاری مواجه شویم:
if ($status === 'pending') {
// ...
} elseif ($status === 'paid') {
// ...
} elseif ($status === 'shipped') {
// ...
}
اما در State Pattern:
Order
↓
Current State
↓
PendingState / PaidState / ShippedState
Context عملیات را به State فعلی واگذار میکند و با تغییر State، رفتار Object نیز تغییر میکند. این دقیقاً همان ایده اصلی State Pattern است.
با این حال نباید State Pattern را برای هر شرط سادهای استفاده کرد. زمانی که وضعیتها زیاد میشوند، قوانین Transition پیچیده میشوند و رفتار Object در متدهای مختلف بر اساس State تغییر میکند، این Pattern میتواند ساختار کد را بسیار تمیزتر و قابل نگهداریتر کند.
# سوالات متداول درباره State Pattern در PHP
State Pattern در PHP چیست؟
State Pattern یک Behavioral Design Pattern است که رفتار وابسته به وضعیت یک Object را در کلاسهای جداگانه قرار میدهد. Context به State فعلی خود delegate میکند و با تغییر State، رفتار آن نیز تغییر میکند.
State Pattern چه مشکلی را حل میکند؟
مهمترین مشکل آن، مدیریت شرطهای متعدد مربوط به وضعیت Object است. بهجای تکرار if و switch در چندین متد، منطق هر وضعیت در یک کلاس مستقل قرار میگیرد.
تفاوت State و Strategy چیست؟
Strategy معمولاً برای انتخاب یک الگوریتم یا روش انجام کار استفاده میشود، در حالی که State برای تغییر رفتار Object در طول Lifecycle و بر اساس وضعیت داخلی آن استفاده میشود.
آیا State Pattern در Laravel کاربرد دارد؟
بله. State Pattern میتواند برای سفارشها، پرداختها، Workflowها، تیکتها، اشتراکها، اسناد و سایر Entityهایی که Lifecycle مشخص دارند استفاده شود.
آیا PHP Enum همان State Pattern است؟
خیر. Enum میتواند مقدار وضعیت را به شکل type-safe مدیریت کند، اما State Pattern رفتار وابسته به وضعیت را نیز در Objectهای جداگانه کپسوله میکند.
آیا State Pattern همیشه بهتر از if/else است؟
خیر. برای منطق ساده، if/else کاملاً مناسب است. State Pattern زمانی ارزش بیشتری دارد که تعداد وضعیتها، رفتارها و Transitionها زیاد شده و شرطهای وضعیت در بخشهای مختلف برنامه تکرار شوند.
آیا State Pattern برای پروژههای Laravel بزرگ مناسب است؟
در پروژههایی که Entityهای پیچیده و Lifecycle مشخص دارند، میتواند بسیار مفید باشد؛ مخصوصاً زمانی که منطق وضعیت از Model خارج شده و در لایه Domain یا Business Logic مدیریت شود.
State Pattern چه ارتباطی با State Machine دارد؟
State Pattern میتواند برای پیادهسازی یک State Machine استفاده شود. State Machine روی وضعیتها و Transitionهای مجاز تمرکز دارد، در حالی که State Pattern رفتار مربوط به هر وضعیت را نیز در Objectهای جداگانه قرار میدهد.
# نتیجه نهایی
اگر در کد PHP خود مرتباً با چنین عباراتی مواجه میشوید:
if ($status === ...)
یا:
switch ($status) {
// ...
}
و این شرطها در چندین متد تکرار شدهاند، احتمالاً زمان آن رسیده است که بررسی کنید آیا State Pattern میتواند طراحی شما را بهتر کند یا خیر.
اصل مهم این Pattern ساده است:
State تغییر میکند
↓
رفتار Object تغییر میکند
بهجای اینکه یک کلاس بزرگ تمام وضعیتها را بشناسد، هر State مسئول رفتار خودش میشود.
در مقاله بعدی این کلاستر میتوانیم سراغ Chain of Responsibility Pattern در PHP برویم؛ الگویی که برای عبور دادن یک درخواست از زنجیرهای از Handlerها بسیار کاربردی است و ارتباط خوبی با مباحثی مثل Middleware، Validation و Pipeline در PHP و Laravel دارد.





