سلام دوستان. گاهی اوقات توی برنامه کلاس‌هایی داریم که نمونه‌سازی از اونها هزینه زیادی داره. برای مثال کلاس کار با دیتابیس رو در نظر بگیرید:

class DB {
  private conn;

  constructor() {
    this.conn = mysql.createConnection('...')
  }

  public query() { ... }
}

// User.file
const db = new DB();
const users = db.query('...');

// Post.file
const db = new DB();
const posts = db.query('...');

طبق این کد، به محض ساخته‌شدن نمونه از کلاس DB، ارتباط با دیتابیس برقرار میشه. حالا اگه توی یک چرخه اجرای برنامه ۵۰ درخواست SQL داشته باشیم، ۵۰ تا کانکشن ساخته میشه که اصلاً بهینه نیست و باعث افت سرعت و مصرف بالای منابع سخت‌افزاری میشه.

یک راه برای حل این موضوع اینه که صرف نظر از اینکه توی برنامه چند تا درخواست SQL داریم (۵۰ تا؟ ۵۰۰ تا؟) کاری کنیم که توی سرتاسر برنامه فقط و فقط یک کانکشن وجود داشته باشه و کوئری‌های SQL رو مجبور کنیم که فقط از همون یک کانکشن استفاده کنن. که الگوی Singleton (سینگِلْتون) بهمون کمک می‌کنه تا این راه حل رو پیاده‌سازی کنیم 😉

 

الگوی Singleton چیه؟ 🤔

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

 

پیاده‌سازی الگوی Singleton

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

// config/db.js
{
  'host': '127.0.0.1',
  'port': '3306',
  'user': 'root',
  'pass': 'secret'
};

و این فایل هم برای تنظیمات مربوط به ایمیل:

// config/mail.js
{
  'host': 'domain.com',
  'port': '587',
  'address': 'reply@domain.com'
};

حالا می‌خوایم کلاسی بنویسیم که بتونیم سرتاسر برنامه به این تنظیمات دسترسی داشته باشیم، اونها رو تغییر بدیم و یا مقدار جدیدی اضافه کنیم. کلاس Config رو در نظر بگیرید که هنوز هیچ الگویی روی اون اعمال نشده:

class Config {
  private items = [];

  constructor() {
    const files = this.loadAllConfigFiles();

    for (const file in files) {
      this.items[file] = files[file];
    }
  }

  public get(key) {
    return this.items[key];
  }

  public set(key, value) {
    this.items[key] = value;
  }
}

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

const config = new Config();

config.get('db.host');
config.get('mail.address');

// ...
config.set('mail.from', 'google@microsoft.com');

کلاس Config توی سرتاسر برنامه در دسترس هست و تنظیمات مدنظرمون رو ارائه میده. برای دسترسی به تنظیمات، ابتدا این کلاس باید همه فایل‌های کانفیگ رو رجیستر کنه. فرض کنیم ۱۰ فایل کانفیگ داریم. آیا این راه درستی هست که هر بار از کلاس Config استفاده می‌کنیم، بیایم همه فایل‌های کانفیگ رو گردآوری کنیم؟ خب خیر. با توجه به مشکلاتی که بررسی کردیم، راه درست اینه که این اتفاق فقط و فقط یک بار رخ بده.

 

اعمال کردن الگوی Singleton

ابتدا باید کلاس Config رو بازنویسی می‌کنیم. مرحله اولِ پیاده‌سازی این الگو اینه که یک پراپرتی (فیلد) private از نوع static تعریف کنیم. این پراپرتی برای نگهداری نمونه سینگلتون هست:

class Config {
  private static instance: Config = null;
 
  // ...
}

مرحله بعد باید متد سازنده (constructor) کلاس رو private کنیم. چرا؟ چون هیچ‌کس غیر از خودش نتونه با کلمه‌کلیدی new از اون نمونه‌سازی کنه:

class Config {
  private static instance: Config = null;
 
  private constructor() {
    // ...
  }
}

new Config; // Error: Config class is not instantiable

مرحله بعد اینه که یک متد تعریف کنیم که مسئول ساختن و تحویل دادن نمونه هست. این متد که اسم اون معمولاً getInstance در نظر گرفته میشه رو به صورت static تعریف می‌کنیم تا بتونیم بدون new کردن از کلاس به اون دسترسی داشته باشیم:

class Config {
  private static instance: Config = null;

  private constructor() {
    // ...
  }

  public static getInstance() {
    // ...
  }
}

const config = Config.getInstance();

خب گفتیم این متد مسئول ساختن نمونه تحویل دادن اون هست. متد getInstance رو به صورت زیر می‌نویسیم:

class Config {
  private static instance: Config = null;

  private constructor() {
    // ...
  }

  public static getInstance() {
    if (this.instance === null) {
      this.instance = new Config;
    }

    return this.instance;
  }
}

توی این متد ابتدا بررسی کردیم که اگه پراپرتی instance برابر null بود، یک نمونه از کلاس Config بساز و اون رو به instance نسبت بده. و نهایتاً instance رو برگردون. با این کار حتی اگه بی‌نهایت بار getInstnace رو صدا بزنیم، شرط خط ۹ فقط یک بار اجرا میشه و نمونه‌ای که return میشه همونی هست که بار اول تولید شده:

Config.getInstance();
Config.getInstance();
Config.getInstance();
Config.getInstance();
Config.getInstance();
Config.getInstance();

const config1 = Config.getInstance();
const config2 = Config.getInstance();

config1 === config2; // true

خب پیاده‌سازی الگوی سینگلتون به همین راحتی بود. با اضافه کردن دو متد دیگه این مثال هم کامل میشه:

class Config {
  private static instance: Config = null;
  private items;

  private constructor() {
    this.items = new Object();
    const files = this.loadAllConfigFiles();

    for (const file in files) {
      this.items[file] = files[file];
    }
  }

  public static getInstance() {
    if (this.instance === null) {
      this.instance = new Config;
    }

    return this.instance;
  }

  public get(key) {
    return this.items[key];
  }

  public set(key, value) {
    this.items[key] = value;
  }
}

const config = Config.getInstance();

config.get('app.locale'); // undefined
config.set('app.locale', 'fa');
config.get('app.locale'); // fa

const config2 = Config.getInstance();
config === config2; // true


(اگه خوشتون اومد Star بزنین 😉)

 

مزایای الگوی Singleton

۱. می‌تونیم مطمئن بشیم که از یک کلاس فقط یک نمونه توی برنامه وجود داره

۲. راهی رو در اختیار ما قرار میده تا سراسر برنامه به همون نمونه دسترسی داشته باشیم

۳. آبجکت Singleton به صورت Lazy ساخته میشه. یعنی این آبجکت اولین بار طبق درخواست ما و در موقع لازم ساخته میشه

 

معایب الگوی Singleton

۱. این الگو قانون اول SOLID رو نقض می‌کنه. چرا؟ متد getInstance همزمان دو کار انجام میده. هم مسئول ساختن آبجکت هست و هم مسئول تحویل دادن اون

۲. یک نمونه در سراسر برنامه در دسترس هست و هر قسمتی از برنامه می‌تونه روی این نمونه تاثیر بذاره که باعث میشه عملکرد قسمت‌های دیگه تحت تاثیر قرار بگیره

۳. توی زبان‌های Multi-Thread هنگام ساخته‌شدن آبجکت ممکنه حالت Race Condition به وجود بیاد و در نتیجه چندین نمونه ساخته بشه

۴. Unit تست یک آبجکت Singleton راحت نیست و برای کلاسی که constructor اون private هست و متدهای استاتیک داره، باید به دنبال راه‌های خلاقانه برای ساختن آبجکت Mock باشیم

 

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

همونطور که گفته شد، اگه شرایطی داریم که می‌خوایم فقط یک نمونه از یک کلاس سرتاسر برنامه وجود داشته باشه از این الگو استفاده می‌کنیم. همچنین اگه به دنبال یک راه محدود شده برای دسترسی به اون نمونه هستیم می‌تونیم از این الگو استفاده کنیم. چون الگوی Singleton امکان رو بهمون میده تا از راهی که خودش تعیین کرده به اون نمونه دسترسی داشته باشیم (در مقایسه با متغیرهای سراسری که کاملاً در معرض تغییرِ بدون محدودیت هستن).

 

خب دوستان با این الگو، همه الگوهای Creational رو بررسی کردیم. الگوهای بعدی به زودی منتشر میشن. روزتون خوش 😉✌️

منابع: