. Синглтон и время жизни объекта
Синглтон и время жизни объекта

Синглтон и время жизни объекта

Эта статья является продолжением моей первой статьи “Использование паттерна синглтон” [0]. Сначала я хотел все, что связано со временем жизни, изложить в этой статье, но объем материала оказался велик, поэтому решил разбить ее на несколько частей. Это — продолжение целого цикла статей про использование различных шаблонов и методик. Данная статья посвящена времени жизни и развитию использования синглтона. Перед прочтением второй статьи настоятельно рекомендуется ознакомиться с моей первой статьей [0].

В предыдущей статье была использована следующая реализация для синглтона:

  1. Использование памяти при не созданном объекте.
  2. Использование памяти уже удаленного объекта.
  3. Неосвобождение памяти, занимаемой объектом.

На примере синглтона можно с легкостью показать, как делаются такие ошибки. Открываем статью в Википедии [1] и находим реализацию для C++:

  1. Программы по поиску утечек памяти будут все время показывать эти утечки для синглтонов.
  2. Синглтоном может быть достаточно сложный объект, обслуживающий открытый конфигурационный файл, связь с базой данных и проч. Неправильное уничтожение таких объектов может вызывать проблемы.
  3. Все начинается с малого: сначала не следим за памятью для синглтонов, а потом и для остальных объектов.
  4. И главные вопрос: зачем делать неправильно, если можно сделать правильно?
  1. Не использовать new.
  2. Не использовать delete.
Умные указатели

К счастью, в C++ есть замечательное средство, которое называется «умный указатель». Их умность заключается в том, что, хотя они и ведут себя как обычные указатели, при этом контролируют время жизни объектов. Для этого они используют счетчик, который самостоятельно подсчитывает количество ссылок на объект. При достижении счетчиком нуля объект автоматически уничтожается. Будем использовать умный указатель из стандартной библиотеки std::shared_ptr заголовочного файла memory. Стоит отметить, что такой класс доступен для современных компиляторов, которые поддерживают стандарт C++0x. Для тех, кто использует старый компилятор, можно использовать boost::shared_ptr. Интерфейсы у них абсолютно идентичны.

  1. Контроль времени жизни объектов, используя умные указатели.
  2. Создание экземпляров, в том числе и производных классов, не используя операторов new в вызывающем коде.

Этим условиям удовлетворяет следующая реализация:

  1. Конструктор использует move-семантику [6] из C++0x стандарта для увеличения быстродействия при копировании.
  2. Метод create создает объект нужного класса, по умолчанию создается объект класса T.
  3. Метод produce создает объект в зависимости от принимаемого значения. Назначение этого метода будет описано позднее.
  4. Метод copy производит глубокое копирование класса. Стоит отметить, что для копирования в качестве параметра необходимо указывать тип реального экземпляра класса, базовый тип не подходит.

При этом синглтон перепишется в следующем виде:

Вспомогательные макросы будут такими:

Небольшим изменениям подвергся макрос BIND_TO_IMPL_SINGLE, который теперь использует вместо функции single функцию anSingle, которая, в свою очередь, возвращает уже заполненный экземпляр An. О других макросах я расскажу позже.

Использование синглтона

Теперь рассмотрим использование описанного класса для реализации синглтона:

Теперь это можно использовать следующим образом:

Что на экране даст цифру 2, т.к. для реализации использовался класс Y.

Контроль времени жизни

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

Теперь посмотрим, что выведется на экран при таком вызове функции out:

Разберемся, что здесь происходит. В самом начале мы говорим, что хотим реализацию класса B, взятую из синглтона, поэтому создается класс B. Затем вызываем фукнцию out, которая берет реализацию класса A из синглтона и берет значение a. Величина a задается в конструкторе A, поэтому на экране появится цифра 1. Теперь программа заканчивает свою работу. Объекты начинают уничтожатся в обратной последовательности, т.е. сначала разрушается класс A, созданный последним, а потом разрушается класс B. При разрушении класса B мы снова зовем фукнцию out из синлтона, но т.к. объект A уже разрушен, то на экране мы видим надпись -1. Вообще говоря, программа могла и рухнуть, т.к. мы используем память уже разрушенного объекта. Таким образом, данная реализация показывает, что без контроля времени жизни программа может благополучно упасть при завершении.

Давайте теперь посмотрим, как можно сделать то же самое, но с контролем времени жизни объектов. Для этого будем использовать наш класс An:

  1. Объекты A и B используют класс An для синглтонов.
  2. Класс B явно декларирует зависимость от класса A, используя соответствующий публичный член класса (подробнее об этом подходе можно узнать из предыдущей статьи).

Посмотрим, что теперь выведется на экран:

Как видно, теперь мы продлили время жизни класса A и изменили последовательность уничтожения объектов. Отсутствие значения -1 говорит о том, что объект существовал во время доступа к его данным.

Итого

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

Многие спрашивают, а в чем, собственно, смысл? Почему нельзя просто сделать синглтон? Зачем использовать какие-то дополнительные конструкции, которые ясности не добавляют, а лишь усложняют код. В принципе, при внимательном прочтении первой статьи [0] уже можно понять, что данный подход более гибок и устраняет ряд существенных недостатков синглтона. В следующей статье будет отчетливо понятно, зачем я это так расписывал, т.к. в ней речь уже будет идти не только о синглтоне. А через статью будет вообще понятно, что синглтон тут абсолютно не при чем. Все, что я пытаюсь показать — это использование Dependency inversion principle [4] (см. также The Principles of OOD [5]). Собственно, именно после того, как я увидел впервые этот подход на Java, мне стало обидно, что в C++ это слабо используют (в принципе, есть фреймворки, которые предоставляют подобный функционал, но хотелось бы чего-то более легковесного и практичного). Приведенная реализация — это лишь маленький шажок в этом направлении, который уже приносит огромную пользу.

  1. Класс, описывающий синглтон, можно использовать в нескольких экземплярах без каких-либо ограничений.
  2. Синглтон заливается неявно посредством функции anFill, которая контролирует количество экземпляров объекта, при этом можно использовать конкретную реализацию вместо синглтона при необходимости (показано в первой статье [0]).
  3. Есть четкое разделение: интерфейс класса, реализация, связь между интерфейсом и реализацией. Каждый решает только свою задачу.
  4. Явное описание зависимостей от синглтонов, включение этой зависимости в контракт класса.
Update

Почитав комментарии я понял, что есть некоторые моменты, которые стоит прояснить, т.к. многие не знакомы с принципом обращения зависимостей (dependency inversion principle, DIP или inversion of control, IoC). Рассмотрим следующий пример: у нас есть база данных, в которой содержится необходимая нам информация, например список пользователей:

У нас есть класс, который выдает нужную нам информацию, в том числе и необходимого пользователя:

Здесь мы создаем член aDatabase, который говорит о том, что ему необходима некая база данных. Ему не важно знать, что это будет за база данных, ему не нужно знать, кто и когда это будет заполнять/заливать. Но класс UserManager знает, что ему туда зальют то, что нужно. Все, что он говорит, это: «дайте мне нужную реализацию, я не знаю какую, и я сделаю все, что вам нужно от этой базы данных, например, предоставлю необходимую информацию о пользователе из этой базы данных».

Теперь мы делаем хитрый трюк. Так как у нас есть одна лишь база данных, которая содержит всю нашу информацию, то мы говорим: ок, раз есть только одна база данных, давайте сделаем синглтон, и чтобы не париться каждый раз в заливке реализации, сделаем так, чтобы синглтон сам заливался:

Т.е. мы создаем реализацию MyDatabase и говорим, что для синглтона будем использовать именно ее, используя макрос BIND_TO_IMPL_SINGLE. Тогда следующий код автоматически будет использовать MyDatabase:

С течением времени оказалось, что у нас есть еще одна база данных, в которой тоже есть пользователи, но, скажем, для другой организации:

Мы конечно же хотим использовать наш UserManager, но уже с другой базой данных. Нет проблем:

И как по волшебству, теперь мы берем пользователя из другой базы данных! Это достаточно грубый пример, но он отчетливо показывает принцип обращения зависимостей: это когда объекту UserManager заливают реализацию IDatabase вместо традиционного подхода, когда UserManager сам ищет себе необходимую реализацию. В рассмотренной статье используется этот принцип, при этом синглтон для реализации берется как частный случай.

📎📎📎📎📎📎📎📎📎📎