Published on

الگوی طراحی Singleton (تک‌نمونه)

نویسندگان

الگوی Singleton: راهکاری برای ایجاد یک نمونه‌ی یکتا

مقدمه

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

چرا Singleton؟

برخی از مواردی که Singleton در آن‌ها به کار می‌رود:

  • مدیریت منابع مشترک: مانند اتصال به پایگاه داده، کش، یا فایل‌های پیکربندی.
  • کنترل دسترسی به منابع: مانند مدیریت جلسات (Sessions) در برنامه‌های تحت وب.
  • ساختارهای سیستمی: مانند مدیریت چاپگر یا تنظیمات سیستم‌عامل.

مشکلات متغیرهای سراسری

برخی توسعه‌دهندگان ممکن است متغیرهای سراسری (Global Variables) را به عنوان جایگزین Singleton پیشنهاد دهند، اما این روش مشکلاتی دارد:

  1. کنترل ضعیف روی مقداردهی اولیه: ممکن است نمونه زودتر از نیاز ایجاد شود.
  2. وابستگی بالا: باعث کاهش انعطاف‌پذیری و افزایش وابستگی‌های متقابل در کد می‌شود.
  3. مشکل در چندنخی (Multithreading): ممکن است چندین نمونه به‌طور هم‌زمان ایجاد شوند.

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

پیاده‌سازی کلاسیک

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {} // جلوگیری از نمونه‌سازی مستقیم

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

مشکل: این پیاده‌سازی در برنامه‌های چندنخی (Multithreading) مشکل دارد، زیرا ممکن است دو Thread هم‌زمان getInstance() را اجرا کرده و دو نمونه مختلف ایجاد کنند.

راهکار: همگام‌سازی (Synchronization)

public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}
  • مزیت: جلوگیری از مشکل چندنخی
  • عیب: کندی عملکرد به دلیل استفاده از synchronized در هر بار دسترسی

راه‌حل بهتر: قفل‌گذاری دو مرحله‌ای (Double-Checked Locking)

public class Singleton {
    private static volatile Singleton uniqueInstance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
  • مزیت: کارایی بالا و ایمنی در چندنخی
  • نقش volatile: از کش شدن مقدار در CPU جلوگیری می‌کند.

بهترین روش در جاوا: استفاده از Enum

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("Singleton is working");
    }
}
  • مزیت: مدیریت خودکار چندنخی، جلوگیری از Serialization و Reflection
  • استفاده:
Singleton.INSTANCE.doSomething();

چرا enum برای Singleton؟

استفاده از enum در جاوا یک روش ایمن، ساده و بهینه برای پیاده‌سازی Singleton است. دلایل این برتری عبارتند از:

  1. تضمین یکتا بودن نمونه: جاوا تضمین می‌کند که فقط یک نمونه از INSTANCE ایجاد شود.
  2. امنیت در برابر چندنخی (Thread-Safety): نیازی به synchronized یا volatile نیست، زیرا مدیریت نمونه‌سازی به‌طور خودکار توسط جاوا انجام می‌شود.
  3. محافظت در برابر Serialization: در روش‌های کلاسیک، اگر یک Singleton قابل سریال‌سازی (Serializable) باشد، می‌توان با ObjectInputStream نمونه‌های جدیدی ایجاد کرد. اما enum به‌صورت پیش‌فرض در برابر این مشکل مقاوم است.
  4. محافظت در برابر Reflection: در روش‌های سنتی، با استفاده از Reflection می‌توان متد private constructor را دور زد و نمونه‌های جدید ایجاد کرد. اما enum این مشکل را ندارد.

با این حال، enum دارای محدودیت‌هایی است:

  • عدم امکان Lazy Initialization: نمونه INSTANCE از ابتدا ایجاد می‌شود، حتی اگر هرگز استفاده نشود.
  • عدم قابلیت ارث‌بری: چون enum از java.lang.Enum ارث‌بری می‌کند، امکان ارث‌بری از کلاس‌های دیگر را ندارد.

مقایسه روش‌های مختلف

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

آیا Singleton همیشه بهترین گزینه است؟

هرچند Singleton در بسیاری از موارد مفید است، اما همیشه بهترین گزینه نیست. در برخی سناریوها استفاده از Dependency Injection (DI) راهکار بهتری است:

  • افزایش تست‌پذیری (Testability): Singleton‌ها تست را دشوارتر می‌کنند، در حالی که DI این مشکل را حل می‌کند.
  • کاهش وابستگی‌های سخت‌کد شده: Singleton باعث ایجاد وابستگی‌های سراسری می‌شود که قابلیت تغییر را کاهش می‌دهد، اما DI انعطاف بیشتری فراهم می‌کند.
  • مدیریت بهتر چرخه‌ی حیات اشیا: Singleton همیشه در حافظه باقی می‌ماند، اما با DI می‌توان چرخه‌ی حیات را بهتر مدیریت کرد.

بنابراین، در برنامه‌هایی که مقیاس‌پذیری و تست‌پذیری بالا مورد نیاز است، بهتر است به جای Singleton از مدیریت وابستگی‌ها در فریمورک‌هایی مانند Spring یا Guice استفاده شود.

نتیجه‌گیری

الگوی Singleton یک راهکار مناسب برای مدیریت منابع محدود و داده‌های سراسری در برنامه‌ها است. پیاده‌سازی صحیح این الگو از ایجاد نمونه‌های غیرضروری جلوگیری کرده و کارایی سیستم را بهبود می‌بخشد. روش Enum به عنوان بهترین گزینه در جاوا توصیه می‌شود، اما بسته به نیاز پروژه می‌توان از روش‌های دیگر نیز استفاده کرد. همچنین در برخی موارد، استفاده از Dependency Injection به جای Singleton می‌تواند انعطاف‌پذیری و تست‌پذیری بهتری ارائه دهد.