در این مطلب قصد داریم با دیزاین پترن chain of responsibility و کاربرد های آن آشنا شویم. ابتدا ساختار کلی و فواید این الگو را مطرح کرده و سپس مثالی انتزاعی از آن به کمک زبان PHP میآوریم و در انتها کاربرد آن در بازسازی (refactor) کردن کد ها و کدنویسی تمیز را میبینیم.
طرح یک مشکل
تصور کنید روی ساخت یک سیستم سفارش آنلاین کار میکنید، میخواهید دسترسی برای ساخت سفارش را به کاربران لاگین شده محدود کنید. همچنین کاربرانی که دسترسی ادمین دارند باید به لیست همه سفارش ها دسترسی داشته باشند.
بعد از کمی فکر به این نتیجه میرسیم که باید این بررسی ها به ترتیب انجام شوند. یعنی ابتدا باید لاگین بودن کاربر را بررسی کنیم. اگر کاربر در سیستم ما لاگین نشده باشد دیگر دلیلی بر بررسی سایر موارد وجود ندارد.
بعد از مدتی بررسی های جدیدی به لیست شما اضافه میشود. به عنوان مثال یک لایه جهت ارزیابی (validation) دادههای ورودی از سمت کاربر اضافه میکنید، یک لایه دیگر جهت بررسی درخواست های مکرر از یک IP به برنامه اضافه میشود و یا یک بخش دیگر برای کش (cache) کردن پاسخ ارسالی به کاربر اضافه میکنید تا سرعت پاسخ دهی برنامه برای درخواست هایی که ورودی یکسان دارد افزایش یابد.
به ازای هر قابلیت جدید که به کد اضافه میشود کد ناخوانا تر و بدتر میشود، تغییر یک بخش ممکن است در بخش دیگر باعث ایجاد خطا شود و از همه بدتر اگر بخواهید یکی از بخشها را جایی دیگر در برنامه استفاده کنید از آنجایی که ممکن است همه بخشها را لازم نداشته باشید یا ترتیب استفاده متفاوت باشد مجبور به کپی کردن کد می شوید.
الگوی chain of responsibility راه حل مناسبی برای این مشکل ارائه میدهد.
الگوی chain of responsibility چیست ؟
یک الگوی رفتاری (behavioral) است که توانایی فراخوانی اشیا را به صورت زنجیره ای به ما میدهد در حالی که به هر کدام از اشیا این قابلیت را میدهیم تا از اجرای ادامه چرخه جلوگیری کند و یا درخواست را به شی بعدی در زنجیره بفرستد.
در واقع کاربر درخواستی را به برنامه ما ارسال میکند بدون این که بداند درخواست او را کدام شی handle میکند.
برای تصور بهتر میتوان فرآیند تماس با پشتیبانی سرویس اینترنت را در نظر گرفت.
· شماره گیری میکنیم
· داخلی مورد نظر را میگیریم
· مراحل حل مشکل را دنبال میکنیم
در هر بخش اگر ایرادی وجود داشته باشد از ادامه کارها صرف نظر میکنیم، این مراحل مثل زنجیر به هم وابسته اند.
مثالهای دیگری برای تصور بهتر این الگو میتوان زد، مانند فرآیند ثبتنام در دانشگاه، شما تا مدرک دیپلم خود را به دانشگاه تحویل نداده باشید قادر به انجام انتخاب واحد نیستید.
برای درک بهتر میتوانید فرآیند خرید تا دریافت یک کالا از فروشگاه اینترنتی را در ذهن خود تصور کنید.
الگوی chain of responsibility چگونه کار میکند ؟
مثل خیلی دیگر از الگو های رفتاری این الگو به ما کمک میکند تا رفتار های خاصی را به کلاسهای مستقل تبدیل کنیم.
این الگو هر بررسی (check) که در ابتدای مطلب (2) مطرح شد را به یک کلاس تبدیل میکند. این کلاس یک متد برای انجام بررسی ها دارد که درخواست را به عنوان ورودی دریافت میکند. این الگو پیشنهاد میدهد که مجموعه ای از این کلاسها را مانند زنجیر به هم متصل کنید. بنابراین هر کلاس یک فیلد برای مشخص کردن شی بعدی در زنجیره دارد و همچنین هر کلاس قابلیت این را دارد که درخواست را به شی بعدی در زنجیره منتقل کند یا این که تصمیم بگیرد این فرآیند را متوقف کند و درخواست به اشیا بعدی در زنجیره نرسد.
بر اساس این الگو کد ما به سه بخش اصلی تقسیم میشود.
1- کنترل کننده (Handler) : یک قرار داد برای مدیریت درخواست ها تعریف میکند. میتواند یک کلاس abstract باشد که متدهای پیشفرض و متدی برای set کردن شی بعدی در زنجیره داشته باشد.
2- پردازش کننده (Processor) : تعداد این اشیا میتواند بینهایت باشد. وظیفه آنها پردازش درخواست و تصمیم گیری در مورد این که فرآیند زنجیره را متوقف کنند یا درخواست را به شی بعدی در زنجیره بفرستند است.
3- مشتری (Client) : وظیفه آن آماده کردن درخواست و ایجاد زنجیره است.
چه زمانی از chain of responsibility استفاده میکنیم؟
· وقتی استفاده کنید که لازم است چندین کلاس پردازش کننده را به ترتیب خاصی اجرا کنید (این پردازش کننده ها میتوانند چک کردن شرط یا انجام تغییرات در پایگاه داده و ... باشد). از آنجایی که شما بخشهای زنجیره را بهم متصل میکنید، کارها دقیقا طبق برنامه و هدف شما پیش میروند.
· وقتی استفاده کنید که نیاز دارید پردازش کننده ها و ترتیب آنها در حین اجرای برنامه تغییر کنند.
شما این قابلیت را دارید که به صورت داینامیک اشیا را به زنجیره اضافه یا از آن حذف کنید.
· این الگو به شما این امکان را میدهد که چندین پردازش کننده (processor) را به صورت زنجیر به هم وصل کنید و یک در خواست را در این زنجیر ارسال کنید، به این صورت هر پردازش کننده این فرصت را دارد تا درخواست شما را پردازش کند و کاری انجام دهد.
· وقتی استفاده کنید که میتوانید برنامه خود را به صورت یک زنجیره که اجزای آن بهم متصل و وابسته هستند تصور کنید، زیرا هر بخش از زنجیره میتواند درخواست را پردازش کند و از ادامه زنجیره جلوگیری کند.
پیاده سازی chain of responsibility با PHP
تصور کنید هنگام خروج از خانه چندین پارامتر را بررسی میکنیم.
· آیا در ها قفل اند؟
· آیا چراغ ها خاموش هستند؟
· آیا سیستم هشدار دهنده روشن است؟
اگر پاسخ سؤال اول منفی باشد پرسیدن سایر سؤالها فایده ای ندارد! زیرا در هر صورت ما نمیتوانیم خانه را ترک کنیم تا زمانی که پاسخ سؤال اول مثبت باشد.
اگر بخواهیم این مسئله را به کد تبدیل کنیم هر کدام از این پرسش ها یک کلاس میشوند که ما در هرکدام از آنها وضعیت خانه را دریافت میکنیم و یک مورد را برسی میکنیم اگر مشکلی بود فرآیند را متوقف میکنیم و اگر نبود وضعیت خانه را به کلاس دیگری میدهیم. این دقیقا همان زنجیره ای است که از آن صحبت شد. یک کلاس برای وضعیت خانه میسازیم.
class HomeStatus
{
public $locks = true;
public $lightsOff = false;
public $alarmOff = true;
}
حالا باید کلاسهایی بسازیم که قابلیت بررسی وضعیت خانه را دارند و همچنین میتوانند وضعیت خانه را به شی بعدی ( کلاسی دیگر که مورد دیگری را برسی میکند) بفرستند.
برای انجام این کار ابتدا یک کلاس abstract میسازیم تا مطمئن شویم همه کلاسهایی که وظیفه آنها چک کردن خانه است متدهای لازم را دارند.
هر کلاسی که از این کلاس ارثبری میکند باید متد check را برای خودش پیاده سازی کند. این متد وضعیت خانه ( درخواست) را به عنوان ورودی می پذیرد.
abstract class HomeChecker
{
protected $successor;
abstract public function check(HomeStatus $home);
public function succeedWith(HomeChecker $successor)
{
$this->successor = $successor;
}
public function next(HomeStatus $home)
{
if (isset($this->successor)) {
$this->successor->check($home);
}
}
}
متد succeedWith وظیفه تعیین شی بعدی در زنجیره را دارد، این متد یکی از فرزندان همین کلاس را به عنوان ورودی دریافت میکند.
متد next وظیفه صدا زدن متد check در شی بعدی در زنجیره را دارد.
حالا میتوانیم سؤالهایی که ابتدای مطلب داشتیم را در قالب کلاسهایی که از این کلاس ارثبری میکنند پیاده سازی کنیم.
class Locks extends HomeChecker
{
public function check(HomeStatus $home)
{
if (!$home->locks) {
throw new Exception('not locked ABORT!');
}
$this->next($home);
}
}
همانطور که می بینید این کلاس بررسی میکند اگر قفل ها بسته نباشند فرآیند اجرا را متوقف میکند اما در غیر این صورت وضعیت خانه (درخواست) را به شی بعدی در زنجیره میفرستد.
دو پرسش دیگر راهم دقیقا به همین شکل به کلاس تبدیل میکنیم.
تا اکنون بخشهای کنترل کننده و پردازش کننده را پیاده سازی کرده ایم حالا باید بخش مشتری را بنویسیم که در آن یک زنجیره درست کنیم و درخواست خود را به آن بدهیم.
$locks = new Locks();
$lights = new Lights();
$alarm = new Alarm();
$locks->succeedWith($lights);
$lights->succeedWith($alarm);
$locks->check(new HomeStatus);
اشیا مورد نیاز را آماده کردیم و با مشخص کردن شی بعدی هر کدام یک زنجیره ساختیم، سپس از آن جایی که میخواستیم بررسی ها آغاز شود در خواست خود ( وضعیت خانه) را به متد check فرستادیم، حالا به ترتیب متدcheck اعضای زنجیره فراخوانی میشوند و در صورت وجود مشکل از اجرای ادامه زنجیره جلوگیری میشود.
استفاده از مفهوم chain of responsibility برای نوشتن کد های تمیز تر
قبل از این که به سراغ مثال واقعی تر از این الگوی طراحی برویم بهتر است با در نظر داشتن مفهوم این الگو کد های خود را بازسازی کنیم.
یک متد از کلاسی را در نظر بگیرید که در آن کارهای زیادی باید انجام شود، شاید بیش از 10 کار متفاوت.
class Thing
{
public function perform()
{
//Do This
//Do That
//Run Sth
//Erase Sth Else
//Add Foo To Bar
}
}
انجام همه این کار ها ممکن است بیش از 200 خط کد ایجاد کند و با کمی دقت متوجه می شویم که روند فعلی بیشتر از این که Object Oriented باشد Procedural است.
همچنین دیگران کد ما را به سختی میخوانند و متوجه کاری که انجام دادهایم نمیشوند.
اولین کاری که به ذهن میرسد خارج کردن چند تا متد است اما با این که کد کمی خوانا تر میشود هنوز هم داخل این کلاس کارهای متفاوت زیادی انجام میگیرد.) این کار برای وقتی که نهایتا 3 کار انجام میدهیم در آن متد و منطق زیادی هم وجود ندارد خوب است).
برای بازسازی این کد ابتدا برای هر وظیفه یا کار یک کلاس میسازیم که نام آن گویای کاری که میکند باشد. سپس مطمئن می شویم که همه این کلاسها از یک قرارداد (Interface) پیروی میکنند. ( اجباری به ساخت interface نیست همین که همه آنها یک متد مشترک داشته باشند هم کفایت میکند).
class DoThis
{
public function handle()
{
//Do This
}
}
سایر کار ها هم مانند عکس بالا به کلاس تبدیل میشوند و متد handle را خواهند داشت.
کلاس اصلی هم به شکل زیر تغییر میکند، آرایهای از کلاسها را مینویسیم و روی هر کدام متد handle را صدا میزنیم.
class Thing
{
public function perform()
{
$tasks = [
DoThis::class,
DoThat::class,
RunSth::class,
EraseSthElse::class,
AddFooToBar::class,
];
foreach ($tasks as $task) {
(new $task)->handle();
}
}
}
این کاری که کردیم پیاده سازی الگوی chain of responsibility به طور کامل نیست اما از مفهوم آن برای بهتر شدن کد خود استفاده کردیم.
مزایا و معایب الگوی طراحی chain of responsibility
مزایا:
· میتوان ترتیب را در زنجیره کنترل کرد.
· به قانون کلاسهای تک مسئولیتی پایبند می مانید.
· میتوان کلاسهای processor جدید ساخت بدون خراب کردن کد موجود.
(open/closed principle)
معایب:
· ممکن است برخی از درخواست ها پردازش نشده بمانند.
ارتباط chain of responsibility با سایر الگو ها
الگو های chain of responsibility، command، mediator و observer همگی راه های مختلفی برای برقراری اتصال بین ارسال کننده ها و دریافت کننده های درخواست ایجاد میکنند.
· Chain of responsibility یک درخواست را به ترتیب از بین زنجیره ای از پردازش گر ها عبور میدهد به طوری که هر پردازش گر این فرصت را دارد تا از ادامه این فرآیند جلوگیری کند.
· Command ارتباط غیر مستقیمی بین ارسال کننده ها و دریافت کننده ها برقرا میکند.
· Mediator ارتباط مستقیم بین ارسال کننده و دریافت کننده را از بین می برد و آنها را مجبور به برقراری ارتباط از طریق یک شی mediator میکند.
· Observer به دریافت کننده ها این امکان را میدهد که دریافت درخواست را به صورت داینامیک قطع و وصل کنند.
Chain of responsibility و Decorator شباهت های زیادی با یک دیگر دارند و این باعث ایجاد سردرگمی در استفاده از آنها میشود. این واقعیت که شما میتوانید زنجیره را در هر لایه ای متوقف کنید این الگو را از Decorator متفاوت کرده است.
Decorator را میتوان اجرای کارها باهم بدون هیچ تأثیری بر decorator های دیگر تصور کرد در حالی که اجرای یک زنجیره را میتوان به صورتی یکی یکی در نظر گرفت چرا که اجرای هر بخش به بخش قبلی در زنجیره وابسته است
جمعبندی
در این مطلب سعی شد به درک خوبی از چگونگی استفاده از این الگوی طراحی برسیم، در انتها این توسعهدهنده است که با توجه به شرایط الگویی مناسب را انتخاب میکند.
نکتهای که حائز اهمییت میباشد این است که بهتر است تا زمانی که برنامه ما ابعداد کوچکی دارد و رشد بسیار زیادی برای آن پیش بینی نمیشود انجام کارها را ساده نگه داریم و پیچیدگی بیهوده به برنامه خود اضافه نکنیم.
اگر دوست دارید پیاده سازی یک مثال واقعی از این دیزاین پترن در لاراول را ببینید، می توانید به مقاله پیاده سازی فرآیند خرید محصول در لاراول با استفاده از الگوی Chain of Responsibility در سایت سکان آکادمی مراجعه کنید.