سرفصل‌های آموزشی
آموزش الگوهای طراحی (Design Pattern)
آشنایی با الگوی طراحی Decorator

آشنایی با الگوی طراحی Decorator

در این آموزش قصد داریم تا با Decorator Design Pattern در قالب مثالی کاربردی در زبان برنامه‌نویسی PHP آشنا شویم که یکی از زیرشاخه‌های الگوهای طراحی Structural (ساختاری) است و به‌کارگیری از آن در شرایطی موجب بهبود پروسۀ توسعۀ اپلیکیشن می‌شود که دولوپرها بخواهند تا یکسری فیچر خاص را به کلاس مد نظر خود بیفزایند. 

الگوی طراحی دکوراتور یک روش جایگزین برای مفهوم Sub Class است که بدین وسیله دولوپرها در زمان اجرای اپلیکیشن می‌توانند کلاسی را پیاده‌سازی کنند که علاوه بر برخورداری از خصوصیات منحصربه‌فرد خود، کلیهٔ خصوصیات کلاس پَرِنت (والد) را اصطلاحاً به ارث می‌برد به طوری که خصوصیات کلاس والد روی تمامی نمونه‌های ساخته‌شده از این کلاس اِعمال خواهد شد در حالی که الگوی طراحی دکوراتور این امکان را در اختیار دولوپرها قرار می‌دهد تا بتوانند در حین اجرای برنامه، رفتارها و فیچرهای جدید را تنها روی آبجکت‌ مد نظر خود اضافه کنند. بر اساس تعریف ارائه‌شده در ویکیپدیا برای این دیزاین پترن داریم:

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

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

برای شروع، نیاز است تا پوشه‌ای تحت عنوان decorator-design-pattern در لوکال‌هاست ایجاد کنیم و در ادامه فایل‌های زیر را در آن ساخته و در طول آموزش تک‌تک آن‌ها را تکمیل خواهیم کرد:

decorator-design-pattern/
├── Email.php
├── EmailBody.php
├── index.php
├── NewYearEmailBody.php
└── YaldaEmailBody.php

پیش از هر توضیحی، توجه داشته باشیم که کلیهٔ فایل‌های این آموزش با دستور php?> شروع می‌شوند که به دلیل شلوغ نشدن کدها، از نوشتن این دستور در تمامی اسکریپت‌های این آموزش خودداری کرده‌ایم. در ادامه اینترفیسی تحت عنوان EmailBody را در فایل EmailBody.php پیاده‌سازی می‌کنیم که توسط تمامی کلاس‌های وب اپلیکیشن قابل‌دسترسی باشد و در آن گفته‌ایم که تمامی کلاس‌هایی که از این اینترفیس اصطلاحاً implements می‌شوند موظف بر پیاده‌سازی فانکشنی تحت عنوان ()loadBody خواهند بود:

interface EmailBody {
    public function loadBody();
}

در ادامه فایلی به نام Email.php ایجاد کرده و داخل آن کلاسی تحت عنوان Email را از روی اینترفیس EmailBody ایمپلیمنت می‌کنیم که به منظور تولید محتوای متنیِ پیش‌فرض مورد استفاده قرار می‌گیرد:

require_once 'EmailBody.php';
class Email implements EmailBody {
    public function loadBody() {
        echo "This is the main Email body.<br />";
    } 
}

برای این منظور، در ابتدا فایل مربوط به اینترفیس مد نظر را ایمپورت کنیم و همان‌طور که می‌بینید، کلاس Email را از اینترفیس فوق‌الذکر ایمپلیمنت کرده‌ایم و از همین روی نیاز است تا متد ()loadBody را در آن بنویسیم. سپس داخل این متد گفته‌ایم استرینگ «.This is the main Email body» به عنوان متن اصلی تشکیل‌دهندۀ محتوای ایمیل در خروجی چاپ شود.

نحوۀ افزودن فیچرهای جدید به کلاس والد
همان‌طور که پیش‌تر اشاره کردیم، در پیاده‌سازی کلاس‌ها با به‌کارگیری از دیزاین پترن دکوراتور می‌توانیم بدون تغییر ساختار کلاسِ اصلی، فیچر مد نظر خود را به این کلاس اضافه کنیم. برای این منظور، ابتدا فایلی تحت عنوان EmailBodyDecorator.php حاوی یک الگوی دکوراتور برای ایمیل به صورت زیر ایجاد می‌کنیم:

require_once 'EmailBody.php';
require_once 'Email.php';
abstract class EmailBodyDecorator implements EmailBody {
    protected $emailBody;
    public function __construct(EmailBody $emailBody) {
        $this->emailBody = $emailBody;
    }
    abstract public function loadBody();
}

در تفسیر کد فوق باید گفت که در ابتدا فایل‌های مورد نیاز این کلاس همچون فایل مربوط به اینترفیس EmailBody و کلاس اصلی Email را ایمپورت کرده و در ادامه کلاسی از نوع اَبسترکت تحت عنوان EmailBodyDecorator را از روی اینترفیس EmailBody ایمپلیمنت کرده‌ایم و بدین دلیل نوع این کلاس را اَبسترکت تعریف کرده‌ایم که کلاس مذکور نقش یک کلاس به اصطلاح Parent (والد) را برای کلاس‌های Child (فرزند) خواهد داشت که قرار است تا بدین طریق بتوانیم تغییرات مد نظر خود روی کلاس اصلی را در آن‌ها پیاده‌سازی کنیم که برای این منظور نیز باید حداقل یک متد از جنس اَبسترکت داخل کلاس والد تعریف کنیم (در واقع، متد اَبسترَکت مذکور را در آینده در کلاس‌های فرزند و متناسب با نیاز خود پیاده‌سازی کرده و فیچرهای جدیدی را به آن اضافه خواهیم کرد.)

در ادامه، یک پراپرتی به نام emailBody$ با سطح دسترسی protected تعریف کرده‌ایم که قرار است تا آبجکتی از جنس کلاس EmailBody به آن منتسب شود (سطح دسترسی protected امکانی را برای ما فراهم می‌کند تا بتوانیم از تمامی کلاس‌های فرزند و همچنین از خودِ کلاس اصلی به پراپرتی مذکور دسترسی داشته باشیم.)

در ادامه کانستراکتوری تعریف کرده‌ایم که به محض ساخت آبجکتی از روی این کلاس فراخوانی شده و یک آبجکت از نوع کلاس EmailBody تحت عنوان emailBody$ ساخته و از طریق this->emailBody$ به پراپرتی فوق‌الذکر منتسب می‌کند که بدین ترتیب می‌توانیم به متدهای این کلاس دسترسی داشته و آن‌ها را فراخوانی کنیم. سپس متد ()loadBody از کلاس مذکور را در قالب یک متد به اصطلاح اَبسترَکت تعریف کرده‌ایم که داخل این‌گونه متدها نمی‌توان هیچ دستوری برای اجرا نوشت و صرفاً نام‌شان در کلاس والد آورده می‌شود و این در حالی است که متد مذکور می‌باید حتماً در کلاس‌های فرزند پیاده‌سازی شده که بدین وسیله فیچرهای مد نظر خود را به آن اضافه خواهیم کرد.

حال به منظور اِعمال تغییرات مد نظر خود روی کلاس EmailBodyDecorator نیاز است تا یک کلاس به اصطلاح Sub Decorator را پیاده‌سازی کنیم به طوری که از کلاس دکوراتور اصلی یکسری ویژگی‌ مشترک به منظور انجام تَسک مد نظر را ارث‌بری کرده و سایر فیچرها نیز متناسب با شرایط مختلف در آن پیاده‌سازی می‌شود که در همین راستا کلاس فرزند مذکور را داخل فایل YaldaEmailBody.php توسعه می‌دهیم و در آن فانکشن ()loadBody را به منظور ارسال ایمیل تبریک به کاربران به مناسبت «شبِ یلدا» تغییر می‌دهیم:

require_once 'Email.php';
require_once 'EmailBody.php';
require_once 'EmailBodyDecorator.php';
class YaldaEmailBody extends EmailBodyDecorator {
    public function loadBody() {
        echo 'This is the extra content for Yalda.<br />';
        $this->emailBody->loadBody();
    }
}

در ابتدا فایل‌های مورد نیاز همچون فایل مربوط به کلاس اصلی Email، فایل مربوط به اینترفیس EmailBody که کلاس EmailBodyDecorator از روی آن ایمپلیمنت شده و در نهایت فایل کلاس EmailBodyDecorator که کلاس YaldaEmailBody از آن ارث‌بری کرده است را ایمپورت می‌کنیم و همان‌طور که در ادامه می‌بینید کلاسی تحت عنوان YaldaEmailBody ایجاد کرده‌ایم و در ادامه از کلیدواژۀ extends استفاده کرده و سپس نام کلاسی را می‌آوریم که قصد داریم تا از آن ارث‌بری کنیم و از این پس کلاس YaldaEmailBody تمامی خصوصیات کلاس EmailBodyDecorator را دارا است که از آن جمله می‌توان به متد ()loadBody اشاره کرد که در کلاس والد به شکل یک متد اَبسترَکت درج شده بود و کلاس فرزند موظف بر پیاده‌سازی آن می‌باشد.

در ادامه، متد ()loadBody را پیاده‌سازی کرده و فیچرهای مد نظر خود را به آن اضافه می‌کنیم و همان‌طور که می‌بینید، داخل بلوک کد مرتبط با این متد فوق گفته‌ایم استرینگ «.This is the extra content for Yalda» پس از اینتر ایجادشده توسط تگ </ br> از متن پیش‌فرض ایمیل به محتوای آن افزوده شده و در ادامه فانکشن ()loadBody روی پارامتر ساخته‌شده از کلاس EmailBody و منتسب به یک پراپرتی تحت عنوان this->emailBody$ فراخوانی می‌شود که این امر امکان ساخت آبجکت‌های دیگر از روی این کلاس را فراهم می‌آورد به طوری که رفتار یک آبجکت روی رفتار سایر آبجکت‌های ساخته‌شده از این کلاس تأثیری نداشته باشد.

در ادامه، فرض کنیم می‌خواهیم ایمیلی به مناسبت نوروز برای کاربران خود ارسال کنیم که برای این منظور مجدداً باید یک ساب‌کلاس از روی کلاس اصلی تعریف کنیم تا تمامی خصوصیات آن را به ارث ببرد و در نهایت ویژگی‌های مد نظر خود را نیز به این کلاس فرزند اضافه می‌کنیم که در همین راستا فایلی تحت عنوان NewYearEmailBody.php را بدین صورت تکمیل می‌کنیم:

require_once 'Email.php';
require_once 'EmailBody.php';
require_once 'EmailBodyDecorator.php';
class NewYearEmailBody extends EmailBodyDecorator {
    public function loadBody() {
        echo 'This is the extra content for the New Year.<br />';
        $this->emailBody->loadBody();
    }
}

نحوۀ عملکرد کُد بالا نیز مانند مثال قبل است با این تفاوت که کلاس فوق منجر بدین خواهد شد تا استرینگ «.This is the extra content for the New Year» در سطر بعد به همراه محتوای متن اصلی ایمیل در خروجی نمایش داده شود.

حال قصد داریم تا نحوۀ عملکرد اپلیکیشن خود را بررسی کنیم که برای این منظور کدهای زیر را در فایل index.php نوشته و آن را اجرا می‌کنیم:

require_once 'Email.php';
$email = new Email(); // normal mail
$email->loadBody();

در توضیح کد فوق باید بگوییم که ابتدا فایل‌های مورد نیاز برای انجام تَسک مد نظر را ایمپورت کرده و در ادامه آبجکتی تحت عنوان email$ از روی کلاس Email ساخته‌ایم و در خط سوم فانکشن ()loadBody از این کلاس را روی آبجکت مذکور فراخوانی کرده‌ایم که منجر بدین خواهد شد تا استرینگ زیر به عنوان متن اصلی ایمیل در خروجی نمایش داده شود:

This is the main Email body.

اکنون کدهای قبلی را حذف می‌کنیم و کدهای زیر را در فایل index.php می‌نویسیم تا ببینیم چگونه می‌توان فیچر ارسال ایمیل تبریک به کاربران به مناسبت «شبِ یلدا» را به کلاس فوق‌الذکر اضافه کرد:

require_once 'Email.php';
require_once 'YaldaEmailBody.php';
$email = new Email();
$email = new YaldaEmailBody($email);
$email->loadBody();

در ابتدا فایل‌های مربوط به کلاس اصلی Email و کلاس YaldaEmailBody را ایمپورت کرده و در خط سوم آبجکتی تحت عنوان email$ از کلاس Email ساخته و در ادامه آبجکت دیگری با عنوان email$ از کلاس YaldaEmailBody ایجاد می‌کنیم و آبجکت ساخته‌شده از کلاس Email را به عنوان پارامتر ورودی به آن می‌دهیم و از آنجایی که این کلاس از EmailBodyDecorator ارث‌بری می‌کند، بنابراین به تمامی خصوصیات کلاس والد از جمله ساختار کانستراکور آن دسترسی داشته و در هنگام ساخت آبجکت از کلاس فرزند، کانستراکتور مذکور فراخوانی می‌شود که این وظیفه را دارا است تا پارامترهای ورودی به این کلاس را به یک پراپرتی از جنس کلاس EmailBody تحت عنوان emailBody$ منتسب کند.

حال با توجه به اینکه کلاس EmailBodyDecorator از جنس اَبسترِکت بوده و از اینترفیس EmailBody ایمپلیمنت شده است، باید فانکشن‌های داخل اینترفیس مذکور در این کلاس پیاده‌سازی شود که در همین راستا متد ()loadBody که در قالب یک متد اَبسترَکت در کلاس والد ذکر شده و بالتبع در کلاس فرزندِ YaldaEmailBody پیاده‌سازی شده است فراخوانی شده و استرینگ «.This is the extra content for Yalda» در خروجی نمایش داده می‌شود و در ادامه در خط پنجم متد ()loadBody روی آبجکت ساخته‌شده از این کلاس تحت عنوان email$ که اکنون به پارامتری از جنس کلاس EmailBody منتسب شده است کال (فراخوانی) شده و منجر بدین خواهد شد تا استرینگ کلاس اصلی به همراه استرینگ جدید چاپ شده و رفتار سایر آبجکت‌های ساخته‌شده از این کلاس نیز روی آبجکت‌ منتسب به پارامتر email$ بی‌تأثیر باشد که روی هم رفته خروجی به صورت زیر خواهد بود:

This is the extra content for Yalda.
This is the main Email body.

به همین ترتیب نیز می‌توانیم کلاس مربوط به NewYearEmailBody را تست کنیم:

require_once 'Email.php';
require_once 'NewYearEmailBody.php';
$email = new Email();
$email = new NewYearEmailBody($email);
$email->loadBody();

همان‌طور که مشاهده می‌کنید، آبجکت ساخته‌شده از روی کلاس Email تحت عنوان email$ را به عنوان پارامتر ورودی به آبجکت ساخته‌شده از کلاس NewYearEmailBody داده‌ایم که همان اتفاقات فوق‌الذکر مجدداً تکرار شده و فراخوانی فانکشن ()loadBody روی آبجکت منتسب به پراپرتی email$ از این کلاس منجر به چاپ استرینگ «.This is the extra content for the New Year» شده است و همچنین موجب می‌شود که تمامی آبجکت‌های ساخته‌شده از کلاس مذکور رفتاری منحصربه‌فرد داشته باشند که به عنوان خروجی کامل اجرای اسکریپت فوق داریم:

This is the extra content for the New Year.
This is the main Email body.

حال در ادامه قصد داریم تا نحوۀ افزودن دو فیچر به صورت هم‌زمان به کلاس اصلی ایمیل را مورد بررسی قرار دهیم که برای این منظور داریم:

require_once 'Email.php';
require_once 'YaldaEmailBody.php';
require_once 'NewYearEmailBody.php';
$email = new Email();
$email = new YaldaEmailBody($email);
$email = new NewYearEmailBody($email);
$email->loadBody();

ابتدا تمامی فایل‌های مورد نیاز برای اجرای اپلیکیشن را ایمپورت می‌کنیم و در ادامه آبجکتی از روی کلاس اصلی Email ساخته و سپس آبجکتی با عنوان email$ از کلاس YaldaEmailBody تعریف می‌کنیم که از کلاس EmailBodyDecorator ارث‌بری کرده‌ است و از همین روی به تمامی خصوصیات آن از جمله ساختار کانستراکتور دسترسی دارد که به محض ساخت آبجکتی از کلاس فرزند، کانستراکتور مذکور فراخوانی شده و آبجکت ساخته‌شده از این کلاس را به پارامتری از جنس کلاس EmailBody منتسب می‌کند که بدین ترتیب متد ()loadBody که در کلاس والد در قالب یک متد اَبسترَکت ذکر شده است فراخوانی شده و فانکشن مذکور که در کلاس فرزند تحت عنوان YaldaEmailBody پیاده‌سازی شده است اجرا می‌شود و در ادامه همین اتفاق نیز مجدداً برای آبجکت ساخته‌شده از روی کلاس NewYearEmailBody تکرار می‌شود که در نهایت استرینگ‌های زیر در خروجی چاپ خواهند شد:

This is the extra content for the New Year.
This is the extra content for Yalda.
This is the main Email body.

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

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

online-support-icon