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

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

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

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

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

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

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

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

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

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

به‌علاوه، با تغییر کد فعلی، شما باید کدهای جدید را دوباره تست کنید چرا که این نوع تغییرات، در سیستمی که به کامپوننت‌های مختلفی وابسته است، در اکثر مواقع باعث ایجاد باگ‌های جدیدی می‌شود. به‌جای این کار، می‌توانید نوعی از استراتژی پترن و آداپتر پترن را پیاده‌سازی نمایید که به‌راحتی می‌توانند این گونه مسائل را حل‌وفصل کنند:

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 است که به شما اجازه می‌دهد راهکارهایی که برنامهٔ مورد نظرتان باید اجرا کند، بسته به شرایط خاصی که در Runtime (یا هنگام اجرای برنامه) برایش رخ می‌دهد را انتخاب نمایید. شما 2 الگوریتم مختلف را داخل 2 کلاس جدا از یکدیگر به‌اصطلاح Encapsulate می‌کنید و هنگام اجرای برنامه، انتخاب می‌شود که از کدام استراتژی استفاده کند.

در مثال بالا، استراتژی‌ای که مورد استفاده قرار می‌گیرد، طبق متغیر context$ای است که در زمان معرفی کلاس موردنظر ایجاد شده است. اگر شما کلاس اول را مدنظر قرار دهید، این متغییر نیز از class_one استفاده می‌کند و بالعکس.

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

در ساده‌ترین حالت، شما می‌توانید از دستور شرطی 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.

بسته به موقعیت، ممکن است بخواهید دکمه‌های مختلفی را ایجاد کنید. اینجا است که می‌توانید از یک کارخانه (فکتوری) بخواهید که این کار طاقت‌فرسا را برایتان انجام دهد! برای روشن‌تر شدن این مسئله، مثالی می‌زنیم؛ فرض کنید 3 کلاس به‌صورت زیر داریم:

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 نیز وجود دارد که باعث می‌شود برای تغییر هر Instance نیاز به درگیری با یک دکوراتور نداشته باشید. این پترن جایگزینی برای Subclassing است (Subclassing اشاره دارد به ساخت کلاسی که عملکرد خود را از یک کلاس والد به ارث می برد.)

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

- کلاس اصلی کامپوننت را به‌عنوان والد کلاس دکوراتور در نظر بگیرید.
- در کلاس دکوراتور، یک اشاره‌گر به کامپوننت (والد) را به‌عنوان یک فیلد در نظر بگیرید.
- کامپوننتی را به کانستراکتور کلاس دکوراتور ارجاع دهید تا اشاره‌گر کامپوننت تعریف شود.
- در کلاس دکوراتور، تمامی متدهای کامپوننت را به اشاره‌گر کامپوننت ریدایرکت کنید.
- در کلاس دکوراتور، هر متد از کامپوننت که رفتارش نیاز به تغییر دارد را Override کنید.

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

- اگر در صفحهٔ اصلی (Homepage) هستیم و لاگین کرده‌ایم، این لینک داخل تگ h2 قرار بگیرد.
- اگر در صفحهٔ متفاوتی هستیم و لاگین کرده‌ایم، این لینک در تگ underline قرار بگیرد.
- اگر لاگین کرده‌ایم، این لینک در تگ 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 را تهیه کنید که به‌عنوان یکی از بهترین کتاب‌ها در زمینهٔ دیزاین پترن‌ها شناخته شده است.)

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

منبع


مرتضی صمدی