آشنایی با Singleton Design Pattern در جاوا
در این مقاله قصد داریم تا با Singleton Design Pattern آشنا شویم و به مزیت ها و معایب استفاده از این Design Pattern بپردازیم.
با استفاده از این Design Pattern اطمینان مییابیم که از یک Object تنها یک instance ایجاد شود. بنابراین اولین مفهومی که به ذهن میرسد این است که این object باید به صورت static تعریف شود. استفاده از این Design Pattern ممکن است در نگاه اول بسیار ساده باشد اما در هنگام پیاده سازی و موارد مصرف آن بعضی موارد باید مد نظر قرار گیرد که در این مقاله بررسی خواهد شد.
پیاده سازی
در ابتدا باید به شیوه پیاده سازی این Design pattern بپردازیم. ممکن است چندین روش برای پیاده سازی این Design Pattern وجود داشته باشد اما تمام آنها از این سه قانون پیروی میکنند:
- متد constructor باید به صورت private باشد تا کاربر نتواند هربار که به یک instance از کلاس احتیاج دارد، با استفاده از کلمه کلیدی new آن را ایجاد کند
- instance ایجاد شده باید به صورت private باشد و تنها از یک متد static بتوان از آن استفاده کرد.
- متدی که امکان دسترسی به instance را فراهم میکند باید به صورت public و static باشد.
معمولا پنچ روش برای پیاده سازی Singleton ها وجود دارد که به ترتیب به آنها میپردازیم.
Eager Initialization
با استفاده از این روش یک instance از کلاس وقتی که کلاس ها load میشوند، ایجاد خواهد شد. این روش علاوه بر ساده بودن، یک عیب بزرگ نیز دارد. معمولا کلاس هایی را به صورت Singleton ایجاد میکنیم که میخواهیم در طول برنامه بسیار از آنها استفاده کنیم. به همین علت معمولا سرویس های مهمی را در اختیار برنامه نویس قرار میدهند. برای مثال کلاسی که دسترسی به پایگاه داده را برای ما مهیا میکند، میتواند به صورت singleton تعریف شود. بنابراین ایجاد یک instance در هنگام load کلاس تا زمانی که از آن استفاده شود ممکن است هزینه بالایی را برای سیستم به همراه داشته باشد. قطعه کد زیر روش پیاده سازی به سبک Eager initialization را نشان میدهد:
public class EagerInitialization { private final static EagerInitialization instance = new EagerInitialization(); private EagerInitialization(){ // Zero to Hero } public static EagerInitialization getInstance() { return instance; } }
همانطور که در کد بالا مشاهده میکنید، حتی اگر کاربر از این کلاس استفاده نکند، باز هم فضایی در حافظه به آن اختصاص یافته است. هنگام ایجاد کلاس در Intellij اگر Kind را Singleton انتخاب کنید، یک کلاس با استفاده از روش Eager ایجاد میکند.
Static Block Initialization
این روش مشابه روش قبل است با این تفاوت که instance درون یک static block ایجاد میشود. تنها مزیت این روش نسبت به روش قبل این است میتوانیم به هنگام instance ساختن از روی کلاس، از Exception Handling نیز استفاده کنیم:
public class StaticBlockInitialization { static { try { INSTANCE = new StaticBlockInitialization(); } catch (RuntimeException exception) { ZeroToHeroLogger.e(exception); } } private static StaticBlockInitialization INSTANCE; private StaticBlockInitialization(){ // zero to hero static block initialization } public static StaticBlockInitialization getINSTANCE() { return INSTANCE; } }
Lazy Initialization
همانطور که تا به حال از مفهوم Lazy در قسمت های مختلف جاوا استفاده کردیم، میتوانیم ایجاد instance را نیز به صورت Lazy انجام دهیم. به این صورت که وقتی کاربر به این کلاس احتیاج داشت یک instance از این کلاس را دریافت کند. در همین هنگام اگر instance از روی این کلاس ایجاد نشده است، ایجاد شود. به این ترتیب دیگر نگران این نیستیم که بخشی از فضای بدون استفاده اشغال شده باشد.
public class LazyInitialization { private static LazyInitialization ourInstance; public static LazyInitialization getInstance() { if (ourInstance == null) { ourInstance = new LazyInitialization(); } return ourInstance; } private LazyInitialization() { // Zero to Hero lazy initialization } }
اما روش فوق ممکن است در multi threading دچار مشکل شود. به این صورت که ممکن است دو thread به صورت همزمان بخواهند برای اولین بار متد getInstance را فراخوانی کنند. به این ترتیب به هریک از آنها دو instance متفاوت میدهد و باعث میشود تا فلسفه Singleton دچار مشکل شود.
Thread Safe Initialization
اولین روشی که برای رفع مشکل روش قبل به ذهن میرسد استفاده از کلمه کلیدی synchronized است. به کلمه کلیدی synchronized به صورت کامل در مقاله ای مجزا میپردازیم اما در این مقاله به یک توضیح مختصر پیرامون آن بسنده میکنیم. وقتی از کلمه کلیدی synchronized استفاده میکنیم، این تضمین را دریافت میکنیم که در هر لحظه فقط و فقط یک thread میتواند به آن دسترسی پیدا کند. به این ترتیب مشکل روش قبل مرتفع میگردد.
public class ThreadSafeInitialization { private static ThreadSafeInitialization INSTANCE; public synchronized static ThreadSafeInitialization getINSTANCE() { if (INSTANCE == null) { INSTANCE = new ThreadSafeInitialization(); } return INSTANCE; } private ThreadSafeInitialization() { // Zero to Hero Thread Safe Initialization } }
اما استفاده از synchronized باعث میشود که بهینگی کد کاهش یابد که در مقاله مربوط به synchronized به علت آن میپردازیم.
Double Check Locking Initialization
برای بهتر کردن بهینگی در روش قبل، از روش Double Check Locking استفاده میشود:
public class DoubleCheckLockInitialization { private static DoubleCheckLockInitialization INSTANCE; public static DoubleCheckLockInitialization getINSTANCE() { if (INSTANCE == null) { synchronized (DoubleCheckLockInitialization.class) { INSTANCE = new DoubleCheckLockInitialization(); } } return INSTANCE; } }
به این ترتیب اگر instance از روی کلاس ایجاد نشده باشد، وارد synchronized میشود.
همچنین میتوانیم از یک روش دیگر با نام Bill Pugh Singleton به عنوان جایگزین synchronized استفاده کنیم:
public class BillPughInitialization { public static class BillPughHolder{ private final static BillPughInitialization INSTACE = new BillPughInitialization(); } public static BillPughInitialization getInstace() { return BillPughHolder.INSTACE; } private BillPughInitialization(){ // Zero to Hero Bill Pugh Initialization } }
در این روش با استفاده از یک holder مشکل استفاده از synchronized را مرتفع ساختهایم. در ابتدا با شروع کار JVM, کلاس اصلی که BillPughInitialization است٬ load میشود. این عمل تنها یک بار اتفاق میافتد. اما کلاس درونی که کلاس holder است load نمیشود و instance از آن ایجاد نمیگردد. وقتی متد getInstance فراخوانی شود٬ کلاس holder برای اولین بار initialize میشود و به همراه آن یک instance از کلاس اصلی نیز ایجاد میگردد. این روش thread safe است و نیازی به استفاده از syncrhonized ندارد.
Singleton میتواند با استفاده از reflection با مشکل مواجه شود. به این صورت که میتوانیم با استفاده از reflection به constructor دسترسی پیدا کنیم و یک instance جدید از روی کلاس بسازیم که با خواسته های اصلی singleton مغایرت دارد.
اما نیاز است تا کمی بیشتر پیرامون Singleton صحبت کنیم. باید توجه داشته باشیم که کلاس های Singleton باید برای کلاس هایی تعریف شوند که نیاز است تا تنها یک instance از روی آن ساخته شود. گاهی ممکن است برنامه نویسان برای آسان تر شدن کد نویسی تعداد کلاس های singleton را افزایش دهند. این امر نه تنها کارایی برنامه را افزایش نمیدهد بلکه یک تعداد object را در حافظه نگه میدارد که ممکن است از لحاظ مدیریت حافظه بهینه نباشد. برای مثال در اندروید، اگر بخواهیم از bottom navigation استفاده کنیم، بهتر است fragment ها را به صورت singleton تعریف کنیم تا در صورت جابجایی میان گزینه های bottom navigation یک instance جدید از fragment ها ایجاد نشود و در وضعیت قبلی خود باقی بمانند.
در مقالات بعد به بررسی Design Pattern های دیگر میپردازیم.
با ما همراه باشید.
مطالب زیر را حتما مطالعه کنید
از Java به Dart – کلاس و Constructor
آموزش Gradle – اهمیت Project Automation
تفاوت Sequence و List در کاتلین
درک مفهوم کدنویسی تمیز در اندروید
بهترین محیط های توسعه(IDE) برای جاوا
5 هک ساده برای کاهش سایز فایل APK
2 Comments
Join the discussion and tell us your opinion.
دیدگاهتان را بنویسید لغو پاسخ
برای نوشتن دیدگاه باید وارد بشوید.
مرسی کد ها خیلی واضح و اوکی بود . فقط اگه یه پروژه ساده اندروید رو برای هر کدوم از design pattern ها میزاشتین که کاربردشون رو به صورت عملی ببینیم بهتر بود.
ممنون از همراهیتون دوست عزیز. بله حتما سعی میشه به استفاده های این design pattern ها در اندروید هم بپردازیم.