یک فنجان جاوا - دیزاین پترن ها - Singleton

میتونین برای آشنایی با الگوهای طراحی (یا همون دیزاین پترن های) زبان جاوا به مقاله ی یک فنجان جاوا - دیزاین پترن ها Design Patterns مراجعه کنین.

(همونطور که گفته شده این الگو زیرمجموعه ی الگوهای تکوینی یا ساختنی (Creational) هست)


الگوی Singleton

سینگلتون شاید پایه ای ترین الگوی طراحی ای باشه که اکثر برنامه نویسا باهاش آشنا هستن و اغلب اینطور تعریف میشه که این الگو تضمین میکنه ما صرفاً یه شئ از روی یه کلاس خواهیم داشت و صرفاً یه راه برای دسترسی به اون شئ در سرتاسر نرم‌افزار وجود داره. اما اگه بخوایم یذره بهتر تعریف کنیم باید بگیم:
هدف اینه که نشه از کلاسی بیشتر از یک شی ساخت.

خیلیا (مثل خودِ من قبل از اینکه بیشتر با این الگو آشنا بشم) فکر میکنن مفهوم سینگلتون رو کاملاً درک کردن و کاری نداره پیاده سازیش. اما توی این مقاله میبینیم که اوضاع یذره پیچیده تر از چیزی میتونه باشه که توی نگاه اول و داخل یه خط تعریف ساده گنجونده بشه. (البته باید توجه کنیم که اغلب توی استفاده های عادی مشکلی وجود نداره و در ادامه متوجه میشیم منظور ما از پیچیده تر شدن، یسری زمان های خاصی هست که باهاشون آشنا خواهیم شد).

برای پیاده سازی این الگو راه های زیادی وجود داره که سعی میکنیم مهمتریناشون رو بررسی کنیم (یه نکته اینکه به شئ ای که قراره یکی بیشتر نشه ازش ساخت توی این الگو اصطلاحاً instance گفته میشه):


روش Eager initialization

توی این روش که ساده ترین نوع پیاده سازی الگوی سینگلتون هست، instance در زمان لود کلاس ساخته میشه. در حقیقت چه بخوایم از instance استفاده بکنیم چه نخوایم، در هر صورت ساخته میشه و زمان فراخوانی تابع شئ ای که از قبل ساخته شده برگردونده میشه. این روش بصورت زیر پیاده میشه:

public class EagerInitializedSingleton {
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();          
    //private constructor to avoid client applications to use constructor private 
    EagerInitializedSingleton(){}
    public static EagerInitializedSingleton getInstance(){
        return instance;
    }
}

اگه کلاس سینگلتونمون ریسورس های زیادی نداره، این روش مناسبیه ولی خب متأسفانه خیلی وقتا هست که ما دسترسی به فایل ها، دیبتابیس و سایر موارد این چنینی رو توی کلاس سینگلتون میذاریم و ممکنه باعث بوجود اومدن مشکلاتی مثل کمبود حافظه یا الکی درگیر نگه داشتم کانکشن ها بشیم.


روش Static block

این روش خیلی شبیه روش قبلی هست با این تفاوت که instance داخل بلوک static ساخته میشه (که البته یکی از معایب روش قبل رو که عدم وجود امکان هندل کردن اکسپشن ها بود رو نداره، اصطلاحاً امکان exception handling رو محیا میکنه):

public class StaticBlockSingleton {
    private static StaticBlockSingleton instance
    private StaticBlockSingleton(){}
    //static block initialization for exception handlin
    static{
        try{  
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
} 

هردوی این روش و روش قبلی، حتی اگه از instance استفاده نشه، بازم اون رو میسازن و طبیعتاً میتونیم بگیم توی خیلی از شرایط نمیتونه بهترین انتخاب یا اصطلاحاً best practice باشه (گرچه خیلی اوقات هم از همین روش برای پیاده سازی بدون مشکل استفاده میشه).


روش Lazy Initialization

این روش برای ساخت instance زمانی که بهش نیاز داریم استفاده میشه و به این صورت قابل پیاده سازیه:

public class LazyInitializedSingleton {
    private static LazyInitializedSingleton instance;
    private LazyInitializedSingleton(){}
    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

در حقیقت با این روش (بر خلاف دو روش قبلی) instance از اول ساخته نمیشه که شاید ازش استفاده بشه شاید هم نیشه، و بجای اون زمانی که میخوایم ازش استفاده کنیم چک میکنیم اگه قبلاً ساخته نشده بود میسازیمش.


روش Thread Safe Singleton

خب قبل از گفتن این روش یه مقدمه بخونیم.
تا اینجا سه روش مرسوم توی حالت عادی رو دیدیم. اما یه سوال، اگه ما بخوایم نرم افزارمون چندنخی (یا اصطلاحاً multi thread) باشه چی؟ بازم میتونیم از این روش استفاده کنیم؟
جواب منفیه! توی نوشتن برنامه های چندنخی، هر نخ میتونه instance جدا داشته باشه.

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

با توجه به چیزایی که گفتیم، برای اینکه جلوگیری کنیم از اینکه هر نخ instance خودش رو بسازه، میتونیم از synchronized استفاده بکنیم. بصورت زیر:

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;
    private ThreadSafeSingleton(){}
    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
} 

پیاده سازی بالا اصطلاحاً thread-safe هست (در برابر چند نخی مقاومت داره و اجازه نمیده چند نخی نتیجه ی متفاوتی از تک نخی داشته باشه) اما بخاطر استفاده از synchronized کارایی یا همون performance رو کاهش میده. برای اینکه هربار اصطلاحاً overhead نداشته باشیم از double checked locking استفاده میکنیم و کد بالا رو به شکل زیر تغییر میدیم:

public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    if(instance == null){
        synchronized (ThreadSafeSingleton.class) {
            if(instance == null){
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
} 

برای آشنایی بیشتر با اینکه چیکار کردیم میتونین نگاهی به این مقاله بندازین.


روش Bill Pugh Singleton

این روش بیشتر به نسخه های جاوا قبل از 5 مرتبط میشه که مشکلاتی در حافظه و چندنخی داشتن. به همین خاطر روش زیر مطرح شد:

public class BillPughSingleton {
    private BillPughSingleton(){}
    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
} 

دقت کنیم که instance داخل یک کلاس داخلی استاتیک (یا اصطلاحاً private inner static) قرار داره. بنابراین، زمانی که در کد بالا کلاس BillPughSingleton لود میشه، کلاس SingletonHelper توی مموری لود نمیشه (زمانی که تابع getInstance صدا زده بشه لود میشه)

این روش هم خیلی خوبه و نیازی به synchronization نداره، و البته فهم و پیاده سازیش راحته نسبتاً.

برای آشنایی بیشتر هم میتونین به این لینک مراجعه کنین.


خب تا همین جا هم تعداد روش قابل قبولی برای پیاده سازی الگوی سینگلتون دیدیم، در صورت علاقه میتونین به مقاله ی اصلی Java Singleton Design Pattern Best Practices with Examples مراجعه کنین که این موارد رو هم بیان کرده:
Using Reflection to destroy Singleton Pattern
Enum Singleton
Serialization and Singleton


منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian