سرفصل‌های آموزشی
آموزش الگوهای طراحی (Design Pattern)
پیاده سازی الگوی طراحیMediator

پیاده سازی الگوی طراحیMediator

پیاده سازی الگوی طراحیMediator

در قسمت قبل ساختار اصلی و مراحل پیاده سازی  الگوی طراحی Mediator را بررسی کردیم و در این بخش به مثال قسمت قبل که مربوط به شکل های 2-1 و 2- 2 می شود می پردازیم. همانطور که در بخش مربوط به فرم ها یا Dialogها گفته شد، یک فرم یا Dialog از چند کامپوننت تشکیل شده است و اگر هر یک از آن ها را یک کلاس UIدر نظر بگیریم الگوی Mediatorبه ما کمک می کند که وابستگی های مشترک بین این کلاس های UI(رابط کاربری) مختلف را حذف کنیم تا بتوانیم ویژگی استفاده مجدد را به این بخش های اپلیکیشن اضافه کنیم.

شکل 3-1

هرچند که به نظر برسد یک کامپوننت فراخوانی شده توسط کاربر مجبور است که با بقیه کامپوننت ها مستقیماً ارتباط برقرار کند ولی مشاهده می کنیم که این اتفاق نمی افتد و کامپوننت ها از یکدیگر کاملا جدا هستند. کامپوننت های مورد نظر نیاز دارند تا با ارسال هر گونه اطلاعیه به همراه یک شاخص، توسط متد notify، رویدادی که می خواهند را به Mediatorاعلام کنند.
 

در مثال زیر که مربوط به شکل 3-1 است، رابط کاربری Mediator یک متد را تعریف می کند تا اجزا به وسیله آن، event ها یا رخداد ها را به اطلاع کلاس Mediator برسانند. در گام بعدی این کلاس Mediator است. که تصمیم می گیرد نسبت به رخداد ها چه برخوردی داشته باشد و در صورت نیاز ارتباطی بین اجزا برقرار کند.

// The mediator interface declares a method used by components
// to notify the mediator about various events. The mediator may
// react to these events and pass the execution to other
// components.
interface Mediator {
    method notify(sender: Component, event: string)
}

کلاس AuthenticationDialog همان کلاس اصلی یا ConcreteMediator است. که در توضیحات ساختار به آن پرداخته شد. در این کلاس تمامی ارتباطات موجود بین تک تک اجزا تعریف شده است.

متد Construct این کلاس وظیفه ایجاد کردن شیء های مربوط به اجزا و معرفی کردن Mediator به متد Construct هر یک از آن ها را دارد. پس بدین صورت زمانی که اتفاقی برای کامپوننتی رخ دهد، به کلاس Mediator اطلاعیه فرستاده می شود. Mediator با دریافت کردن اطلاعیه تصمیم می گیرد که ابتدا اگر داخل خودش واکنشی برای این رخداد تعریف شده آن را عملی کند و در غیر این صورت درخواست را به کامپوننت دیگری بفرستد.

این واکنش ها در متد notify نوشته می شوند و در اینجا از حلقه های ifو else برای بررسی حالت های متفاوت استفاده شده است. 

// The concrete mediator class. The intertwined web of
// connections between individual components has been untangled
// and moved into the mediator.
class AuthenticationDialog implementsMediator {
    private field title: string
    private field loginOrRegisterChkBx: Checkbox
 private field loginUsername, loginPassword: Textbox
    private field registrationUsername, registrationPassword
private field registrationEmail: Textbox
    private field okBtn, cancelBtn: Button
    constructorAuthenticationDialog() {
                       
// Create all component objects and pass the current
// mediator into their constructors to establish links.
// When something happens with a component, it notifies the
// mediator. Upon receiving a notification, the mediator may
// do something on its own or pass the request to another
// component.
method notify(sender, event) {     
        if (sender == loginOrRegisterChkBx and event == "check")
            if (loginOrRegisterChkBx.checked)
                title = "Log in"
                // 1. Show login form components.
                // 2. Hide registration form components.
            else
                title = "Register"
                // 1. Show registration form components.
                // 2. Hide login form components
        if (sender == okBtn && event == "click")
            if (loginOrRegister.checked)
                // Try to find a user using login credentials.
                if (!found)
                   // Show an error message above the login
                   // field.
            else
                // 1. Create a user account using data from the
                // registration fields.
                // 2. Log that user in.
                // ...
}

اجزا به وسیله ی رابط کاربری Mediator با کلاس آن ارتباط برقرار می کنند. بنابراین به کمک این رابط کاربری می توانیم Mediator های دیگری نیز داشته باشیم و بدون اینکه مشکلی داشته باشیم از همین اجزا در داخل آن ها استفاده کنیم.

// Components communicate with a mediator using the mediator
// interface. Thanks to that, you can use the same components in
// other contexts by linking them with different mediator
// objects.
class Component {       
    field dialog: Mediator

    constructor Component(dialog) {
        this.dialog = dialog
                    }

    method click() {
        dialog.notify(this, "click")
                    }

    method keypress() {
        dialog.notify(this, "keypress")
                    }
}

بدین صورت همانطور که چندین بار به این موضوع اشاره شد، اجزاء ما با یکدیگر در ارتباط نیستند. اجزا تنها یک کانال ارتباطی در خارج از خودشان خواهند داشت که آن نیز فرستادن اطلاعیه ها یا notifications به کلاس Mediator است..

// Concrete components don't talk to each other. They have only
// one communication channel, which is sending notifications to
// the mediator.
class Button extendsComponent {
    // ...
}
class Textbox extendsComponent {
    // ...
}
class Checkbox extendsComponent {
    method check() is
        dialog.notify(this, "check")
    // ...         
}

شیوه ی استفاده در لاراول

هرچند که از الگوی طراحی Mediator در زبان های Java و یا C# در بخش طراحی  GUI یا رابط کاربری گرافیکی استفاده می شود این الگو در زبان برنامه نویسی php چندان محبوب نیست. زیرا با اینکه ممکن است در اپلیکیشن های php اجزای زیادی وجود داشته باشند ولی این اجزا به ندرت در یک session به طور مستقیم ارتباط برقرار می کنند. با این حال از این الگو در php و محیط لاراول در مبحث رویداد ها (event dispatcher) استفاده می کنیم. در واقع مثالی که می توان برای کاربرد این الگوی طراحی بیان کرد، گسترش دادن به الگوی Observer به وسیله ی ارسال کننده ی رویداد (event dispatcher) است.. این کار باعث می شود که یک شیء بتواند رویداد یک شیء دیگر را فعال کند بدون آنکه به کلاس آن نیاز داشته باشد.

کلاس EventDispatcher به عنوان Mediator ما منطق عضویت و اطلاعیه ها را در خود جای می دهد. در حالی که یک کلاس Mediator معمولا وابسته به اجزای استفاده کننده اش هست ولی در این نمونه تنها به رابط کاربری abstract آن مرتبط است.. اجزا هر کدام می توانند به وسیله ی ی رابط کاربری Mediator عضو event مورد نیازشان باشند. 

دو نکته:

  1. در اینجا از رابط کاربری های Subject/Observer که در هسته ی php نوشته شده اند استفاده نمی کنیم چون با این کار آنها را از چیزی که برایش طراحی شده اند دور خواهیم کرد و از کد استاندارد فاصله می گیریم.
  2. یک شیء تمام فعل و انفعالات بین تعدادی اشیا دیگر را در خود جای می دهد.

مثالی که در این بخش به آن خواهیم پرداخت یک کتابخانه مجازی است.. در این کتابخانه ما دو کامپوننت داریم، یکی BookAuthorAssociate و دیگری BookTitleAssociate. عملکرد کلی به این صورت خواهد بود که ابتدا کاربر یک کتاب را معرفی می کند و با نوشتن نام نویسنده و عنوان کتاب اطلاعات را ارسال کند و سپس در کنترلر مربوطه اطلاعات دریافت شده و با Mediator شروع به پردازش می شود. سپس Mediator بررسی می کند که چه عملکردی را انجام بدهد که در اینجا دو عملکرد بزرگ و کوچک کردن تمام حروف کلمات لاتین را برای عنوان یا نویسنده کتاب تعیین کرده ایم به این صورت که کنترلر ابتدا نگاه می کند که چه گزینه ای برای تغییر از سوی کاربر درخواست شده و سپس متد مربوط را با کمک Mediator اجرا می کند و در خروجی کاربر تغییر ایجاد شده را مشاهده خواهد کرد.

به منظور درک بهتر بخش های مختلف اپلیکیشن در شکل 4-1 اجزای مختلف طبق توضیحات بخش ساختار مشخص شده اند:

شکل 4-1

آنچه که قرار است در اپلیکیشن ما اجرا شود در شکل 4-1 به تصویر کشیده شده است. همانطور که مشاهده می شود دو کامپوننت ما با عدد 1 و عنوان های Component A و Component B، کلاس abstract با عدد2 ، کلاس Concrete با عدد z3و همچنین کلاس Controller با عدد z4 مشاهده می شود. در زیر عنوان هر کدام نیز نام اصلی آن در اپلیکیشن نوشته شده است و در زیر آن هم تک تک متد های اصلی آن ها آورده شده است. توضیحات بیشتر هر کدام از بخش های بالا در مطالب پیش رو ارائه خواهد شد.

هرچند که در یک نمونه ی کاربردی تر در بین وبسایت های واقعی نیاز به طراحی دیتابیس و بخصوص در محیط لاراول نیاز به تعریف روابط Eloquent دارد ولی به منظور دور نشدن از هدف اصلی که دیدن عملکرد Mediator است. از انجام آن ها صرف نظر شده است.

برای شروع نوشتن کد ابتدا کلاس BookMediator را به گونه ای تعریف می کنیم که دو مشخصه ی نویسنده $authorObject  و عنوان $titleObject از نوع object باشند و سپس از کلاس های زیردست، این اشیا را می سازیم و دو تابع برای صدا زدن آن ها ایجاد می کنیم.

<?php//BookMediator.php
namespace App;


class BookMediator
{
    private $authorObject;
    private $titleObject;

    function __construct($author_in, $title_in)
    {
        $this->authorObject = new BookAuthorAssociate($author_in, $this);
        $this->titleObject = new BookTitleAssociate($title_in, $this);
    }

    function getAuthor()
    {
        return $this->authorObject;
    }

    function getTitle()
    {
        return $this->titleObject;
    }
    // when title or author change case, this makes sure the other
    // stays in sync
    function change(BookAssociate $changingClassIn)
    {
        if ($changingClassIn instanceof BookAuthorAssociate) {
            if ('upper' == $changingClassIn->getState()) {
                if ('upper' != $this->getTitle()->getState()) {
                    $this->getTitle()->setTitleUpperCase();
                }
            } elseif ('lower' == $changingClassIn->getState()) {
                if ('lower' != $this->getTitle()->getState()) {
                    $this->getTitle()->setTitleLowerCase();
                }
            }
        } elseif ($changingClassIn instanceof BookTitleAssociate) {
            if ('upper' == $changingClassIn->getState()) {
                if ('upper' != $this->getAuthor()->getState()) {
                    $this->getAuthor()->setAuthorUpperCase();
                }
            } elseif ('lower' == $changingClassIn->getState()) {
                if ('lower' != $this->getAuthor()->getState()) {
                    $this->getAuthor()->setAuthorLowerCase();
                }
            }
        }
    }

}

همچنین برای بررسی و اعمال تغییرات لازم یک متد تعریف می کنیم تا اول مشخص کند تغییر احتمالی مربوط به کدام عضو است یعنی عنوان کتاب یا نویسنده آن و سپس بزرگ یا کوچک بودن نوشته مورد نظر را مورد بررسی قرار دهد و بعد از آن هم عملیات مورد نیاز را که بزرگ یا کوچک سازی حروف لاتین کلمه درخواست شده است.، انجام دهد.

بعد از این مرحله برای ایجاد کلاس های BookAuthorAssociate و BookTitleAssociate یک کلاس abstract و یا یک رابط کاربری ( در اینجا کلاس abstract گزینه ی بهتری است. زیرا که می خواهیم در متدهای موجود در همین مرحله کد داشته باشیم) ایجاد می کنیم تا برای این دو جزء یک الگو مشخص کنیم:

<?php
//BookAssociate.php
namespace App;


abstract class BookAssociate
{
    private $mediator;

    function __construct($mediator_in)
    {
        $this->mediator = $mediator_in;
    }

    function getMediator()
    {
        return $this->mediator;
    }

    function changed($changingClassIn)
    {
        return $this->getMediator()->titleChanged($changingClassIn);
    }
}
حالا کلاس های زیر دست که قرار است از این کلاس پدر ارث ببرند را ایجاد می کنیم؛ کلاس های BookAuthorAssociate و یا BookTitleAssociate در صورت ایجاد تغییری کلاس BookMediator را باخبر می کنند. زمانی که در هر یک از این کلاس های Associate تغییری ایجاد شود (uppercase یا lowercase شدن حروف ) کلاس BookMediator به کلاس دیگر نیز اعلام می کند تا این تغییرات را لحاظ کند.
<?php
//BookTitleAssociate.php

namespace App;


class BookTitleAssociate extends BookAssociate
{
    private $title;
    private $state;

    function __construct($title_in, $mediator_in)
    {
        $this->title = $title_in;
        parent::__construct($mediator_in);
    }

    function getTitle()
    {
        return $this->title;
    }

    function setTitle($title_in)
    {
        $this->title = $title_in;
    }

    function getState()
    {
        return $this->state;
    }

    function setState($state_in)
    {
        $this->state = $state_in;
    }

    function setTitleUpperCase()
    {
        $this->setTitle(strtoupper($this->getTitle()));
        $this->setState('upper');
        $this->getMediator()->change($this);
    }

    function setTitleLowerCase()
    {
        $this->setTitle(strtolower($this->getTitle()));
        $this->setState('lower');
        $this->getMediator()->change($this);
    }
}
این کلاس برای تشخیص در عنوان کتاب ایجاد شد و با توابع مورد نیاز کامل شده است به این صورت که دو مشخصه ی title و state برای کتاب را دارد و به کمک عنوان و حالت بزرگ یا کوچکی حروف تغییرات را تشخیص داده سپس یا اعلامیه آن را ارسال می کند و یا آن را تصحیح می کند.
<?php
//BookAuthorAssociate.php

namespace App;


class BookAuthorAssociate extends BookAssociate
{
    private $author;
    private $state;

    public function __construct($author_in, $mediator_in)
    {
        $this->author = $author_in;
        parent::__construct($mediator_in);
    }

    /**
     * @return mixed
     */
    public function getAuthor()
    {
        return $this->author;
    }

    /**
     * @param $author_in
     */
    public function setAuthor($author_in): void
    {
        $this->author = $author_in;
    }

    /**
     * @return mixed
     */
    public function getState()
    {
        return $this->state;
    }

    /**
     * @param $state_in
     */
    public function setState($state_in): void
    {
        $this->state = $state_in;
    }

    public function setAuthorUpperCase()
    {
        $this->setAuthor(strtoupper($this->getAuthor()));
        $this->setState('upper');
        $this->getMediator()->change($this);
    }


    public function setAuthorLowerCase()
    {
        $this->setAuthor(strtolower($this->getAuthor()));
        $this->setState('lower');
        $this->getMediator()->change($this);
    }
}

کلاس بالا نیز برای تشخیص در نویسنده ی کتاب ایجاد و کامل شده است به این صورت که دو مشخصه ی author و state برای کتاب را دارد و به کمک عنوان و حالت بزرگ یا کوچکی حروف تغییرات را تشخیص داده سپس یا اعلامیه آن را ارسال می کند و یا آن را تصحیح می کند.

با این کار ما عملا الگوی طراحی Mediator را در دستور کدمان قرار داده ایم ولی برای استفاده از آن باید چندین گام دیگر نیز انجام شود. ابتدا نیاز داریم که از یک فرم استفاده کنیم که اطلاعات کتاب های جدید را برای ما ارسال کند و بعد از آن به کنترلر هایی نیاز داریم که اطلاعات را ارزشیابی کند و در دیتابیس ما ذخیره سازی کند در آخر هم کنترلری نیاز داریم که این فرایند Mediator را که در اینجا بزرگ کردن یا کوچک کردن حروف عنوان یا نویسنده است برای ما انجام دهد. بنابراین با توجه به این که چندین و چند فایل و کدنوشته برای این عملیات نیاز داریم انجام تمامی این بخش ها متأسفانه محوریت این مثال را از موضوع اصلی دور می کند پس تصمیم گرفتم که به سادگی هرچه تمام تر به آن ها پرداخته شود.

در ادامه یک فرم در صفحه ی welcome.blade.php درست می کنیم که دو مؤلفه ی عنوان و نویسنده ی کتاب را در آن بنویسیم و همچنین مشخص کنیم که چه تغییری را در آن باید ایجاد کنیم:

<!DOCTYPE html>

<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>   ...</head>

<body>

<div class="flex-center position-ref full-height">
    <div class="content">
        <div class="title m-b-md">
            Mediator
        </div>
        <div >
            <form action="{{ route('test') }}">
                <label for="author">Author:</label>
                <input type="text" id="author" name="author" placeholder="author name">
                <label for="title">Title:</label>
                <input type="text" id="title" name="title" placeholder="title">
                <br>
                <label for="change">Change:</label>
                <select name="change" id="change">
                    <option value="title">title</option>
                    <option value="author">author</option>
                </select>
                <label for="state">State:</label>
                <select name="state" id="state">
                    <option value="upper">uppercase</option>
                    <option value="lower">lowercase</option>
                </select>
                <br>
                <input type="submit">
            </form>
        </div>
    </div>

</div>

</body>

</html>

سپس مسیر ها را به صورت زیر تصحیح می کنیم:

Route::get('/', function () {
    return view('welcome');
});
Route::get('test',[BookController::class,'test'])->name('test');

در مرحله ی بعدی کلاس BookController.php را ایجاد کرده و دستور کار تست را در آن می نویسیم:

<?php
//BookController.php
namespace App\Http\Controllers;

use App\BookMediator;
use Illuminate\Http\Request;

class BookController extends Controller
{
    public function test(Request $request)
    {
        $author_in = $request['author'];
        $title_in = $request['title'];
        $mediatorTest = new BookMediator($author_in, $title_in);
        $author = $mediatorTest->getAuthor();
        $title = $mediatorTest->getTitle();

        if ($request['change'] == 'author') {
            if ($request['state'] == 'lower') {
                $author->setAuthorLowerCase();
                return $author->getAuthor() ;
            } elseif ($request['state'] == 'upper') {
                $author->setAuthorUpperCase();
                return $author->getAuthor();
            }

        } elseif ($request['change'] == 'title') {
            if ($request['state'] == 'lower') {
                $title->setTitleLowerCase();
                return $title->getTitle();
            } elseif ($request['state'] == 'upper') {
                $title->setTitleUpperCase();
                return $title->getTitle();
            }



        }
    }
}

شکلی که در نهایت پوشه بندی ما در لاراول به خود خواهد گرفت شبیه به زیر می شود:

/App
         /Http/Controllers/
                 BookController.php
         /Interfaces/
                 BookMediator.php
         /Components/
                 BookTitleAssociate.php
                 BookAuthorAssociate.php
         /Mediator/
                 BookAssociate.php
/Resources/views/
                 Welcome.blade.php

پس از پر کردن فرم می بینیم که تغییری که می خواستیم در آن ایجاد کنیم در پس زمینه ی برنامه ها طبق آنچه گفته شد مسیر خود را طی کرده و به عنوان خروجی به ما نمایش داده می شود و Mediator به گونه ای عمل کرده که بار اصلی برنامه که تصمیم گیری عملیات است بر روی دوش خودش است و دو جزء هیچ ارتباطی خارج از چهار چوب Mediator با یکدیگر نداشته اند.

مزایا و معایب

  • استفاده شدن از اصل Single Responsibility از اصول SOLID. به این گونه که تمامی ارتباطات بین اجزا مختلف را در یک محل مشخص که Mediatorاست. قرار می دهیم.
  • استفاده از اصل Open/Closed از اصول SOLID. می توانیم به سادگی بدون آنکه در اجزا تغییری ایجاد کنیم یک کلاس Mediatorدیگر در کدمان اضافه کنیم.
  • سبب کاهش coupling در کد می شود که در نتیجه تکرار شدن کد را کاهش می دهد.
  • به کمک آن می توانیم از هر یک از اجزا مورد نیاز چندین بار در چند Mediator مختلف استفاده کنیم.
  • از آنجایی که کلاس Mediator شامل جزییات زیادی از کامپوننت ما است. به مرور زمان و با زیاد شدن اجزا و ارتباطات آن ها ممکن است که به یک God Object تبدیل شود؛ یعنی چنان خارج از چهارچوب در خود حاوی اطلاعات شود که به عنوان یک شیء رفتار مناسبی در کد نخواهد داشت و بیش از اندازه بزرگ بشود!
  • کارایی کم و استفاده ی بسیار محدود در زبان php و به خصوص محیط لاراول به علت کم بودن تنوع اجزا در بخش های مختلف!

ارتباط با الگوی طراحی های دیگر

  • الگو های Mediator، Observer، Chain of responsibilityو Command هر کدام به گونه ای روش های مختلفی از ایجاد اتصال و ارتباط بین ارسال کننده و دریافت کننده یک درخواست را مطرح می کنند.
  • الگو های Mediator و Facade تقریبا هر دو یک وظیفه را انجام می دهند که آن سازماندهی کردن ارتباط بین چندین و چند کلاس جفت شده است..
  • الگوی طراحی های Mediatorو Observer بسیار شبیه به هم هستند و گاهی از هر کدام از آن ها می توان به جای دیگری استفاده کرد.

جمع بندی

در این مقاله به طور خلاصه شیوه ی پیاده سازی الگوی طراحی  Mediator در فریم ورک لاراول  را بررسي مي كنيم. از بسیاری از الگوهای برنامه نویسی استفاده شده است تا کار را برای کاربران آن راحت تر کند، به صورت مشهود از الگوهای Observer و Mediator در مبحث رویدادهای لاراول یا Event and Listener استفاده شده است و هرچند که این استفاده قطعاً تنها کاربرد این الگو در زبان php نیست ولی این الگو در زبان phpجز نمونه های موردی، کاربرد چندانی ندارد.

منابع

  1. Dive Into DESIGN PATTERNS by Alexander Shvets – digital e-book – published by refactoring.guru 2018
  2. Design Patterns in PHP and Laravel, By Kelt Dockins, Apress, 2016
online-support-icon