Грабли
Итак, для начала собственно грабли. В унаследованном проекте активно использовались классы-одиночки. Было их больше десяти. Использовалось динамическое создание экземпляров при первом обращении, т.е. метод getInstance имел следующий вид
Singleton* Singleton::getInstance() { if (sInstance == NULL) { sInstance = new Singleton; } return sInstance; }
И тут началась охота за утечками памяти. Уничтожение одиночек предусмотрено не было, потому профилировщик выдавал кучу ложных «утечек», среди которых практически невозможно было найти реальную проблему. Да и искать не хотелось. Потому решено было написать метод destroyInstance, который позволял бы разрушать экземпляры классов одиночек по запросу.
Ситуацию существенно упрощал тот факт, что реализация Singleton-логики была вынесена в отдельный шаблонный класс — в глобальной переделке необходимости не было. Достаточно было добавить удаляющий метод и его вызов для каждого одиночки… И вот тут-то сработала теория. В основном два ее пункта
- Зависимости от классов одиночек не очевидны из интерфейсов классов и модулей, для обнаружения нужно исследовать непосредственную реализацию, что не всегда легко и даже возможно
- У 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
ссылка на оригинал статьи http://habrahabr.ru/post/185128/
Добавить комментарий