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

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

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

چرا از الگوی طراحی Singleton استفاده کنیم؟

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

منظور از کلاس های هزینه بر چیست؟

به طور کلی، منظور از کلاس‌های هزینه‌بر کلاس‌هایی است که وظایف محول‌شده به آن‌ها ممکن است هزینۀ بالایی از نظر زمان اجرا یا حافظۀ مصرفی برای اپلیکیشن داشته و منجر به کاهش سرعت آن نیز شوند که در ادامه چند مثال از برخی تسک‌های پرهزینه را نام برده‌ایم:

- تسک‌های مربوط به اتصال به برخی سرویس‌های API به طوری که به ازای هر بار اتصال هزینه‌ای برای آن پرداخت شود.
- برخی کلاس‌هایی که وظایفی همچون تشخیص نوع دیوایس مورد استفادۀ کاربر را بر عهده دارند که این امر ممکن است منجر به کاهش سرعت اپلیکیشن گردد.
- برقراری کانکشن با دیتابیس که این موضوع نیز زمان‌بر بوده و تکرار مجدد آن در هر بار اجرا منجر به کاهش سرعت اپلیکیشن می‌گردد (که در این آموزش این مسأله مورد بررسی قرار خواهد گرفت.)

درک صحیح ساختار دیزاین پترن Singleton با مثالی کاربردی

در همین راستا، برای درک بهتر ساختار دیزاین پترن سینگلتون مثالی را در نظر می‌گیریم که در آن قصد داریم تا کلاسی را به منظور برقراری کانکشن با دیتابیس مربوط به وب اپلیکیشن فرضی خود پیاده‌سازی می‌کنیم که در چنین شرایطی اگر بخواهیم اپلیکیشن خود را بدون استفاده از دیزاین پترن سینگلتون پیاده‌سازی کنیم، پوشه‌ای تحت عنوان singleton-design-pattern در لوکال‌هاست ساخته و فایلی به نام ConnectDbWOSingleton.php داخل آن ایجاد می‌کنیم و در ادامه فایل مذکور را بدین شکل تکمیل می‌کنیم:

class ConnectDbWithoutSingleton {
    private $conn;
    private $host = 'localhost';
    private $username = 'root';
    private $pass = 'root';
    private $dbname = 'sokanacademy';

    public function __construct() {
        $this->conn = new PDO("mysql:host={$this->host}; dbname={$this-> dbname }", 
        $this->username, $this->pass,
        array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'"));
    }

    public function getConnection() {
        return $this->conn;
    }
}

پیش از هر توضیحی، لازم به یادآوری است که کلیهٔ فایل‌های این آموزش با دستور php?> شروع می‌شوند که به دلیل شلوغ نشدن کدها، از نوشتن آن در تمامی اسکریپت‌ها خودداری کرده‌ایم. همان‌طور که در کد فوق می‌بینید، از لایبرری PDO برای برقراری ارتباط با دیتابیس استفاده کرده‌ایم که برای آشنایی با نحوۀ پیاده‌سازی آن نیز می‌توانید به مقالۀ ارتباط با دیتابیس در PHP از طریق لایبرری PDO مراجعه نمایید.

در تفسیر کد فوق باید بگوییم که ابتدا کلاسی تحت عنوان ConnectDbWithoutSingleton ساخته و در آن یکسری پراپرتی از جنسِ پرایوت تعریف کرده‌ایم به طوری که متغیر conn$ به منظور نگهداری آبجکتی که مسئول برقراری کانکشن با دیتابیس می‌باشد تعریف شده و host$ به منظور نگهداری نام هاست است که در اینجا ما آن را از نوع localhost انتخاب کرده‌ایم چرا که هم دیتابیس و هم اسکریپت‌های آموزش قرار است تا روی یک سرور اجرا شوند و پراپرتی username$ نیز به منظور نگهداری نام‌کاربری مورد استفاده برای اتصال به مای‌اس‌کیوال تعریف شده است که در این مثال نام کاربری مربوط به phpMyAdmin نصب‌شده روی سیستم root است و در ادامه پراپرتی pass$ را به منظور نگهداری رمزعبور مورد استفاده برای اتصال به دیتابیس تعریف کرده‌ایم که این مورد نیز برابر با root در نظر گرفته شده است و در نهایت هم پراپرتی dbname$ را برای نگهداری نام دیتابیس تعریف کرده‌ایم که در این مثال sokanacademy در نظر گرفته شده است.

نکته لازم به یادآوری است که بسته به محیط توسعه‌ای که از آن استفاده می‌کنید، نام‌کاربری و رمزعبور phpMyAdmin می‌تواند با آنچه در این آموزش مطرح شد متفاوت باشد.

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

در ادامه یک کانستراکتور ساخته‌ایم و در آن گفته‌ایم در صورت ساخت آبجکتی از روی کلاس ConnectDbWithoutSingleton فوراً آبجکتی از روی کلاس PDO ساخته شود که این آبجکت چهار آرگومان ورودی می‌گیرد که آرگومان اول مربوط به نوع سیستم مدیریت دیتابیس، نام دیتابیس و سرور است که نام سیستم مدیریت دیتابیس را mysql در نظر گرفته و در ادامه مقدار منتسب به هر یک از پراپرتی‌های host$ و dbname$ را از طریق علائم {} به عنوان پارامتر ورودی به آن داده‌ایم و بدین ترتیب مفسر پی‌اچ‌پی نام پراپرتی‌های مذکور را به عنوان استرینگ معمولی داخل علائم " " نخوانده و مقادیر منتسب به آن‌ها را به ترتیبِ localhost و sokanacademy به عنوان پارامتر ورودی در نظر می‌گیرد که بدین ترتیب کانکشنی با دیتابیس مد نظر روی لوکال‌هاست برقرار می‌شود که در آن نام کاربری و پسورد مربوط به لوکال‌هاست root در نظر گرفته شده و برای آرگومان آخر نیز گفته‌ایم که در هر بار برقراری کانکشن با دیتابیسی تحت عنوان sokanacademy دیتای مربوط به کانکشن برقرارشده در قالب کاراکترهایی با فرمت utf-8 به سرور ارسال شوند و در نهایت هم آبجکت مذکور از طریق this->conn$ به پراپرتی متناظرش منتسب می‌شود.

در تکمیل کلاس فوق، در ادامه هم فانکشنی تحت عنوان ()getConnection نوشته‌ایم که مشخصات مربوط به آبجکت ساخته‌شده از روی این کلاس (کانکشن برقرارشده با دیتابیس) و منتسب به پراپرتی conn$ را در خروجی ریترن می‌کند. حال جهت تست، فایل دیگری تحت عنوان index.php ساخته و کدهای زیر را در آن نوشته و اجرا می‌کنیم:

require_once 'ConnectDbWithoutSingleton.php';

$instance = new ConnectDbWithoutSingleton();
$conn = $instance->getConnection();
var_dump($conn);

$instance = new ConnectDbWithoutSingleton();
$conn = $instance->getConnection();
var_dump($conn);

$instance = new ConnectDbWithoutSingleton();
$conn = $instance->getConnection();
var_dump($conn);

 در کد فوق، ابتدا فایل مربوط به کلاس ConnectDbWithoutSingleton را ایمپورت کرده و در ادامه آبجکتی از این کلاس تحت عنوان instance$ ساخته‌ایم و بر اساس ساختار تعریف‌شده برای کانستراکتور، اکنون کانکشنی با دیتابیس sokanacademy برقرار شده و در ادامه فانکشن ()getConnection از این کلاس را روی آبجکت فوق فراخوانی کرده و نتیجۀ حاصل از اجرای آن را به پراپرتی conn$ منتسب می‌کنیم و در سطر بعد پراپرتی مذکور را به عنوان پارامتر ورودی به فانکشن ()var_dump می‌دهیم تا بدین ترتیب مشخصات مربوط به آبجکت ساخته‌شده (کانکشن برقرارشده با دیتابیس) در خروجی ریترن شود.

در سطر بعد نیز به همان ترتیب آبجکت دیگری از روی کلاس ConnectDbWithoutSingleton و مطابق با پروسۀ ساخت آبجکت اول می‌سازیم و برای بار سوم مجدداً آبجکت جدیدی از روی کلاس مذکور ساخته و مشخصات مربوط به کانکشن برقرارشده را با دستور ()var_dump در خروجی ریترن می‌کنیم که در نهایت نتیجۀ حاصل از اجرای کد فوق به صورت زیر خواهد بود:

object(PDO)#2 (0) { } object(PDO)#4 (0) { } object(PDO)#6 (0) { }

همان‌طور که می‌بینید، آرایه‌ای متشکل از سه آبجکت با یکسری اصطلاحاً Resource ID متفاوت داریم که در هر مرتبه آبجکتی جدید از روی کلاس ConnectDbWithoutSingleton ساخته شده است بدین معنی که در هر مرتبه کانکشنی مجزا با دیتابیس مذکور برقرار شده است که فرآیندی زمان‌بر بوده و منجر به کاهش سرعت وب اپلیکیشن می‌شود. در همین راستا و برای حل مشکل فوق، دیزاین پترن سینگلتون را در ادامه پیاده‌سازی می‌کنیم تا بدین وسیله ساخت آبجکت از روی کلاسی که مسئول ارتباط با دیتابیس است را مدیریت کنیم.

برای این منظور، پروژۀ خود را بر اساس ساختار زیر در لوکال‌هاست ایجاد کرده و در ادامه هر یک از فایل‌ها را توسعه می‌دهیم:

singleton-design-pattern/
├── ConnectDb.php
└── index.php

داخل پوشه‌ای به نام singleton-design-pattern یا هر نام دلخواه دیگری، فایلی تحت عنوان ConnectDb.php ایجاد می‌کنیم و کلاسی با همین نام را در این فایل به صورت زیر پیاده‌سازی می‌کنیم:

class ConnectDb {
    private static $instance = null;
    private $conn;
    private $host = 'localhost';
    private $user = 'root';
    private $pass = 'root';
    private $dbname = 'sokanacademy';

    private function __construct() {
        $this->conn = new PDO("mysql:host={$this->host};dbname = {$this-> dbname}",
        $this->user, $this->pass,
        array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'"));
    }

    public static function getInstance() {
        if (!self::$instance) {
            self::$instance = new ConnectDb();
        }
        return self::$instance;
    }
    
    public function getConnection() {
        return $this->conn;
    }
}

در کد فوق مطابق کلاس ConnectDbWithoutSingleton عمل کرده و یکسری پراپرتی با همان کارکردهای مشابه را برای کلاس ConnectDb نیز تعریف کرده و علاوه بر آن‌ها پراپرتی دیگری تحت عنوان instance$ تعریف کرده‌ایم که قرار است تا آبجکت ساخته‌شده از روی کلاس فوق در آن نگهداری شود که مقدار اولیۀ null را برایش در نظر گرفته‌ایم.

حال قصد داریم تا بر اساس فلسفهٔ دیزاین پترن سینگلتون، ساخت آبجکت از کلاس فوق را محدود کنیم به طوری که تنها یک آبجکت از روی آن ساخته شود. برای شروع، پراپرتی instance$ را از جنس static تعریف می‌کنیم و در ادامه کانستراکتوری از جنس private تعریف کرده‌ایم و از همین روی از خارج از کلاس ConnectDb به آن دسترسی نخواهیم داشت و بدین ترتیب از هر نقطۀ وب اپلیکیشن نمی‌توان آبجکتی از کلاس مذکور ایجاد کرد و ساخت آبجکت از آن صرفاً محدود به داخل این کلاس می‌شود که در این کانستراکتور گفته‌ایم در صورت ساخت آبجکت از روی کلاس فوق کانکشنی با دیتابیس مذکور و بر اساس ویژگی‌های مشابه مثال قبل ساخته شده و به پراپرتی conn$ منتسب شود.

نکته : پراپرتی‌های استاتیک زمانی به کار می‌آیند که بخواهیم داخل خودِ کلاس آن‌ها را به کار گیریم نه اینکه آبجکتی از روی کلاس مذکور بسازیم.
در سطر بعد فانکشنی تحت عنوان ()getInstance از جنس static تعریف کرده‌ایم بدین دلیل که فانکشن‌های استاتیک برای فراخوانی نیازی به ساخت آبجکت از کلاس مربوطه ندارند و از آنجایی که می‌خواهیم تا ساخت آبجکت از کلاس فوق را محدود کنیم، فانکشن ()getInstance را بدین صورت تعریف کرده‌ایم تا فراخوانی آن بدون ساخت آبجکت نیز مقدور باشد و در ادامه گفته‌ایم پراپرتی instance$ داخل دستور شرطی چِک شود که اگر آبجکتی به آن منتسب نشده و مقدار اولیهٔ null را داشته باشد، آبجکت جدیدی از روی کلاس ConnectDb ساخته شده و به پراپرتی instance$ منتسب شود و در غیر این صورت آبجکت قبلی و منتسب به این پراپرتی را ریترن کند (برای دسترسی به این پراپرتی نیز از دستور ::self استفاده می‌کنیم چرا که دسترسی به پراپرتی‌ها و متدهای استاتیک با اپراتور :: امکان‌پذیر می‌باشد و کیوردِ self نیز اشاره بر تعلق پراپرتی مذکور به کلاس جاری دارد.)

در ادامه فانکشنی تحت عنوان ()getConnection تعریف کرده‌ایم که این وظیفه را دارا است تا مشخصات آبجکت ساخته‌شده از روی کلاس یا همان کانکشن برقرارشده با دیتابیس و منتسب به پراپرتی conn$ را در خروجی ریترن کند. بدین ترتیب در شرایطی که بخواهیم آبجکت جدیدی از روی کلاس به منزلۀ برقراری کانکشن جدید با دیتابیس بسازیم، متد ()getInstance چک می‌کند تا اگر کانکشنی از قبل با دیتابیس مذکور برقرار شده باشد هرگز آبجکت جدیدی ساخته نشده و تسک مد نظر روی همان کانکشن قبلی انجام می‌شود. برای تست این موضوع، فایل index.php را به صورت زیر تکمیل می‌کنیم:

require_once 'ConnectDb.php';

$instance = ConnectDb::getInstance();
$conn = $instance->getConnection();
var_dump($conn);

$instance = ConnectDb::getInstance();
$conn = $instance->getConnection();
var_dump($conn);

$instance = ConnectDb::getInstance();
$conn = $instance->getConnection();
var_dump($conn);

نحوۀ عملکرد کد فوق مشابه کدهای متناظر آن از مثال قبل می‌باشد با این تفاوت که برای فراخوانی متد استاتیکِ ()getInstance از کلاس ConnectDb نیازی به ساخت آبجکت از روی این کلاس نداریم؛ به عبارت دیگر، از طریق پراپرتی instance$ و با استفاده از اپراتور :: به متد مذکور دسترسی یافته و آن را فراخوانی می‌کنیم که نتیجۀ حاصل از فراخوانی این متد به پراپرتی conn$ منتسب می‌شود و در نهایت خروجی حاصل از اجرای کد فوق به صورت زیر خواهد بود:

object(PDO)#2 (0) { } object(PDO)#2 (0) { } object(PDO)#2 (0) { }

همان‌طور که می‌بینید، آرایه‌ای متشکل از سه آبجکت با یکسری Resource ID متناظرشان داریم که مشابه هستند بدین معنی که در ساخت آبجکت از روی کلاس ConnectDb و در هر سه مرتبه تنها یک آبجکت ساخته شده است.

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

online-support-icon