اگر بخواهیم جایگاه الگوی طراحی State را در طبقه بندی الگو های طراحی بررسی کنیم، مشخص میشود که این الگو بر اساس هدف، جزء الگو های رفتاری یا Behavioral بوده و بر اساس حوزه، در دسته Object قرار گرفته است.
این الگو کاری میکند که ارتباطات بین کلاسها و موجودیت ها کنترل شود و میتوان آن را نسخه پویا و داینامیک الگوی strategy دانست. رفتار این الگو به این شکل است که وقتی حالت داخلی یک شی تغییر میکند، با توجه به آن تغییر، برنامه رفتار خود را تغییر میدهد و این طور به نظر می رسد که شی، کلاس خود را تغییر داده است. این تغییر با فراخوانی حالت های از پیش تعریف شده درون الگو اتفاق می افتد.
شاید درباره ی ماشینهایstate شنیده باشید. ماشینهای state معمولا با عملگرهای شرطی زیادی (مثل if یا switch) اجرا میشوند که با توجه به وضعیت فعلی شی، رفتار مناسب را انتخاب میکنند. روش الگوی طراحی state، روشی تمیزتر برای یک ماشین state میباشد که میتواند رفتار خود را در زمان اجرا تغییر دهد، بدون این که تبدیل به عبارت بزرگ شرطی شود.
شکل زیر ساختار این الگو را نشان میدهد. که بخشهای مختلف آن هرکدام وظیفه و تعریفی دارند:
· Interface State: این رابط کاربری برای متدهایی است که در هر کلاسِ state پیاده سازی میشوند. این متد ها باید به گونهای باشند که در هر کدام از کلاسهای state، بتوانیم پیاده سازی درست و با معنی از آنها داشت باشیم.
· State: با توجه به معنی کلمه، مشخص است که یک وضعیت را نشان میدهد. در این جا state یک حالت از پیاده سازی متدهایی میباشد که در interface اعلام شده است. به ازای هر state یک کلاس خواهیم داشت. در این کلاس بر اساس شرایط مختلف، متدهای state مربوط به همان شرط، اجرا خواهد شد.
· Context: این کلاس یک شی از state را نگهداری میکند و یا به بیانی دیگر به یک state خاص اشاره میکند. و از طریق interface مربوط به state با شی state ارتباط برقرار میکند.
· Concrete States: در بر گیرنده پیاده سازی براي متد هاي state میباشد. در واقع کلاسهایی هستند که حالت های خاص خود را از متدهایی که در interface اعلام شده است، پیاده سازی میکنند و در Context بر اساس شرایط مختلف، یکی از این کلاسهای Concrete States اجرا خواهد شد.
اگر بخواهیم نحوه ی عملکرد این الگو را جزیی تر بررسی کنیم، ابتدا باید به کلاس Context دقت کنیم که یک متغیر از نوع State را در خود دارد و این متغیر به یکی از شی های ساخته شده از کلاسهای ConcreteStates اشاره میکند. در هر ConcreteStates متدهایی تعریف شده است (مثلا doThisو doThat) که این متدها بر اساس interfaceتعریف میشوند و همه ی عملیات لازم در هر State از طریق این متد ها انجام میشوند.
همانطور که در شکل دیده میشود کلاس Interface State به دادههای Context دسترسی کامل دارد. بنابراین در هر زمانی، هم کلاس Context و هم شی State میتوانند تصمیم بگیرند که تغییر حالت بدهند. این کار با تغییر شی ذخیره شده state درContext انجام میشود. بعد از این تغییر state، همه ی درخواست ها به state جدید ارسال میشوند که ممکن است متدی که در این State انجام میشود با متد State قبلی کاملا رفتار متفاوتی داشته باشد.
ساختار دایرکتوری
نمونه کدهای مختلفی که از این الگوی طراحی استفاده کردهاند را بررسی کردیم ، میتوان ساختار های مختلفی برای دایرکتوری های این الگو در نظر گرفت ولی در ادامه تنها یک نمونه از این ساختار ها ذکر شده است و صرفا پیشنهاد بوده و میتوان بر اساس پروژه، آن را تغییر داد:
- app
|-- States
| |--StateInterface.php
| |--Context.php
|
| |--State
| |--State1.php
| |--State2.php
| |--State3.php
| |--State4.php
لازم به ذکر است نام Context صرفا جهت هماهنگ بودن با ساختار توضیح داده شده با این الگو میباشد و میتوان بر اساس پروژه و نیازها، نام بهتری انتخاب کرد. همچنین علاوه بر این کلاسها، کلاسهای دیگری هم ممکن است نیاز باشد که میتوان آنها را نیز در کنار این کلاسها ایجاد کرد.
همچنین state1.php تا state4.php کلاسهای پیاده سازی شده از interface هستند که در ساختار توضیح داده شده ConcreteStates نام دارد.
اگر نیاز به چند الگوی طراحی state باشد، میتوانیم پوشه هایی با عنوان مناسب، در پوشه ی States ایجاد کرده و درون آنها برای هر کدام، ساختاری را مشابه ساختار بالا پیاده کنیم.
شرایط استفاده از الگوی طراحی State
از الگوی طراحی state در شرایط زیر استفاده میشود:
· پیاده سازی ابزارهای گرافیکی
· اشیائی که با توجه به موقعیت فعلی ، رفتار کنند و در زمان اجرا تغییر کنند.
· اشیائی که در حال پیچیده شدن هستند و شرط های زیادی دارند.
· هنگامی که تعداد کدهای تکراری در حالت های مشابه زیاد شوند و انتقال state ها مبتنی بر شرایط باشد.
مزایا و معایب استفاده از الگوی طراحی State
مزایای استفاده از الگوی طراحی State میتواند موارد زیر باشد:
1. کد مربوط به state های خاص در کلاسهای جداگانه سازماندهی میشود . (Single Responsibility)
2. حالت ها یا state های جدید، بدون تغییر در کلاسهای State موجود یا Context معرفی میشوند.(Open/Closed) در واقع این الگو پیشنهاد میکند همهی حالت های خاص کد را درون مجموعه ای از کلاسهای مجزا قرار دهیم. در نتیجه میتوانیم حالت جدیدی اضافه کنیم یا حالت های موجود را به طور مستقل از یکدیگر، تغییر دهیم و هزینه نگهداری را کاهش دهیم.
3. کد کلاس Context را با حذف شرطهای حجیم ماشین state ساده میکند.
استفاده از الگوی طراحی State ممکن است دارای معایبی باشد:
· اگر یک ماشین state فقط چند حالت داشته باشد یا به ندرت تغییر کند، استفاده از الگوی state باعث پیچیدگی بیشتر میشود.
نمونه کد جهت استفاده از الگوی طراحی State
در این بخش یک مثال از نحوه ی استفاده از الگوی طراحی State ذکر میکنیم . در این مثال یک آسانسور داریم که حالت های مختلفی میتواند داشته باشد و میخواهیم با استفاده از الگوی طراحی State، این حالت ها را پیاده سازی کنیم. یک آسانسور میتواند حالت های زیر را داشته باشد. در شکل زیر، State Diagram آن را مشاهده میکنید:
· باز (Open)
· بسته (Close)
· در حال حرکت (Move)
· ایستاده (Stop)
ساختار دایرکتوری که برای این مثال در نظر گرفتهایم، به صورت زیر خواهد بود:
- app
|-- States
| |--ElevatorStateInterface.php
| |--Elevator.php
|
| |--State
| |--close.php
| |--open.php
| |--move.php
| |--stop.php
1- ابتدا یک interface ایجاد میکنیم و بعد درون این Interface همهی متدهایی که میخواهیم در state ها پیاده سازی شوند را اعلام میکنیم. با توجه به 4 حالت آسانسور، برای تغییر state، متدهای interface را به صورت زیر پیاده سازی میکنیم:
<?php
namespace App\DesignPatterns\States\Elevator;
interface ElevatorStateInterface
{
public function open();
public function close();
public function move();
public function stop();
}
2- سپس بر اساس Interface، کلاسهای State را پیاده سازی میکنیم . در این کلاسها بعضی از متد ها تغییری در context انجام نمیدهند که ممکن است به دو دلیل باشد: یا در همان state فعلی قرار دارند و نیازی به تغییر state ندارند یا این که امکان انتقال به State درخواستی وجود ندارد.
پیاده سازی کلاسهای state را در ادامه میبینید:
پیاده سازی کلاس وضعیت Close:
<?php
namespace App\DesignPatterns\States\Elevator\State;
use App\DesignPatterns\States\Elevator\ElevatorStateInterface;
class Close implements ElevatorStateInterface
{
public function close()
{
echo "Already Closed". PHP_EOL;
return new Close();
}
public function open()
{
return new Open();
}
public function move()
{
return new Move();
}
public function stop()
{
echo "cannot change position from close to stop" . PHP_EOL;
return new Close();
}
}
پیاده سازی کلاس وضعیت Move:
<?php
namespace App\DesignPatterns\States\Elevator\State;
use App\DesignPatterns\States\Elevator\ElevatorStateInterface;
class Move implements ElevatorStateInterface
{
public function move()
{
echo "Already Moving. PHP_EOL ";
return new Move();
}
public function stop()
{
return new Stop();
}
public function close()
{
echo "cannot change state from move to close". PHP_EOL;
return new Move();
}
public function open()
{
echo "cannot change state from move to open". PHP_EOL;
return new Move();
}
}
پیاده سازی کلاس وضعیت Open:
<?php
namespace App\DesignPatterns\States\Elevator\State;
use App\DesignPatterns\States\Elevator\ElevatorStateInterface;
class Open implements ElevatorStateInterface
{
public function open()
{
echo "Already Opened". PHP_EOL;
return new Open();
}
public function close()
{
return new Close();
}
public function move()
{
echo "cannot change state from open to move". PHP_EOL;
return new Open();
}
public function stop()
{
echo "cannot change state from open to stop". PHP_EOL;
return new Open();
}
}
پیاده سازی کلاس وضعیت Stop:
<?php
namespace App\DesignPatterns\States\Elevator\State;
use App\DesignPatterns\States\Elevator\ElevatorStateInterface;
class Stop implements ElevatorStateInterface
{
public function open()
{
return new Open();
}
public function close()
{
return new Close();
}
public function move()
{
return new Move();
}
public function stop()
{
echo "Already Stopped" . PHP_EOL;
return new Stop();
}
}
3- نوبت به پیاده سازی کلاس Context میرسد. در این کلاس بر اساس شرایط مختلف، متدهای State مربوط به همان شرط، اجرا میشود. این کلاس از یکی از state ها استفاده میکند و بوسیله ی interface کلاینت با State های مختلف ارتباط برقرار میکند. در این جا اسم کلاس Context ما Elevator بوده و در متد construct این کلاس، شی ای از کلاس Stop به عنوان اولین state ایجاد شده است.
<?php
namespace App\DesignPatterns\States\Elevator;
use App\DesignPatterns\States\Elevator\State\Open;
use App\DesignPatterns\States\Elevator\State\Stop;
class Elevator
{
private $state;
private function setState(ElevatorStateInterface $state)
{
$this->state = $state;
echo "set state to : " . get_class($state) . PHP_EOL;
}
public function __construct()
{
$this->setState(new Stop());
}
public function open()
{
$this->setState($this->state->open());
}
public function close()
{
$this->setState($this->state->close());
}
public function move()
{
$this->setState($this->state->move());
}
public function stop()
{
$this->setState($this->state->stop());
}
}
4- حالا برای استفاده از این کلاسها، یک کلاس index به صورت زیر پیاده سازی میکنیم.
<?php
namespace App\DesignPatterns\States\Elevator\State;
use App\DesignPatterns\States\Elevator\Elevator;
class index
{
public function elevator()
{
// The elevator is initially stop
$elevator = new Elevator();
$elevator->open();
$elevator->move();
$elevator->close();
$elevator->stop();
}
}
با اجرای کلاس بالا، اتفاقاتی به ترتیب زیر میافتد:
ابتدا یک شی از کلاس Elevator ایجاد میشود. از آن جایی که در متد constructاز کلاس Elevator، شیای از کلاس Stop ایجاد شده است، پس ابتدا دستور echo با مقدار کلاس stop اجرا میشود.
در خط بعدی، متد open از شی ایجاد شده فراخوانی میشود، به این معنی که متد open در کلاس Stop اجرا شده و در آن جا، شی ما تغییر وضعیت داده و شی جدیدی از کلاس Open ایجاد میشود، پس دستور echo برای این کلاس نیز اجرا خواهد شد.
نکتهای که وجود دارد این است که متد setState در کلاس Elevator، مقادیر جدید state را برای هر کدام از متد ها تنظیم میکند. به این معنی که دستور $elevator->open() ابتدا متد open در کلاس Elevator را فراخوانی کرده و در آن کلاس، متد setState با مقدار ورودی $this->state->open فراخوانی شده است. از آن جایی که state در حالت اولیه مربوط به کلاس Stop میباشد، پس متد open از کلاس Stop فراخوانی شده و در آن جا شی جدید ایجاد شده برگردانده میشود.
در خط بعد با اجرای $this->state->move()، متد move در کلاس open اجرا میشود و پیغام مناسب که یک خطا میباشد echo میشود و context بر روی همان شی کلاس open باقی می ماند.
خط بعدی با اجرای $this->state->close، متد close در کلاس open اجرا شده و شی جدید از کلاس Close ایجاد و سپس دستور echo اجرا میشود.
و در نهایت با اجرای خط $this->state->stop، متد stop در کلاس Close اجرا شده و همان echo که در حقیقت یک پیغام خطا میباشد، اجرا میشود. و context بر روی حالتclose باقی می ماند.
خروجی این کلاس به صورت زیر خواهد بود. همانطور که مشاهده میشود، state یک شی درون کلاسها تغییر میکند و شی جدیدی از کلاس دیگر، ایجاد میشود. در جایی که امکان تغییر state وجود نداشته باشد، پیغام مناسب به عنوان هشدار echo شده است.
set state to : App\DesignPatterns\States\Elevator\State\Stop
set state to : App\DesignPatterns\States\Elevator\State\Open
cannot change state from open to move
set state to : App\DesignPatterns\States\Elevator\State\Open
set state to : App\DesignPatterns\States\Elevator\State\Close
cannot change position from close to stop
set state to : App\DesignPatterns\States\Elevator\State\Close
جمعبندی
در این مقاله با یک مثال کاربردی، الگوی طراحی State یا وضعیت را آموزش دادیم. الگوی طراحی State یک الگوی طراحی رفتاری است. به این شکل که وقتی حالت داخلی یک شی تغییر میکند با توجه به آن تغییر، رفتار خود را تغییر میدهد. به بیان دیگر، تعدادی حالت وجود دارد که درون هر کدام، امکان تغییر حالت به یک حالت دیگر وجود دارد. این الگو برای اشیایی که باید در زمان اجرا تغییر کنند و همچنین برای اشیایی که در حال پیچیده شدن هستند و شرط های زیادی دارند توصیه میشود.
منابع
1. https://refactoring.guru/design-patterns/state
2. https://sourcemaking.com/design_patterns/state
3. https://designpatternsphp.readthedocs.io/en/latest/Behavioral/State/README.html
4. https://medium.com/better-programming/state-strategy-design-patterns-by-example-f57ebd7b6211