Как стать автором
Обновить

Грабли 1: Восстание одиноких фениксов

Время на прочтение 4 мин
Количество просмотров 5.2K
Хотел написать статью о теоретических недостатках паттерна Singleton, но недолгий поиск показал, что материалов на эту тему достаточно. А вот реальных примеров архитектурных проблем с одиночками, как мне кажется, не хватает. Постараюсь восполнить этот пробел с помощью данного поста. В конце будут приведены выводы из собственных ошибок, которые пока позволяют избегать повторения проблем.

Грабли

Итак, для начала собственно грабли. В унаследованном проекте активно использовались классы-одиночки. Было их больше десяти. Использовалось динамическое создание экземпляров при первом обращении, т.е. метод getInstance имел следующий вид

Singleton* Singleton::getInstance()
{
    if (sInstance == NULL)  {
        sInstance = new Singleton;
    }
    return sInstance;
}

И тут началась охота за утечками памяти. Уничтожение одиночек предусмотрено не было, потому профилировщик выдавал кучу ложных «утечек», среди которых практически невозможно было найти реальную проблему. Да и искать не хотелось. Потому решено было написать метод destroyInstance, который позволял бы разрушать экземпляры классов одиночек по запросу.

Ситуацию существенно упрощал тот факт, что реализация Singleton-логики была вынесена в отдельный шаблонный класс — в глобальной переделке необходимости не было. Достаточно было добавить удаляющий метод и его вызов для каждого одиночки… И вот тут-то сработала теория. В основном два ее пункта
  1. Зависимости от классов одиночек не очевидны из интерфейсов классов и модулей, для обнаружения нужно исследовать непосредственную реализацию, что не всегда легко и даже возможно
  2. У singleton-класса нет явного владельца. На то он и одиночка. От того и страдает.

Вызвав все destroyInstance, я открыл профилировщик в предвкушении охоты за реальными утечками и… окунулся все в то же море ложных срабатываний! Большинство одиночек жило в свое удовольствие и дальше, несмотря на пережитое разрушение. Первым делом, как всегда, был обвинен компилятор. И, как всегда, реальная проблема оказалась в собственных руках.

Все корректно разрушалось. Но неконтролируемые зависимости существовали и между одиночками. Часто в деструкторе одного использовался другой. А поскольку getInstance не просто возвращал существующий экземпляр класса, но и при необходимости создавал новые, стали происходить массовые неконтролируемые восстания из пепла. Сразу вспомнилась книга Александреску [1]. Только, в отличие от меня, он занимался некромантией (в разделе 6.6, что почти символично) умышленно.

Ситуацию нужно было исправлять. Первая мысль — при уничтожении экземпляра выставлять флаг, препятствующий повторному созданию объекта. Но это вело к необходимости масштабной модификации и без того запутанного кода, который был написан в расчете на то, что getInstance всегда возвращает корректный указатель. В результате, осознав, что легкого решения нет, я почти полностью искоренил одиночек (оставив одного), чему до сих пор только радуюсь.

Выводы

Проблемы, связанные с управлением временем жизни singleton-классов, часто поднимаются и обсуждаются. Мне же кажется, что корень проблемы в неправильном понимании сути этого паттерна. Одиночка должен быть один. И это решает все. Да, в результате требуется более тщательное проектирование, выявление зависимостей между сущностями. Но оно того стоит. Потому что в конце концов получается единая точка управление приложением, проясняются интерфейсы и логика.

Конечно, наверняка существуют приложения, где этот подход неприемлем. Но это исключения, и к ним нужно относиться именно как с исключениям. Если приложение одно, то и полностью самостоятельный в плане своего создания и уничтожения класс должен быть один.

Например, классический пример с клавиатурой, дисплеем и логом [1]. Владельцем этих класссов можно сделать глобальный Application. Название избитое, но вот его плюсы
  • Определенность. После вызова Application::free можно не бояться фениксов зомби — все будет уничтожено именно в том порядке, в котором задумывалось разработчиком. Лог же удаляется последним простым вызовом оператора delete mLog после удаления всех остальных объектов.
  • Простота и переносимость. Не требуется использования (и даже знания) тонкостей языка. Архитектура свободно адаптируется для основных объектно-ориентированных языков. К тому же простые решения гораздо меньше подвержены ошибкам и предъявляют более низкие требования к квалификации персонала. Другими словами они дешевле и лучше сложных[3]
  • Тестируемость. Использование ухищрений типа atexit [1,2] для планирования уничтожения классов-одиночек делает невозможным сброс глобального состояния приложения без полного завершения его работы. В результате, например, для выполнения двух независимых тестов придется каждый раз перезапускать программу.


Единственной проблемой может остаться бесконтрольное создание/копирование этих классов. Так, теоретически, программист может не найти, что объект управления клавиатурой нужно получать из Application, и создать свой экземпляр. Такое развитие событий можно легко пресечь, объявив все нежелательные конструкторы закрытыми. Это сделает невозможным «ручное» создание. Чтобы класс-владелец все еще мог инстанцировать объект клавиатуры, достаточно объявить его другом.
class Keyboard
{
private:
	Keyboard();

	friend class Application;
};

Подобная реализация сыграет и важную информационную роль. Даже безо всякой документации, просто изучив интерфейс класса Keyboard, разработчик сможет понять, что за экземплярами нужно обращаться к Application.

Класс-одиночка практически всегда может быть заменен лучшим решением. По крайней мере, в моем случае за последний год каждая попытка сделать еще один singleton при более подробном анализе неизменно оказывалось проявлением лени и нежелания лишний раз подумать над планируемой архитектурой.

[1] Александреску А. Современное проектирование на С++: Обобщенное программирование и прикладные шаблоны проектирования.
[2] atexit
[3] KISS
Теги:
Хабы:
-3
Комментарии 18
Комментарии Комментарии 18

Публикации

Истории

Работа

QT разработчик
13 вакансий
Программист C++
121 вакансия

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн