Singleton Pattern (الگوی سینگلتون)

یک نمونه را در تمام برنامه به اشتراک بگذارید!

Singleton ها کلاس هایی هستند که می توانند فقط و فقط یک بار نمونه سازی شوند و به صورت سراسری قابل دسترسی هستند. این نمونه واحد را می توان در سراسر برنامه به اشتراک گذاشت، که Singletons را برای مدیریت وضعیت سراسری در یک برنامه عالی می کند.

برای درک بهتر ماجرا بیایید ببینیم که با استفاده از کلاس ES2015 یک سینگلتون چطور خواهد بود. برای این منظور، قصد داریم یک کلاس Counter بسازیم که دارای:

  • یک متد getInstance که مقدار نمونه را برمی گرداند.
  • یک متد getCount که مقدار فعلی متغیر counter را برمی گرداند.
  • یک متد increment که مقدار counter را یک بار افزایش می دهد.
  • یک متد increment که مقدار counter را یک بار کاهش می دهد.
let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

البته کلاس بالا معیارهای یک Singleton را ندارد! یک Singleton فقط باید بتواند یک بار نمونه سازی کند. اما در حال حاضر، ما می توانیم چندین نمونه از کلاس Counter ایجاد کنیم.

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

با دوبار فراخوانی متد new، counter1 و counter2 را برابر با نمونه های مختلف قرار می دهیم. مقادیر بازگردانده شده توسط متد getInstance در counter1 و counter2 مرجع را به نمونه های مختلف برمی گرداند: آنها یکی نیستند!

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


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

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error(&quotYou can only create one instance!&quot);
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

عالی شد! حالا دیگر قادر به ایجاد چندین نمونه نیستیم.


بیایید نمونه Counter را از فایل counter.js اکسپورت کنیم. اما قبل از انجام این کار، باید نمونه را نیز freez کنیم. متد Object.freeze مطمئن می شود که کدهای دیگر نمی تواند Singleton را تغییر دهد. ویژگی‌های موجود در نمونه فریز شده را نمی‌توان اضافه یا اصلاح کرد، که خطر رونویسی تصادفی مقادیر Singleton را کاهش می‌دهد.

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error(&quotYou can only create one instance!&quot);
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;



بیایید نگاهی به برنامه ای بیندازیم که مثال Counter را پیاده سازی می کند. ما فایل های زیر را داریم:

  • counter.js: شامل کلاس Counter است و یک نمونه Counter را به طور پیش فرض اکسپورت می کند
  • index.js: ماژول های redButton.js و blueButton.js را بارگیری می کند
  • redButton.js: Counter را ایمپورت می کند و متد increment counter را به عنوان شنونده رویداد به دکمه قرمز اضافه می کند و با فراخوانی متد getCount مقدار فعلی شمارنده را در کنسول نمایش می دهد.
  • blueButton.js: Counter را ایمپورت می کند و متدincrement counter را به عنوان شنونده رویداد به دکمه آبی اضافه می کند و با فراخوانی متد getCount مقدار فعلی شمارنده را در کنسول چاپ می کند.

هم blueButton.js و هم redButton.js نمونه مشابهی را از counter.js وارد می کنند. این نمونه به عنوان شمارنده در هر دو فایل وارد می شود.

وقتی متد increment را در redButton.js یا blueButton.js فراخوانی می کنیم، مقدار ویژگی counter در نمونه Counter در هر دو فایل به روز می شود. فرقی نمی کند که روی دکمه قرمز یا آبی کلیک کنیم: یک مقدار بین همه نمونه ها به اشتراک گذاشته می شود. به همین دلیل است که شمارنده همچنان یک عدد افزایش می‌یابد، حتی اگر متد را در فایل‌های مختلف فراخوانی کنیم.



معایب

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


در بسیاری از زبان های برنامه نویسی، مانند جاوا یا C++، امکان ایجاد مستقیم اشیاء به روشی که در جاوا اسکریپت هست وجود ندارد. در آن زبان های برنامه نویسی شی گرا، ما نیاز به ایجاد یک کلاس داریم که یک شی ایجاد می کند. آن شی ایجاد شده دارای مقدار instance کلاس است، درست مانند مقدار نمونه در مثال جاوا اسکریپت.


با این حال، پیاده سازی کلاس نشان داده شده در مثال های بالا در واقع overkill است. از آنجایی که می‌توانیم مستقیماً اشیاء را در جاوا اسکریپت ایجاد کنیم، می‌توانیم به سادگی از یک شی معمولی برای رسیدن به همان نتیجه استفاده کنیم. بیایید به برخی از معایب استفاده از Singletons بپردازیم!

استفاده از یک شی معمولی

بیایید از همان مثالی که قبلا دیدیم استفاده کنیم. با این حال، این بار، counter به سادگی یک شی است که شامل:


  • یک خاصیت count
  • یک متد increment که مقدار count را یک عدد افزایش می دهد
  • یک متد decrement که مقدار count را یک بار کاهش می دهد
https://codesandbox.io/embed/competent-moon-rvzrr

از آنجایی که اشیا با مرجع ارسال می شوند، هم redButton.js و هم blueButton.js یک مرجع را به یک شی counter وارد می کنند. تغییر مقدار count در هر یک از این فایل ها، مقدار counter را تغییر می دهد که در هر دو فایل قابل مشاهده است.

آزمایش کنیم

تست کدی که به یک Singleton متکی است ممکن است مشکل باشد. از آنجایی که نمی‌توانیم هر بار نمونه‌های جدیدی ایجاد کنیم، همه آزمایش‌ها به نمونه سراسری آزمون قبلی متکی هستند. ترتیب تست ها در این مورد مهم است و یک تغییر کوچک می تواند منجر به شکست کل مجموعه آزمایشی شود. پس از تست، برای بازنشانی تغییرات انجام شده توسط تست ها، باید کل نمونه را ریست کنیم.

https://codesandbox.io/embed/sweet-cache-n55vi


پنهان بودن وابستگی

هنگام وارد کردن ماژول دیگر، در این مورد superCounter.js، ممکن است واضح نباشد که ماژول در حال وارد کردن یک Singleton است. در فایل های دیگر، مانند index.js در این مورد، ممکن است آن ماژول را وارد کرده و متدهای آن را فراخوانی کنیم. به این ترتیب، ما به طور تصادفی مقادیر موجود در Singleton را تغییر می دهیم. این می تواند منجر به رفتار غیرمنتظره شود، زیرا چندین نمونه از Singleton را می توان در سراسر برنامه به اشتراک گذاشت، که همه آنها نیز اصلاح می شوند.

رفتارهای سراسری

یک نمونه Singleton باید بتواند در کل برنامه ارجاع داده شود. از آنجایی که متغیرهای سراسری در دامنه کل برنامه در دسترس هستند، می‌توانیم به آن متغیرها در سراسر برنامه دسترسی داشته باشیم.

داشتن متغیرهای سراسری به طور کلی به عنوان یک تصمیم بد در طراحی منظور می شود. شلوغ بودن و کثیفی دامنه سراسری می تواند به طور تصادفی مقدار یک متغیر سراسری را بازنویسی کند، که می تواند منجر به رفتارهای غیرمنتظره زیادی شود.

در ES2015، ایجاد متغیرهای سراسری نسبتاً غیر معمول است. کلمه کلیدی let و const جدید با نگه داشتن متغیرهای اعلام شده با این دو کلمه کلیدی در محدوده بلوک، مانع از تداخل تصادفی متغییرهای توسعه دهنده به حوزه سراسری می شود. سیستم ماژول جدید در جاوا اسکریپت با امکان اکسپورت مقادیر از یک ماژول و ایمپورت کردن آن مقادیر در فایل‌های دیگر، ایجاد مقادیر قابل دسترس سراسری را بدون تداخل با دامنه سراسری برنامه را آسان‌تر می‌کند.

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

معمولاً بخش‌های خاصی از کد مقادیر سراسری را تغییر می‌دهند، در حالی که برخی دیگر آن داده‌ها را مصرف می‌کنند. ترتیب اجرا در اینجا مهم است: ما نمی خواهیم ابتدا داده ها را به طور تصادفی مصرف کنیم، زمانی که (هنوز) داده ای برای مصرف وجود ندارد! درک جریان داده هنگام استفاده از یک حالت سراسری می تواند با رشد برنامه بسیار مشکل باشد و ده ها مؤلفه بر هم اثرگذار خواهند بود.

State management in React

در React، ما اغلب به جای استفاده از Singletons، از طریق ابزارهای مدیریت حالت مانند Redux یا React Context از راهکار متغییرهای سراسری استفاده می کنیم. اگرچه رفتار حالت سراسری آنها ممکن است شبیه به حالت یک Singleton به نظر برسد، اما این ابزارها به جای حالت تغییرپذیر Singleton، یک حالت فقط خواندنی را ارائه می دهند. هنگام استفاده از Redux، تنها کاهنده‌های عضو اصلی کلاس می‌توانند وضعیت را به‌روزرسانی کنند، پس از اینکه یک مؤلفه action را از طریق dispatcher ارسال می کند. اگرچه با استفاده از این ابزارها، جنبه‌های منفی داشتن یک متغیر سراسری به طور جادویی ناپدید نمی‌شوند، اما حداقل می‌توانیم مطمئن شویم که حالت(متغیر) سراسری آنطور که ما در نظر داریم جهش یافته است، زیرا مؤلفه‌ها نمی‌توانند مستقیماً وضعیت را به‌روزرسانی کنند.