Підводні камені Singleton: чому найвідоміший шаблон проектування треба використовувати обережно?


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Патерн “Одинак” є, мабуть, найвідомішим патерном проектування. Проте він не позбавлений недоліків, тому деякі програмісти (наприклад, Єгор Бугаєнко) вважають його антипатерном. Розбираємося в тому, які ж підводні камені приховує Singleton.

Визначення патерну

Сам опис патерну простий: клас повинен гарантовано мати лише один об’єкт, і до цього об’єкта має бути наданий глобальний доступ. Швидше за все, причина його популярності якраз і криється в цій простоті – лише один клас, нічого складного. Це, напевно, найпростіший для вивчення і реалізації патерн. Якщо Ви зустрінете людину, яка щойно дізналася про наявність патернів проектування, можете бути впевненим, що вона вже знає про Singleton. Проблема полягає в тому, що коли з інструментів у Вас є тільки молоток, все навколо виглядає як цвяхи. Через це “Одинаком” часто зловживають.

Проста реалізація

Як вже зазначалося вище, в цьому немає нічого складного:

  • Зробіть конструктор класу приватним, щоб не було можливості створити екземпляр класу ззовні.
  • Зберігайте екземпляр класу в private static полі.
  • Надайте метод, який даватиме доступ до цього об’єкта.
public class Singleton { private static Singleton instance = new Singleton (); private Singleton (){ } public static Singleton getInstance (){ return instance; }}

Принцип єдиного обов’язку

Принцип єдиного обов’язку був створений спеціально: якщо клас відповідає за декілька дій, то, вносячи зміни в один аспект поведінки класу, можна зачепити й інший, що істотно ускладнює розробку. Так само розробку ускладнює той факт, що перевикористання (reusability) класу є практично неможливим. Тому доцільно було б, по-перше, винести відстежування того, чи є екземпляр класу єдиним, з класу куди-небудь назовні, а по-друге, зробити так, щоб у класу, залежно від контексту, з’явилася можливість перестати бути Singleton ‘ом, що дозволило б використати його в різних ситуаціях, залежно від необхідності (з одним екземпляром, з необмеженою кількістю екземплярів, з обмеженим набором екземплярів і т. д.).

Тестування

Недолік патерну “Одинак” – значно утруднює юніт-тестування. “Одинак” привносить до програми глобальний стан, тому Ви не можете просто взяти й ізолювати класи, які покладаються на Singelton. Якщо Ви хочете протестувати якийсь клас, то зобов’язані разом з ним тестувати і Singleton, але це ще півбіди. Стан “Одинака” може мінятися, що породжує наступні проблеми:

  • порядок тестів тепер має значення;
  • тести можуть мати небажані сторонні ефекти, породжені Singleton ‘ом;
  • Ви не можете запускати декілька тестів паралельно;
  • декілька викликів того самого теста можуть призводити до різних результатів.

На цю тему є відмінна доповідь з “Google Tech Talks”:

Приховані залежності

Зазвичай, якщо класу треба щось для роботи, це відразу зрозуміло з його методів і конструкторів. Коли очевидно, які залежності є у класу, набагато простіше їх надати. Більше того, у такому разі Ви можете використати замість реально необхідних залежностей заглушки для тестування. Якщо ж клас використовує Singleton, це може бути абсолютно не очевидно. Все стає набагато гірше, якщо екземпляру класу для роботи потрібна певна ініціалізація (наприклад, виклик методу init (...) чи на зразок того). Ще гірше, якщо у Вас є декілька Singleton ‘ів, які мають бути створені та ініціалізовані в певному порядку.

Завантажувач класу

Якщо говорити про Java, то забезпечення існування лише одного екземпляра класу, яке так необхідне для Singleton, стає все складнішим. Проблема полягає в тому, що класична реалізація, не перевіряє, чи є один екземпляр на JVM, він лише переконується, що є один екземпляр на classloader. Якщо Ви пишете невеликий клієнтський додаток, у якому використовується лише один classloader, то жодних проблем не виникне. Проте якщо використовуєте декілька завантажувачів класу або Ваший додаток повинен працювати на сервері (де можуть бути запущені декілька екземплярів додатка в різних завантажувачах класів), то все стає дуже сумним.

Десеріалізація

Ще один цікавий момент полягає в тому, що насправді стандартна реалізація Singleton не забороняє створювати нові об’єкти. Вона забороняє створювати нові об’єкти через конструктор. Є й інші способи створити екземпляр класу, і один з них – серіалізація і десеріалізація. Повного захисту від навмисного створення другого екземпляра Singelton’а можна добитися тільки за допомогою використання enum’а з єдиним станом, але це невиправдане зловживання можливостями мови, адже очевидно, що enum має інше призначення.

Потоконебезпечність

Один з популярних варіантів реалізації Singleton містить ледачу ініціалізацію. Це означає, що об’єкт класу створюється не на самому початку, а лише коли буде отримано перше звернення до нього. Добитися цього зовсім не складно:

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

Проте тут починаються проблеми з потоками, які можуть створювати декілька різних об’єктів. Відбувається це приблизно так:

  • перший потік звертається до getInstance (), коли об’єкт ще не створений;
  • у цей час другий потік також звертається до цього методу, поки перший ще не встиг створити об’єкт, і сам створює його;
  • перший потік створює ще один, другий, екземпляр класу.

Зрозуміло, можна просто помітити метод як synchronised, і ця проблема зникне. Проблема полягає в тому, що, зберігаючи час на старті програми, ми тепер втрачатимемо його кожного разу при зверненні до Singleton через те, що метод синхронізований, а це дуже дорого, якщо до екземпляра доводиться часто звертатися. Адже єдиний раз, коли властивість synchronised дійсно вимагається – перше звернення до методу.

Є два способи розв’язати цю проблему. Перший – позначити як synchronised не весь метод, а тільки блок, де створюється об’єкт:

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

Не забувайте, що це не можна використати у версії Java нижче від 1.5, тому що там використовується інша модель пам’яті. Також не забудьте позначити поле instance як volatile.

Другий шлях – використати патерн “Lazy Initialization Holder”. Це рішення ґрунтується на тому, що вкладені класи не ініціалізувалися до першого їх використання (саме те, що нам треба):

public class Singleton { private Singleton (){ } public static Singleton getInstance (){ return SingletonHolder.instance; } private static class SingletonHolder { private static final Singleton instance = new Singleton (); }}

Рефлексія

Ми забороняємо створювати декілька екземплярів класу, позначаючи конструктор приватним. Проте, використовуючи рефлексію, можна без докладання особливих зусиль змінити видимість конструктора private на publicпід час виконання:

Class clazz = Singleton.class;Constructor constructor = clazz.getDeclaredConstructor ();constructor.setAccessible (true);

Звичайно, якщо Ви використовуєте Singleton тільки у своєму додатку, переживати не варто.  Проте якщо Ви розробляєте модуль, який потім використовуватиметься в сторонніх додатках, то через це можуть виникнути проблеми. Які саме, залежить від того, що робить Ваший “Одинак”: ризики, пов’язані з безпекою, чи непередбачувана поведінка модуля.

Висновок

Попри те що патерн Singleton дуже відомий і популярний, у нього є багато серйозних недоліків. Дедалі більше цих недоліків виявляється, і оригінальні патерни з книги GOF “Design Patterns” часто сьогодні вважаються антипатернами. Однак сама ідея мати лише один об’єкт на клас, як і раніше має сенс, але складно реалізувати її правильно.

Переклад статті “Singleton Pattern Pitfalls”

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *