الگوهای طراحی (دیزاین پترن‌ها) در برنامه‌نویسی

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

الگوهای طراحی

دیزاین پترن‌ چیست؟

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

انواع دیزاین پترن‌ها

همانظور که در ابتدای پست گفتیم، دیزاین پترن‌ها بر اساس هدف در 3 دسته طبقه‌بندی می‌شوند:
الگوهای ساختاری (Structural) به طور کلی با روابط بین موجودیت‌ها سروکار دارند، این الگوها کار موجودیت‌ها با یکدیگر را ساده‌تر می‌کنند.
الگوهای ایجادی (Creational) مکانیزم‌های نمونه‌سازی (instantiation mechanisms) ارائه می‌کنند، ساخت آبجکت‌ها را به روشی مناسب شرایط ساده‌تر می‌کنند. الگوهای ایجادی به جای اینکه مستقیما از آبجکت‌ها نمونه‌سازی (instantiate) کنید، برای شما آبجکت می‌سازند. اینکار به برنامه شما انعطاف‌پذیری بیشتری در تصمیم‌گیری درباره اینکه کدامیک از آبجکت‌ها باید برای یک مورد خاص ایجاد شود را می‌دهد.
الگوهای رفتاری (Behavioral) در ارتباطات بین موجودیت‌ها استفاده می‌شوند. ارتباط بین موجودیت‌ها را ساده‌تر و انعطاف‌پذیرتر می‌سازد. به طور کلی برای مدیریت الگوریتم‌ها، روابط و مسئولیت‌ها بین آبجکت‌ها استفاده می‌شود.

مروری بر الگوهای طراحی

در این مطلب الگوی طراحی زیر بررسی می‌شوند:

  • الگوی Factory: بیشتر در پروژه‌های بزرگ کاربرد دارد و یکی از مزیت‌های آن ساده‌سازی آبجکت در داخل آن است.
  • الگوی Abstract Factory: مشابه الگوی Factory است، با این تفاوت که از یک کلاس Abstract مشتق می‌شود.
  • الگوی Facade: نقش یک واسط را دارد. یکی از کاربردهای آن زمانی است که می‌خواهیم قبل از ارسال داده‌ها، پردازش‌هایی روی آن‌ها انجام شود.
  • الگوی Decorator: یکی از کاربردهای آن رفع مشکل ارث‌بری کلاس از تنها یک کلاس است.
  • الگوی Observer: نقش مسئول بازرسی یک کلاس را دارد و با تغییرات در یک کلاس، عمل خاصی را انجام می‌دهد.
  • الگوی Singleton: از هر کلاس فقط یک آبجکت ساخته می‌شود و کاربرد آن زمانی است که نیاز داریم یک گلوبال آبجکت داشته باشیم.
  • الگوی Iterator: با استفاده از این الگو مدیریت داده‌ها ساده‌تر می‌شود.
  • الگوی Strategy: یک رابط داریم که برای حالت‌ها و عملیات مختلف برنامه، کلاس‌هایی نوشته شده که از آن ارث می‌برند.
  • الگوی Active Record: رکوردهای داخل دیتابیس را به صورت یک آبجکت برمی‌گرداند.
  • الگوی Adaptor: به عنوان تطبیق دهنده مسئول تطبیق برنامه با API سایر برنامه‌ها است. یک آبجکت می‌سازیم و آن را به متدهای یک آبجکت دیگر تبدیل می‌کنیم.

الگوی Factory

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

چه زمانی از این متد استفاده کنیم؟

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

مثال

class MySQLDB
{
    public function setHost($host)
    {
        // code
    }
    public function setDB($db)
    {
        // code
    }
    public function setUserName($user)
    {
        // code
    }
    public function setPassword($pwd)
    {
        // code
    }
    public function connect()
    {
        // code
    }
}

class PostgreSQLDB
{
    public function setHost($host)
    {
        // code
    }
    public function setDB($db)
    {
        // code
    }
    public function setUserName($user)
    {
        // code
    }
    public function setPassword($pwd)
    {
        // code
    }
    public function connect()
    {
        // code
    }
}

if (Config::item('db_type') === 'mysql') {
    $DB = new MySQLDB();
    $DB->setHost("host");
    $DB->setDB("db");
    $DB->setUserName("user");
    $DB->setPassword("pwd");
    $DB->connect();
}
elseif (Config::item('db_type') === 'postgre') {
    $DB = new PostgreSQLDB();
    $DB->setHost("host");
    $DB->setDB("db");
    $DB->setUserName("user");
    $DB->setPassword("pwd");
    $DB->connect();
}
elseif (Config::item('db_type') === 'sqlite') {
    $DB = new SQLiteDB();
    $DB->setHost("host");
    $DB->setDB("db");
    $DB->setUserName("user");
    $DB->setPassword("pwd");
    $DB->connect();
}

به مثال بالا دقت کنید، بزرگترین مشکل کد بالا این است که اگر بخواهیم پروژه را بزرگتر و نوع دیتابیس‌ها را افزایش دهیم، پیچیده‌تر می‌شود، پس دستورات شرطی کد بالا را حذف کنید و کلاس Factory را اضافه کنید:

class DBFactory
{
    protected $driver = null;

    public function setDriver($driver)
    {
        $this->driver = $driver;
    }

    public function makeDB($host, $user, $pass, $dbname)
    {
        if ($this->driver === 'mysql') {
            $DB = new MySQLDB();
        }
        elseif ($this->driver === 'postgre') {
            $DB = new PostgreSQLDB();
        }
        elseif ($this->driver === 'sqlite') {
            $DB = new SQLiteDB();
        }

        $DB->setHost($host);
        $DB->setDB($dbname);
        $DB->setUserName($user);
        $DB->setPassword($pass);
        $DB->connect();

        return $DB;
    }
}

در نهایت با ساخت یک آبجکت به راحتی از آن استفاده کنید:

$dbFactory = new DBFactory;
$dbFactory->setDriver(Config::item('db_type'));
$DB = $dbFactory->makeDB("host", "db", "user", "pwd");

الگوی Abstract Factory

الگوی Abstract Factory مانند الگوی Factory است ولی از یک کلاس Abstract ارث‌بری می‌کنند.

الگوی Facade

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

نکته

برای اینکه نتوان از یک کلاس آبجکت ایجاد کرد، خصوصا برای استاتیک، راه اول private کردن متد سازنده است و راه دوم ایجاد خطا در متد سازنده (throw new Exception(‘Error message’)) است، در هنگام استفاده نیز از بلاک try catch استفاده می‌کنیم و حرفه‌ای‌تر از حالت اول است.

الگوی Decorator

الگو decorator یک مشکل دیگر برنامه‌نویسان را حل کرده است. وقتی نتوانیم به هر دلیلی یک کد را ویرایش کنیم، مثلا کلاسی داریم که بنا به هر دلیلی (مانند کپی‌رایت، فاینال بودن یا غیره) نمی‌توانیم آن را ریفکتور (بازنویسی) کنیم، اگر بخواهیم متدی به آن کلاس اضافه کنیم، از این الگو استفاده می‌کنیم. نحوه پیاده‌سازی بدین صورت است که ابتدا یک کلاس ساخته و در این کلاس یک فیلد خصوصی تعریف می‌کنیم، آبجکت (این آبجکت از کلاس مورد نظر ما مشتق شده است) را به عنوان پارامتر متد سازنده کلاس در نظر می‌گیریم و در فیلد فوق قرار می‌دهیم. حالا متدی هم‌نام با نام متد کلاس مورد نظرمان می‌سازیم و عملیات مورد نظر را انجام می‌دهیم (اغلب تغییرات روی داده‌ها است) و در صورت نیاز مقادیری را برمی‌گردانیم که البته در این متد می‌توانیم متدهای دیگری را نیز برای اعمال تغییرات یا اجرای دستورات فراخوانی کنیم. به این تکنیک مضروب‌سازی (Containment) می‌گویند که شما یک آبجکت از یک کلاس دیگر می‌سازید و از خصوصیات public آن استفاده می‌کنید و یه سری خصوصیات دیگر نیز اضافه می‌کنید و این در جاهایی که خصوصا وراثت امکان‌پذیر نیست، خیلی مفید است، به خصوص در غلبه کردن بر این محدودیت که یک آبجکت نمی‌تواند همزمان بیش از یک کلاس والد داشته باشد.

الگوی Observer

الگوی Observer نیز مشکل دیگری را حل می‌کند، فرض کنید می‌خواهید برخی از آبجکت‌ها تغییرات آبجکت‌های دیگر را دنبال (Track) کنند، یعنی اگر تغییراتی در یک آبجکت دیگر داده شود، متوجه شوند و کار دیگری را انجام دهند. این الگو از دو نوع آبجکت تشکیل شده است Observable و Observer که یکی Event تولید می‌کند و یکی به Eventها پاسخ می‌دهد. هرگاه وضعیت آبجکت observable تغییر کند، تمام آبجکت‌های observer ثبت شده در آبجکت خود را فراخوانی می‌کند و می‌گوید وضعیت من تغییر کرده است. یکی از کاربردها گزارش‌گیری است که خطاها را اعلام می‌کند. باید برای استفاده از آن رابطی با نام observer را پیاده‌سازی کنیم که حاوی متد notify است. مثال استفاده از این الگو به صورت زیر است:

interface observer 
{
    public function notify();
}

class YMNotifier implements observer 
{
    public function notify () 
    { 
        // send alerts using YM echo 'Notifying via YM<br>' . PHP_EOL;
    }
} 

class EmailNotifier implements observer 
{ 
    public function notify() 
    { 
        // send alerts using Email echo 'Notifying via Email<br>' . PHP_EOL;
    }
}

class SmsNotifier implements observer 
{
    public function notify()
    { 
        // send alerts using SMS echo 'Notifying via SMS<br >' . PHP_EOL;
    }
}

class Test 
{ 
    private $observers = [];
    
    public function register($object)
    { 
        if($object instanceof observer)
        {
            $this->observers[] = $object;
        }
        else
        { 
            echo 'The object must implement observer interface.<br >' . PHP_EOL;
        }
    }
    
    public function StateChange()
    { 
        foreach($this->observers as $observer) 
        {
            $observer->notify();
        }
    }
} 

class Notifier { }

$test = new Test(); 
$ym = new YMNotifier();
$em = new EmailNotifier();
$sm = new SmsNotifier(); 
$n = new Notifier();

$test->register($ym);
$test->register($em);
$test->register($sm);
$test->register($n);
$test->StateChange();

الگوی Singleton

تمامی کلاس‌های سینگلتون باید حداقل سه عنصر زیر را داشته باشند:

  • یک متد سازنده با سطح دسترسی private
  • یک پراپرتی استاتیک که نمونه‌ای از کلاس (آبجکت) را نگه دارد.
  • یک متد استاتیک با سطح دسترسی public برای دسترسی به آبجکت

برخلاف کلاس‌های معمولی، از کلاس سینگلتون نمی‌توان ارث‌بری کرد.

// The Database class represents our global DB connection
class Database extends PDO {
	// A static variable to hold our single instance
	private static $_instance = null;

	// Make the constructor private to ensure singleton
	private function __construct()
	{
		// Call the PDO constructor
		parent::__construct(APP_DB_DSN, APP_DB_USER, APP_DB_PASSWORD);
	}

	// A method to get our singleton instance
	public static function getInstance()
	{
		if (!(self::$_instance instanceof Database))
		{
			self::$_instance = new Database();
		}
		return self::$_instance;
	}
}

یک مثال برای استفاده از این الگو، اتصال‌ها به دیتابیس و تنظیمات و اطلاعات فایل‌های config است. الگوی Singleton یکی از پرکاربردترین الگوها است و مشکلات زیادی را حل کرده است، هدف اصلی آن این است که در هر لحظه بیش از یک آبجکت از یک کلاس وجود نداشته باشد. برای پیاده‌سازی این روش، یک فیلد استاتیک خصوصی مثلا $instance می‌سازیم و داخل متد سازنده آبجکت، یک شرط قرار می‌دهیم که اگر if(self::$instance) آنگاه self::$instance = $this و در بیرون شرط نیز همین مقدار return self::$instance را برمی گردانیم. حالا حتی اگر یک میلیون آبجکت هم بسازیم، فقط یک نسخه وجود دارد و فقط همان آبجکت در حافظه قرار می‌گیرد، پس تا جایی که می‌شود باید از این الگو استفاده کنیم تا مصرف حافظه به شدت کاهش یابد.

الگوی Iterator

الگوی Iterator کمک می‌کند تا مجموعه‌ای از داده‌ها را راحت‌تر مدیریت کنید. رابط Iterator دارای 5 متد می‌باشد، rewind ایندکس آرایه را به اولین خانه برمی‌گرداند، current عنصر جاری را برمی‌گرداند، key کلید (اندیس) عنصر جاری را برمی‌گرداند، next اشاره‌گر (اندیس) را یک واحد به جلو می‌برد، valid می‌گوید که آیا عنصری (اندیسی) که اشاره‌گر به آن اشاره دارد، وجود داره یا نه. مثلا می‌توانیم پست‌های یک مطلب را به کلاسی بدهیم که از این رابط ارث‌بری کرده است و این کلاس پست‌ها را پردازش می‌کند، به صورت زیر:

Class Post implements Iterator
{
    private $posts = array();
    
    public function __construct($posts)
    {
        if(in_array($posts))
        {
            $this->posts = $posts;
        } 
    }
    
    public function rewind() 
    {
        reset($this->posts); 
    }
    
    public function current() 
    {
        return current($this->posts); 
    }
    
    public function key()
    {
        return key($this->posts); 
    } 
    
    public function next()
    {
        return next($this->posts); 
    }
    
    public function valid () { 
        return($this->current() !== false); 
    }
}

حالا فرض کنید کلاس مشابهی برای کامنت‌ها داریم، و این کلاس‌ها متدهای دیگری نیز دارند، نحوه استفاده از این کدها به صورت زیر است:

$blogposts = GetAllPosts(); 
$posts = new Posts($posts);

foreach($posts as $post) 
{
    echo $psot->GetTitle();
    echo $psot->GetAuthor();
    echo $psot->GetContet(); 
    
    $comments = new Comments($post->GetComments);
    
    foreach($comments as $comment) 
    {
        echo $comment->GetAuthor();
        echo $comment->GetContent(); 
    }
}

اگر بخواهیم از foreach بتوانیم استفاده کنیم، باید رابط Iterator را پیاده‌سازی کرده باشیم. البته در آرایه‌های php این رابط به صورت پیش‌فرض پیاده‌سازی شده است اما می‌توانیم ساختارش را تغییر بدهیم.

الگوی Strategy

در الگوی Strategy یک رابط می‌نویسیم و برای هر حالت مثلا ارسال sms و mail و fax یک کلاس به ازای هر کدام قرار می‌دهیم که همه یک متد هم نام برای اجرای دستور دارند و انتخاب استفاده از کدام کلاس تعیین کننده ادامه داستان است.  

الگوی Active Record

الگوی ActiveRecord یک الگوی فوق‌العاده کاربردی است. رکوردهای داخل دیتابیس به شکل آبجکت در می‌آیند، یعنی هر آبجکت ActiveRecord با یک رکورد در دیتابیس متناظر است. هدف اصلی این الگو این است که بتوانیم با روشی مانند $post->field به پراپرتی برای ساخت یا تغییر دسترسی داشته باشیم که با مقداردهی یک پراپرتی، یک رکورد متناظر در جدول ایجاد می‌شود و با خواندن یک پراپرتی نیز رکورد متناظر برمی‌گردد، البته ساخت با متدی مانند save انجام می‌شود ولی مقداردهی از روش گفته شده انجام می‌شود و روش پیاده‌سازی بدین صورت است که به نام هر تیبل (مفرد آن مثلا برای تیبل posts نام کلاس را post قرار می‌دهیم)، یک کلاس می‌سازیم، در این کلاس یک فیلد خصوصی قرار می‌دهیم و در متد سازنده نام ستون‌ها را بعنوان اندیس‌های این فیلد مقداردهی اولیه می‌نماییم، سپس با __GET و __SET مقداردهی و مقدارگیری را انجام می‌دهیم، حالا متدهای مورد نظرمان را به صورت استاتیک می‌نویسیم، مثلا متد findByPk($id) یک رکورد خاص از جدول را با استفاده از id آن برمی‌گرداند که این کار را به این صورت انجام می‌دهد که هر ستون رکورد را در یک اندیس پراپرتی کلاس قرار می‌دهیم و سپس می‌توانیم آن را فراخوانی کنیم.

الگوی Adaptor

الگوی Adapter یک آبجکت را به متدهای یک آبجکت دیگر تبدیل می‌کند. وقتی از api یک سرویس استفاده می‌کنیم و سپس به هر دلیلی مجبوریم از یک سرویس‌دهنده دیگر استفاده کنیم. فرض کنیم از سرویس Writely استفاده می‌کردیم و بعد از خرید آن توسط گوگل، مجبور خواهیم بود تا از گوگل‌داک استفاده کنیم، حالا اگر رابطی داریم که کلاس Writely از آن استفاده می‌کند، حالا باید از کلاس گوگل داک استفاده کنیم، برای استفاده از این کلاس نیز نیاز به یک کلاس از نوع Adapter داریم که این کلاس از رابط فوق استفاده می‌کند و در بدنه‌اش، دارای متدهایی است که هر کدام با اجرا بخشی از کد کلاس گوگل‌داک را اجرا می‌کنند، مثلا متد سازنده مسئول ساخت یک شی از کلاس فوق است که آن را در پراپرتی مربوط قرار می‌دهد و متدها نیز مقادیر مورد نظر را برمی‌گردانند.

جمع‌بندی

در رابطه با دیزاین‌پترن، کتاب‌های بسیاری وجود دارد که از جمله بهترین آن‌ها ‌می‌توان کتاب Design Patterns: Elements of Reusable Object-Oriented Software، کتاب Head First Design Patterns و کتاب Patterns of Enterprise Application Architecture را نام برد. الگوهای طراحی مبحث جالبی است که در آینده مطالب بیشتری درباره آن‌ها منتشر خواهیم کرد و هر کدام از الگوها را به صورت جداگانه توضیح خواهیم داد.