الگوی طراحی سینگلتون (Singleton)، که معنای منحصر به فرد بودن را نیز شامل میشود، یکی از سادهترین الگوهای طراحی استاندارد مورد استفاده در بین توسعهدهندگان زبانهای برنامهنویسی مختلف است. این الگو از دو اصل زیر تشکیل میشود:
- ایجاد نمونه از یک کلاس را محدود میکند تا همواره یک نمونه از آن در کل اپلیکیشن وجود داشته باشد.
- امکان دسترسی به نمونهی ایجاد شده از کل اپلیکیشن را نیز فراهم میسازد.
کاربردهای زیادی در استفاده از این الگوی طراحی وجود دارد. از جمله logging، دیتابیس و ...؛ و همچنین از این الگوی طراحی، در کنار الگوهای طراحی دیگری نیز از جمله بیلدر، فکتوری و ... استفاده میشود.
سینگلتون در جاوا
در نگاه اول، این الگوی طراحی چیز سادهای به نظر میآید؛ اما وقتی زمان پیادهسازی آن در جاوا فرار میرسد، نگرانیهای پیادهسازی زیادی را به دنبال خود به همراه دارد! چراکه نحوهی پیادهسازی این الگوی طراحی، همواره بین توسعهدهندگان بحث برانگیز بوده است. لذا ما در اینجا با روشهای مختلف پیادهسازیها آشنا خواهیم شد و بهترین آنها را انتخاب خواهیم کرد.
برای پیادهسازی این الگو، ما رویکردهای مختلفی را داریم؛ اما تمامی آنها در اصلهای زیر یکسان هستند:
- دسترسی متد سازنده باید private باشد؛ تا ایجاد نمونه از حارج کلاس (یا کلاسهای دیگر) زا محدود کند.
- یک متغیر private static از همان کلاس و داخل کلاس باید تعریف شده باشد.
- یک متد public static که نمونهای از کلاس را برگرداند؛ و این یک نقطهی دسترسی Global یا سراسری از کل اپلیکیشن به نمونهی تعریف شده از کلاس خواهد بود.
در ادامهی مقاله، ما روشهای مختلفی را برای اجرای الگوی طراحی سینگلتون یاد گرفته و با نگرانیهای(معضلات) طراحی هرکدام از آنها آشنا میشویم.
- Eager initialization (آغازگر مشتاق)
- Static block initialization (آغازگر بلوک استاتیک)
- Lazy Initialization (آغازگر تنبل)
- Thread Safe
- Bill Pugh
- مشکل استفاه از رفلکشن حهت تخریب الگوی سینگلتون
- Enum Singleton
- Serialization and Singleton
Eager initialization/آغازگر مشتاق
در روش آغازگر مشتاق، نمونهی کلاس سینگلتون، در زمان بارگزاری کلاس ایجاد خواهد شد؛ و این سادهترین روش برای ایجاد یک کلاس سینگلتون میباشد. اما مشکلی که دارد، این است که ممکن است هیچوقت از آن نمونهی ایجاد شده استفاده نشود؛ در حالی که نمونه در زمان باگزاری کلاس در حافظه (مموری)، ایجاد میشود.
package com.fullkade.singleton; public class FuLLKadeSingleton { private static final FuLLKadeSingleton instance = new FuLLKadeSingleton(); //private constructor to avoid client applications to use constructor private FuLLKadeSingleton(){} public static FuLLKadeSingleton getInstance(){ return instance; } }
همانطور که میبینید، کلاس FuLLKade در همان جایی که تعریف شده است نیز مقداردهی میشود.
اگر کلاس سینگلتون شما از منابع زیادی استفاده نکند، این روش برای استفاده مناسب است؛ اما در اکثر سناریوها، کلاس سینگلتون معمولا برای دسترسی به منابعی مانند File System، کانکشن دیتابیس و … ساخته و طراحی میشود. بنابراین ما باید از ایجاد نمونهی اولیه تا زمانی که متد getInstance فراخوانی نشده است، اجتناب کنیم. همچنین از اشکالات دیگر این روش این است که هیچ راهحلی را برای مدیریت Exception در اختیار ما قرار نداده است!
بنابراین اشکالات این روش:
- ایجاد نمونهای از کلاس در همان زمانی که کلاس در حافظه بارگزاری میشود و احتمالا از نمونه استفادهای نشود.
- نداشتن راهحل، برای مدیریت خطا (چرا که در همان ابتدا نمونه ایحاد شده است)
Static block initialization
این روش نیز همانند قبلی است با این تفاوت که مشکل دوم آن یعنی مدیریت خطا را با ایجاد نمونهی کلاس در یک بلوک استاتیک رفع کرده میکند.
package com.fullkade.singleton; public class FuLLKadeSingleton { private static FuLLKadeSingleton instance; private FuLLKadeSingleton(){} //static block initialization for exception handling static{ try{ instance = new FuLLKadeSingleton(); }catch(Exception e){ throw new RuntimeException("Exception occured in creating singleton instance"); } } public static FuLLKadeSingleton getInstance(){ return instance; } }
بنابراین هم روش قبل و هم این روش، هردویشان همچنان مشکل اول رادارند!
Lazy Initialization/آغازگر تنبل
در روش آغازگر تنبل، نمونهی کلاس، در متد سراسری ایجاد خواهد شد.
package com.fullkade.singleton; public class FuLLKadeSingleton { private static FuLLKadeSingletoninstance; private FuLLKadeSingleton(){} public static FuLLKadeSingleton getInstance(){ if(instance == null){ instance = new FuLLKadeSingleton(); } return instance; } }
همانطور که میبینید، نمونه داخل متد getInstance و در صورت null بودن ایجاد شده است؛ سپس این نمونه به بیرون متد برگردانده میشود.
البته این نوع پیادهسازی، در حالت Single Thread به خوبی کار میکند؛ اما زمانی که بخواهیم وارد مولتی تردینگ (Multi thread) شویم، ممکن است مشکلاتی را به وجود بیاورد. به این صورت که فرض کنید چند ترد (Thread) به صورت همزمان و داخل شرط if که برابر null است، قرار بگیرند! این باعث نابودی الگوی طراحی singleton خواهد شد! و تردهای مختلف، نمونههای جدا از همی را دریافت خواهند کرد! بنابراین در روش بعدی، این مشکل را نیز حل خواهیم کرد.
Thread Safe Singleton
سادهترین راه برای امن کردن کلاس سینگلتون از شر برخورد Thread های مختلف، تعریف دستسرسی synchronized برای متد است. بنابراین تنها یک ترد خواهد توانست این متد را اجرا کند و بقیهی تردها تا زمانی که ترد قبلی از متد خارج نشود، در صف انتظار قرار میگیرند.
package com.fullkade.singleton; public class FuLLKadeSingleton { private static FuLLKadeSingleton instance; private FuLLKadeSingleton(){} public static synchronized FuLLKadeSingleton getInstance(){ if(instance == null){ instance = new FuLLKadeSingleton(); } return instance; } }
حالا این پیادهسازی به خوبی کار خواهد کرد و همچنین امنیت تردها را نیز برایمان فراهم میکند؛ اما مشکلی که وجود دارد، باعث کاسته شدن کارایی (performance) میشود. چرا که از synchronized استفاده شده است. و اگرچه ما به آن فقط برای چند ترد اول که ممکن است نمونههای جدایی را ایجاد کنند، نیاز خواهیم داشت؛ ولی همواره روی متد اعمال شده باقی میماند!
بنابراین برای حل این مشکل، از روش دیگر synchronized استفاده میکنیم:
public static FuLLKadeSingleton getInstanceUsingDoubleLocking(){ if(instance == null){ synchronized (FuLLKadeSingleton.class) { if(instance == null){ instance = new FuLLKadeSingleton(); } } } return instance; }
همانطور که میبینید، در اینجا synchronized را بعد از بررسی null بودن نمونه و به صورت یک بلاک، داخل شرط نوشتهایم. لذا تردهای اضافه، بیرون آن منتظر خواهند ماند و سپس داخل آن هم دوباره null بودن را بررسی کردهایم تا درصورت وجود تردهای اضافی در صف انتظار و داخل شرط اول، شرط دوم از ایجاد نمونه جلوگیری کند.
Bill Pugh Singleton
قبل از جاوا 5، مدل حافظه (مموری) جاوا، مشکلات زیادی داشت و رویکردهای فوق،برای رد شدن از سناریوهای خاصی که تردهای زیادی سعی در گرفتن نمونه از کلاس سینگلتون و به صورت همزمان داشتند، استفاده میشد. لذا بیل پگو آمد تا با روشی متفاوت، این کشکل را حل کند. این روش از یک کلاس استاتیک داخلی به صورت زیر استفاده میکند:
package com.journaldev.singleton; public class FuLLKadeSingleton { private FuLLKadeSingleton(){} private static class SingletonHelper{ private static final FuLLKadeSingletonINSTANCE = new FuLLKadeSingleton(); } public static BillPughSingleton getInstance(){ return SingletonHelper.INSTANCE; } }
توجه داشته باشید، کلاس SingletonHelper که به صورت private static class و داخل کلاس اصلی (FuLLKade) تعریف شده است، شامل نمونهای از کلاس اصلی میباشد و این کلاس در زمان بارگزاری کلاس اصلی، بارگزاری نخواهد شد. و تنها وقتی بارگزاری میشود که متد getInstance فراخوانی شود.
این رویکرد به طور گستردهای برای کلاس سینگلتون در نظر گرفته شده است و در آن نیازی به synchronization نخواهد بود. و من خودم از این رویکرد در پروژههای زیادی استفاده میکنم و فهم و پیادهسازی آن نیز بسیار سادهتر است.
مشکل استفاده از رفلکشن برای تخریب الگوی سینگلتون
قابلیت رفلکشن یا بازتاب در برنامهنویسی، میتواند برای نابود کردن تمامی ساختارهای سینگلتون بالا استفاده شود! چرا که با این قابلیت، میتوان محدودیت ایجاد نمونه از کلاس را دور زد! به مثال زیر دقت کنید:
package com.fullkade.singleton; import java.lang.reflect.Constructor; public class ReflectionSingletonTest { public static void main(String[] args) { FuLLKadeSingleton instanceOne = FuLLKadeSingleton.getInstance(); FuLLKadeSingleton instanceTwo = null; try { Constructor[] constructors = FuLLKadeSingleton.class.getDeclaredConstructors(); for (Constructor constructor : constructors) { //Below code will destroy the singleton pattern constructor.setAccessible(true); instanceTwo = (FuLLKadeSingleton) constructor.newInstance(); break; } } catch (Exception e) { e.printStackTrace(); } System.out.println(instanceOne.hashCode()); System.out.println(instanceTwo.hashCode()); } }
زمانی که کد بالا اجرا شود، متوجه خواهید شد که هش کد دو نمونهی ایجاد شده از کلاس یکسان نیست! و این امر باعث نابودی الگوی طراحی سینگلتون ما خواهد شد!
Enum Singleton
برای غلبه بر مشکلی که بازتاب یا رفلکشن به وجود میآورد، آقای Joshua Bloch (که یکی از مهندسین نرم افزار دارای سابقهی کار در شرکت Sun Microsystems و گوگل میباشد)، پیشنهاد کرده است که از Enum ها برای پیادهسازی الگوی طراحی سینگلتون استفاده شود. چرا که جاوا تضمین میکند هر کدام از مقادیر Enum ها، تنها یک بار ایجاد شوند. و از آنجایی که مقدار Enum ها نیز به صورت گلوبال قابل دسترس است، بنابراین سینگلتون نیز میباشد. اما اشکال تنها این است که نوع enum، قدری غیرقابل انعطاف میباشد؛ و برای مثال، به ما اجازهی پیادهسازی روش lazy initialization را نمیدهد.
package com.fullkade.singleton; public enum FuLLKadeSingleton { INSTANCE; public static void doSomething(){ //do something } }
سریالیزیشن در سینگلتون
گاهی اوقات در سیستمهای توزیع شده، ما نیاز داریم رابطهای Serializable را در کلاسهای سینگلتون خود پیاده کنیم؛ تا بتوانیم وضعیت آنهارا در فایل سیستم و در نقطهی دیگری ذخیره کنیم. بنابراین در اینجا، یک کلاس کوچک سینگلتون برای پیادهسازی واسط Serializable در اختیار داریم:
package com.fullkade.singleton; import java.io.Serializable; public class FuLLKadeSingleton implements Serializable{ private static final long serialVersionUID = -7604766932017737115L; private FuLLKadeSingleton(){} private static class SingletonHelper{ private static final FuLLKadeSingleton instance = new FuLLKadeSingleton(); } public static FuLLKadeSingleton getInstance(){ return SingletonHelper.instance; } }
مشکل با کلاسهای سینگلتون serialized این است که هرگاه ما آن را deserialize کنیم، یک نمونهی جدید از کلاس ایجاد خواهد کرد! اجازه دهید ببینیم:
package com.fullkade.singleton; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; public class SingletonSerializedTest { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { FuLLKadeSingleton instanceOne = FuLLKadeSingleton.getInstance(); ObjectOutput out = new ObjectOutputStream(new FileOutputStream( "filename.ser")); out.writeObject(instanceOne); out.close(); //deserailize from file to object ObjectInput in = new ObjectInputStream(new FileInputStream( "filename.ser")); FuLLKadeSingleton instanceTwo = (FuLLKadeSingleton) in.readObject(); in.close(); System.out.println("instanceOne hashCode="+instanceOne.hashCode()); System.out.println("instanceTwo hashCode="+instanceTwo.hashCode()); } }
خروجی کد بالا به صورت زیر خواهد بود:
instanceTwo hashCode=109647522
بنابراین این نیز الگوی طراحی سینگلتون را از بین میبرد؛ و برای غلبه بر این سناریو؛ همه چیزی که ما نیاز داریم تا انجام دهیم، مجهز کردن پیادهسازی متد readResolve() با Override کردن آن میباشد.
protected Object readResolve() { return getInstance(); }
بعد از اینکار، مشکل شما حل شده و خواهید دید که هش کد هردو مورد یکسان است.
امیدوارم این مقاله به درک الگوی طراحی سینگلتون در جاوا برای شما کمک کرده باشد. موفق باشید.
بسیار عالی، مختصر و مفید