سی ام اس نویسسی ام اس نویسیاد بگیر، سیستم خودتو بنویس
مجلهتماس با ما
سی ام اس نویسسی ام اس نویس

CMS نویس مرجعی برای آموزش برنامه نویسی وب و PHP، توسعه افزونه‌های اختصاصی وردپرس، طراحی ورک‌فلو و اتوماسیون با n8n و توسعه CMS های اختصاصی است. هدف ما ارائه آموزش‌های کاربردی و راهکارهای حرفه‌ای برای توسعه‌دهندگان، کسب‌وکارها و علاقه‌مندان به دنیای برنامه‌نویسی است.

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
State Pattern در PHP چیست؟ آموزش الگوی State با مثال عملی

State Pattern در PHP چیست؟ آموزش الگوی State با مثال عملی

ممدیر کل سیستم ۲۱ شهریور ۱۴۰۵ ۲۳ دقیقه مطالعه ۳ بازدید
State Pattern در PHP چیست؟ آموزش الگوی State با مثال عملی

وقتی در یک پروژه 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 دارد.

نظرات (0)

نظرات پس از تأیید مدیر نمایش داده می‌شوند

هنوز نظری ثبت نشده است؛ اولین نفر باشید!

مقالات مرتبط

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

۲۴ دقیقه
ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

۲۲ دقیقه
ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

۲۲ دقیقه
مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

۲۲ دقیقه