سرفصل‌های آموزشی
آموزش الگوهای طراحی (Design Pattern)
آموزش الگوی طراحی Chain of Responsibility

آموزش الگوی طراحی Chain of Responsibility

در این مطلب قصد داریم با دیزاین پترن 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 در سایت سکان آکادمی مراجعه کنید.

online-support-icon