معرفی دیزاین‌ پترن‌های رایج و کاربردی در برنامه‌نویسی

معرفی دیزاین‌ پترن‌های رایج و کاربردی در برنامه‌نویسی

آیا تا‌ به‌ حال به این قضیه فکر کرده‌اید که اساساً کاربرد Design Pattern در کدنویسی چیست و چرا گفته می‌شود که استفادهٔ صحیح و اصولی از دیزاین پترن‌ها در توسعهٔ نرم‌‌افزارهای حرفه‌ای بسیار تأثیرگذار است و همچنین چرا در آگهی‌های استخدام برنامه‌نویس آشنایی با آن‌ها از اهمیت بسزایی برخوردار است؟‌ در این مقاله دقیقاً پاسخ به این دست سؤالات است به طوری که چندین مثال نیز در زبان PHP در ارتباط با نحوهٔ به‌کارگیری مهم‌ترین دیزاین پترن‌ها ارائه خواهیم کرد تا به خوبی متوجه شوید چه زمانی و به چه شکلی باید از آن‌ها استفاده نمایید.

Design Pattern (الگوی طراحی) چیست؟
دیزاین‌ پترن‌ها راه‌حل‌های آزموده و بهینه‌ای در مسائل مربوط به توسعهٔ‌ نرم‌افزار هستند که همه روزه به آن‌ها برخورد می‌کنیم. دیزاین‌ پترن در واقع فقط یک کلاس یا معماری نیست که به‌ سادگی آن‌ را به سیستم مورد نظرمان ایمپورت کرده و نتیجهٔ دلخواه‌مان را کسب کنیم، بلکه مفهومی بسیار فراتر از این‌ها است (برای کسب اطلاعات بیشتر، می‌توانید به مقالهٔ‌ Design Pattern (الگوی طراحی) چیست و چرا هر دولوپری باید با آن آشنایی داشته باشد؟ مراجعه نمایید.)

در یک کلام، دیزاین‌ پترن الگویی است که باید در موقعیت مناسب خود مورد استفاده قرار بگیرد تا بهترین نتیجه را در توسعهٔ نرم‌افزار در اختیار دولوپر قرار دهد. در عین‌ حال توجه داشته باشیم که مفهوم دیزاین‌ پترن اصلاً وابسته به زبان برنامه‌نویسی خاصی نیست و یک دیزاین‌ پترن خوب باید قابلیت پیاده‌سازی در بیشتر زبان‌ها (اگر نگوییم همه) را داشته باشد که این مسئله ارتباط تنگاتنگی با ویژگی‌های زبان مورد استفاده نیز دارا است.

آشنایی با انواع دیزاین پترن‌ها
مهم‌تر از همه اینکه هر دیزاین‌ پترنی را می‌توان به عنوان شمشیر دولبه‌ای در نظر گرفت که اگر در جای نادرست به‌ کار رود، می‌تواند فجایع و مشکلات بی‌شماری را هم برای نرم‌افزار و هم دولوپرها به‌وجود آورد؛ اما اگر در موقعیت و زمان مناسب خود مورد استفاده قرار بگیرد، می‌تواند در نقش ناجی نیز عمل نماید. دیزاین‌ پترن‌ها معمولاً در سه گروه اصلی دسته‌بندی می‌شوند که عبارتند از:

- Structural (ساختاری)
- Creational (تکوینی)
- Behavioral (رفتاری)

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

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

برای روشن‌تر شدن کاربرد الگوهای طراحی، یک مثال از دنیای واقعی در ارتباط با دیزاین پترن می‌زنیم. فرض کنید مسئولیت ارائهٔ سولوشنی برای ادغام دو کلاس مختلف که دو عملکرد کاملاً مجزا از یکدیگر دارند را به شما محول کرده‌اند. این کلاس‌ها در جاهای مختلفی در سیستم به‌ کار رفته‌اند؛ بنابراین حذف این دو کلاس یا تغییر کدهای موجود را باید از سرتان بیرون کنید چرا‌ که اصلاً کار ساده‌ای نخواهد بود. به‌ علاوه اینکه با تغییر کد فعلی باید کدهای جدید را دوباره تست کنید چرا که این نوع تغییرات، در سیستمی که به کامپوننت‌های مختلفی وابسته است، در اکثر مواقع باعث ایجاد باگ‌های جدیدی می‌شود. به‌ جای این کار، به راحتی قادر خواهید بود تا نوعی از Strategy Pattern و Adapter Pattern را پیاده‌سازی نمایید که به‌ راحتی می‌توانند این‌گونه مسائل را حل‌وفصل کنند:

class StrategyAndAdapterExampleClass {
    private $_class_one;
    private $_class_two;
    private $_context;
     
    public function __construct( $context ) {
            $this->_context = $context;
    }
     
    public function operation1() {
        if( $this->_context == "context_for_class_one" ) {
            $this->_class_one->operation1_in_class_one_context();
        } else ( $this->_context == "context_for_class_two" ) {
            $this->_class_two->operation1_in_class_two_context();
        }
    }
}

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

Strategy Pattern
این الگوی طراحی در واقع نوعی از دیزاین‌ پترن‌های اصطلاحاً Behavioral است که به شما اجازه می‌دهد بسته به شرایط خاصی که در حین اجرای برنامه برایش رخ می‌دهد، سولوشن‌های مختلفی را انتخاب نمایید. به عبارت دیگر، دو الگوریتم مختلف را داخل دو کلاس مجزا از یکدیگر به‌ اصطلاح Encapsulate کرده و هنگام اجرای برنامه، انتخاب خواهد شد که از کدام استراتژی استفاده گردد. در مثال بالا، استراتژی‌ای که مورد استفاده قرار می‌گیرد با توجه به متغیر context$ است که در زمان معرفی کلاس مورد نظر ایجاد شده است. به عبارت دیگر، اگر کلاس اول را مد نظر قرار دهید، این متغیر نیز از class_one استفاده می‌کند و بالعکس.

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

در ساده‌ترین حالت ممکن، شما می‌توانید از دستور شرطی if-else استفاده کنید؛ اما اگر خواستید این کلاس را در جای دیگری به کار ببرید چه‌طور؟ در چنین مواقعی باید همین if-else را در ماژول جدید نیز بازنویسی کنید که کاری منطقی به نظر نمی‌رسد. آیا اگر این امکان فراهم شود که فقط نام کلاس را به کار ببرید و بقیه کار را به آن بسپارید، راحت‌تر نخواهید بود؟ به عنوان مثال داریم:

class User {
     
    public function CreateOrUpdate($name, $address, $mobile, $userid = null)
    {
        if( is_null($userid) ) {
            // it means the user doesn't exist yet, create a new record
        } else {
            // it means the user already exists, just update based on the given userid
        }
    }
}

استراتژی پترن در حالت عادی شامل کپسوله کردن الگوریتم‌ها در کلاس دیگری می‌شود اما در این مورد، استفاده از کلاس دیگر بی‌فایده است. به خاطر داشته باشید که نیازی نیست همیشه از ساختار پیش‌فرض پیروی کنید؛ به طوری که تا‌ وقتی‌ مفهوم مورد نظر دیزاین پترن رعایت شده و مشکل حل شود، می‌توانید طبق نیازتان دست به کاستومایز کردن دیزاین پترن مد نظر بزنید.

Adapter Pattern
این الگوی طراحی در اصل یک دیزاین پترن از نوع اصطلاحاً Structural است که به شما این امکان را می‌دهد تا یک کلاس را با اینترفیس متفاوتی سازگار کنید که این کار امکان استفاده از کلاس توسط سیستمی که از متدهای فراخوانی مختلفی استفاده می‌کند را امکان‌پذیر می‌سازد. این الگو همچنین به شما امکان تغییر برخی از ورودی‌هایی را می‌دهد که از آبجکت ساخته‌شده از روی کلاس دریافت شده که همین مسئله باعث می‌شود بتوانید آن‌ها را به چیزی که با فانکشن‌های این پترن نیز هم‌خوانی داشته باشد تبدیل نمایید.

Wrapper اصطلاحی است که به مفهوم آداپتر کلاس اشاره دارد؛ به عبارت دیگر، به دولوپر این اجازه داده می‌شود تا تَسک‌هایی را در داخل یک کلاس اصطلاحاً Wrap‌ (بسته‌بندی) کند و آن‌ها را در موقعیت‌های مناسبی مورد استفاده قرار دهد. به‌ عنوان مثال، به جای فراخوانی کلاس‌های متفاوت و صدا زدن فانکشن‌های آن‌ها به صورت تک‌به‌تک، می‌توانید تمامی این متدها را در یک متد واحد توسط کلاس آداپتر اصطلاحاً Encapsulate کنید که این کار نه‌ تنها امکان استفادهٔ مجدد از این قابلیت‌ها را به هر صورتی که بخواهید به شما می‌دهد، بلکه از بازنویسی کدهای مربوط به یک تَسک خاص که قصد استفاده از آن در ماژول دیگری دارید نیز جلوگیری می‌کند. اگر بخواهیم بدون استفاده از آداپتر پترن کدنویسی کنیم، کدی همچون زیر خواهیم داشت:

$user = new User();
$user->CreateOrUpdate( //inputs );
 
$profile = new Profile();
$profile->CreateOrUpdate( //inputs );

اگر بخواهیم این کار را در جای دیگری نیز تکرار کنیم یا از این کد در پروژهٔ دیگری استفاده کنیم، باید دست به کار شده و تمام کدها را بازنویسی کنیم؛ لذا دقیقاً برعکس حالت قبل و با استفاده از آداپتر پترن، می‌توانیم کد بهتری بنویسیم:

$account_domain = new Account();
$account_domain->NewAccount( //inputs );

در این موارد، ما یک کلاس به‌ اصطلاح Wrapper داریم که قرار است حوزه‌ای برای کلاس Account ما باشد:

class Account()
{
    public function NewAccount( //inputs )
    {
        $user = new User();
        $user->CreateOrUpdate( //subset of inputs );
         
        $profile = new Profile();
        $profile->CreateOrUpdate( //subset of inputs );
    }
}

بدین ترتیب می‌توانید از کلاس Account هر وقت که بخواهید استفاده کنید. علاوه بر این، می‌توانید کلاس‌های دیگری را نیز درون کلاس‌ اصلی خود جای داده یا بهتر بگوییم Wrap کنید.

Factory Pattern
این الگوی طراحی از نوع اصطلاحاً Creational است که معنای آن دقیقاً همان چیزی است که نوشته می‌شود؛ یعنی کلاسی است که به‌ عنوان کارخانهٔ ساخت آبجکت‌ها عمل می‌نماید. هدف اصلی این الگوی طراحی Encapsulate کردن روال ساخت آبجکت است به‌ صورتی که بتوانید چندین کلاس مختلف را درون یک فانکشن واحد پوشش دهید. با فراهم کردن ورودی مناسب به متدی از جنس فکتوری، می‌توان انتظار داشت که آبجکت صحیحی را برگرداند.

بهترین زمان برای استفاده از این الگوی طراحی وقتی است که چندین نوع متفاوت از یک موجودیت (Entity) دارید. فرض کنید یک کلاس تحت عنوان Button (دکمه) دارید و این در حالی است که این کلاس انواع مختلفی دارد مثل ImageButton یا InputButton یا FlashButton. حال بسته به موقعیت‌های مختلف ممکن است بخواهید دکمه‌های مختلفی را ایجاد کنید و اینجا است که می‌توانید از یک فکتوری (کارخانه) بخواهید که این کار زمان‌بَر را برایتان انجام دهد! برای روشن‌تر شدن این مسئله، مثالی می‌زنیم بدین شکل که فرض کنید سه کلاس به‌ صورت زیر داریم:

abstract class Button {
    protected $_html;
     
    public function getHtml()
    {
        return $this->_html;
    }
}
 
class ImageButton extends Button {
    protected $_html = "..."; //This should be whatever HTML you want for your image-based button
}
 
class InputButton extends Button {
    protected $_html = "..."; //This should be whatever HTML you want for your normal button ();
}
 
class FlashButton extends Button {
    protected $_html = "..."; //This should be whatever HTML you want for your flash-based button
}

حال می‌توانیم کلاس فکتوری‌ خود را به صورت زیر ایجاد کنیم:

class ButtonFactory
{
    public static function createButton($type)
    {
        $baseClass = 'Button';
        $targetClass = ucfirst($type).$baseClass;
  
        if (class_exists($targetClass) && is_subclass_of($targetClass, $baseClass)) {
            return new $targetClass;
        } else {
            throw new Exception("The button type '$type' is not recognized.");
        }
    }
}

از این پس می‌توانیم از این کلاس به صورت زیر استفاده نماییم:

$buttons = array('image','input','flash');
foreach($buttons as $b) {
    echo ButtonFactory::createButton($b)->getHtml()
}

خروجی اسکریپت فوق کد HTML برای ایجاد انواع مختلفی از دکمه‌های مورد نظر است. در واقع، با استفاده از این روش می‌توانید برحسب موقعیت فعلی، دکمه‌ای که قصد دارید بسازید را مشخص کنید.

Decorator Pattern
این الگوی طراحی از نوع اصطلاحاً Structural است که در حین اجرای برنامه بسته به موقعیت به ما امکان اضافه کردن رفتارهای جدیدی به یک آبجکت را می‌دهد. به طور کلی، هدف این الگوری طراحی آن است که بتوانید فانکشن‌های توسعه داده شده را برای یک کاربرد خاص به کار ببرید و در عین‌ حال بتوانید از کلاس اصلی یک Instance (نمونه) بسازید به‌ طوری که فانکشن‌های جدید را به‌ همراه نداشته باشد.

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

بهترین موقعیت برای استفاده از دکوراتور پترن وقتی است که شما یک Entity دارید به‌ طوری‌ که تنها اگر موقعیت ایجاب کند، نیاز به تعریف رفتار جدید خواهد بود. برای مثال فرض کنید یک لینک HTML مثل دکمهٔ خروج (Logout) دارید که می‌خواهید بسته به صفحه‌ای که در آن هستید، کارهایی با کمی تفاوت با این لینک انجام دهید که در این مورد به‌ سادگی می‌توانید از دکوراتور پترن استفاده کنید بدین صورت که ابتدا دکوراتورهای مختلفی که نیاز دارید را به صورت زیر تعریف کنید:

- اگر در صفحهٔ اصلی (Homepage) هستیم و لاگین کرده‌ایم، این لینک داخل تگ <h2> قرار بگیرد.
- اگر در صفحهٔ متفاوتی هستیم و لاگین کرده‌ایم، زیر این لینک خط کشیده شود.
- اگر لاگین کرده‌ایم، این لینک در تگ <strong> قرار بگیرد.

حال که دربارهٔ دکوراتورهایمان تصمیم‌گیری کردیم، می‌توانیم آن‌‌ها را به کد تبدیل کنیم:

class HtmlLinks {
    //some methods which is available to all html links
}
 
class LogoutLink extends HtmlLinks {
    protected $_html;
     
    public function __construct() {
        $this->_html = "<a href=\"logout.php\">Logout</a>";
    }
     
    public function setHtml($html)
    {
        $this->_html = $html;
    }
     
    public function render()
    {
        echo $this->_html;
    }
}
 
class LogoutLinkH2Decorator extends HtmlLinks {
    protected $_logout_link;
     
    public function __construct( $logout_link )
    {
        $this->_logout_link = $logout_link;
        $this->setHtml("<h2>" . $this->_html . "</h2>");
    }
     
    public function __call( $name, $args )
    {
        $this->_logout_link->$name($args[0]);
    }
}
 
class LogoutLinkUnderlineDecorator extends HtmlLinks {
    protected $_logout_link;
     
    public function __construct( $logout_link )
    {
        $this->_logout_link = $logout_link;
        $this->setHtml("<u>" . $this->_html . "</u>");
    }
     
    public function __call( $name, $args )
    {
        $this->_logout_link->$name($args[0]);
    }
}
 
class LogoutLinkStrongDecorator extends HtmlLinks {
    protected $_logout_link;
     
    public function __construct( $logout_link )
    {
        $this->_logout_link = $logout_link;
        $this->setHtml("<strong>" . $this->_html . "</strong>");
    }
     
    public function __call( $name, $args )
    {
        $this->_logout_link->$name($args[0]);
    }
}

در ادامه، می‌توانیم به روش زیر از آن‌ها استفاده کنیم:

$logout_link = new LogoutLink();
 
if( $is_logged_in ) {
    $logout_link = new LogoutLinkStrongDecorator($logout_link);
}
 
if( $in_home_page ) {
    $logout_link = new LogoutLinkH2Decorator($logout_link);
} else {
    $logout_link = new LogoutLinkUnderlineDecorator($logout_link);
}
$logout_link->render();

مشاهده می‌کنید که اگر نیاز داشته باشیم می‌توانیم چند دکوراتور را با هم ترکیب کنیم؛ از آنجا که تمامی دکوراتورها از فانکشنی اصطلاحاً Magic به نام call__ استفاده می‌کنند، می‌توانیم همچنان متدهای فانکشن اصلی را فراخوانی کنیم. اگر فرض کنیم که در حال‌ حاضر در صفحهٔ اصلی هستیم، خروجی HTML به صورت زیر خواهد بود:

<strong><h2><a href="logout.php">Logout</a></h2></strong>

Singleton Pattern
دیزاین پترن سینگلتون نوعی از دیزاین پترن‌های اصطلاحاً Creational است که وظیفهٔ آن اطمینان حاصل کردن از این است که شما یک Instance (نمونه) واحد از کلاس خاصی را در طول اجرای برنامه خواهید داشت و یک مرکز دسترسی سراسری را به این آبجکت فراهم می‌کند.

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

تصور کنید که یک کلاس Session ایجاد کرده‌اید که آرایهٔ سراسری SESSION_$ را شبیه‌سازی می‌کند. از آنجا که این کلاس باید فقط‌وفقط یک بار فراخوانی شود، می‌توانیم به صورت زیر از یک سینگلتون پترن استفاده کنیم:

class Session
{
    private static $instance;
     
    public static function getInstance()
    {
        if( is_null(self::$instance) ) {
            self::$instance = new self();
        }
        return self::$instance;
    }
     
    private function __construct() { }
     
    private function __clone() { }
     
    //  any other session methods we might use
    ...
    ...
    ...
}
 
// get a session instance
$session = Session::getInstance();

بدین ترتیب می‌توانیم به نمونهٔ سِشِن مورد نظرمان در جاهای مختلفی از کد و حتی در کلاس‌های دیگر دسترسی داشته باشیم به طوری که این دیتا در سرتاسر فراخوانی‌های متد ‌()getInstance کاملاً یکسان خواهد بود.

نتیجه‌گیری
دیزاین پترن‌های بسیار زیادی برای مطالعه باقی‌ مانده و این در حالی است که در این مقاله تنها به برخی از مهم‌ترین‌ها اشاره کردیم ولی اگر در مورد دیزاین پترن‌ها کنجکاو شده‌اید، صفحهٔ ویکیپدیای دیزاین پترن‌ها اطلاعات خوبی را در اختیارتان قرار می‌دهد و همچنین برای کسب اطلاعات بیشتر می‌توانید کتاب Design Patterns: Elements of Reusable Object-Oriented Software را تهیه کنید که به‌ عنوان یکی از بهترین کتاب‌ها در زمینهٔ دیزاین پترن‌ها شناخته شده است.

نکتهٔ پایانی هم اینکه وقتی می‌خواهید از دیزاین پترنی استفاده کنید، همیشه ابتدا مطمئن شوید که قصد دارید مسألهٔ مناسب آن‌ را حل کنید. در حقیقت، همان‌طور که در ابتدای مقاله بیان شد، این دیزاین پترن‌ها مانند شمشیر دولبه‌ای هستند که اگر در جای نامناسب خود به کار گرفته شوند، ۱۰۰٪ کارتان را خراب‌تر خواهند کرد اما اگر بتوانید آن‌ها را به‌ طور صحیح پیاده‌سازی کنید، دیگر چشم‌پوشی از آن‌ها کار آسانی نخواهد بود و تسلط به آن‌ها چیزی است که می‌تواند یک دولوپر عادی را به یک دولوپر تراز اول و حرفه‌ای مبدل سازد.

منبع