Простой пример использования паттерна singleton си. Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью

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

Проблема

Одиночка решает сразу две проблемы, нарушая принцип единственной ответственности класса.

    Гарантирует наличие единственного экземпляра класса . Чаще всего это полезно для доступа к какому-то общему ресурсу, например, базе данных.

    Представьте, что вы создали объект, а через некоторое время пробуете создать ещё один. В этом случае хотелось бы получить старый объект, вместо создания нового.

    Такое поведение невозможно реализовать с помощью обычного конструктора, так как конструктор класса всегда возвращает новый объект.


Клиенты могут не подозревать, что работают с одним и тем же объектом.

    Предоставляет глобальную точку доступа . Это не просто глобальная переменная, через которую можно достучаться к определённому объекту. Глобальные переменные не защищены от записи, поэтому любой код может подменять их значения без вашего ведома.

    Но есть и другой нюанс. Неплохо бы хранить в одном месте и код, который решает проблему №1, а также иметь к нему простой и доступный интерфейс.

Интересно, что в наше время паттерн стал настолько известен, что теперь люди называют «одиночками» даже те классы, которые решают лишь одну из проблем, перечисленных выше.

Решение

Все реализации одиночки сводятся к тому, чтобы скрыть конструктор по умолчанию и создать публичный статический метод, который и будет контролировать жизненный цикл объекта-одиночки.

Если у вас есть доступ к классу одиночки, значит, будет доступ и к этому статическому методу. Из какой точки кода вы бы его ни вызвали, он всегда будет отдавать один и тот же объект.

Аналогия из жизни

Правительство государства - хороший пример одиночки. В государстве может быть только одно официальное правительство. Вне зависимости от того, кто конкретно заседает в правительстве, оно имеет глобальную точку доступа «Правительство страны N».

Структура



    Одиночка определяет статический метод getInstance , который возвращает единственный экземпляр своего класса.

    Конструктор одиночки должен быть скрыт от клиентов. Вызов метода getInstance должен стать единственным способом получить объект этого класса.

Псевдокод

В этом примере роль Одиночки отыгрывает класс подключения к базе данных.

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

// Класс одиночки определяет статический метод `getInstance`, // который позволяет клиентам повторно использовать одно и то же // подключение к базе данных по всей программе. class Database is // Поле для хранения объекта-одиночки должно быть объявлено // статичным. private static field instance: Database // Конструктор одиночки всегда должен оставаться приватным, // чтобы клиенты не могли самостоятельно создавать // экземпляры этого класса через оператор `new`. private constructor Database() is // Здесь может жить код инициализации подключения к // серверу баз данных. // ... // Основной статический метод одиночки служит альтернативой // конструктору и является точкой доступа к экземпляру этого // класса. public static method getInstance() is if (Database.instance == null) then acquireThreadLock() and then // На всякий случай ещё раз проверим, не был ли // объект создан другим потоком, пока текущий // ждал освобождения блокировки. if (Database.instance == null) then Database.instance = new Database() return Database.instance // Наконец, любой класс одиночки должен иметь какую-то // полезную функциональность, которую клиенты будут // запускать через полученный объект одиночки. public method query(sql) is // Все запросы к базе данных будут проходить через этот // метод. Поэтому имеет смысл поместить сюда какую-то // логику кеширования. // ... class Application is method main() is Database foo = Database.getInstance() foo.query("SELECT ...") // ... Database bar = Database.getInstance() bar.query("SELECT ...") // Переменная "bar" содержит тот же объект, что и // переменная "foo".

Применимость

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

Одиночка скрывает от клиентов все способы создания нового объекта, кроме специального метода. Этот метод либо создаёт объект, либо отдаёт существующий объект, если он уже был создан.

Когда вам хочется иметь больше контроля над глобальными переменными.

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

Тем не менее, в любой момент вы можете расширить это ограничение и позволить любое количество объектов-одиночек, поменяв код в одном месте (метод getInstance).

Шаги реализации

    Добавьте в класс приватное статическое поле, которое будет содержать одиночный объект.

    Объявите статический создающий метод, который будет использоваться для получения одиночки.

Этим постом я открываю цикл статей, посвященных паттернам проектирования. Все написанное мной основывается исключительно на личном опыте.

Паттерны проектирования - это описание некоторых проблем, возникающих во время объектно-ориентированного проектирования, а также способов их решения (как практических, так и теоретических). Иными словами - это примеры правильных подходов к решению типичных задач проектирования.

Одним из самых распространенных паттернов является Singleton (Одиночка). Задача этого шаблона ограничить количество экземпляров некоторого класса в пределах приложения. Зачем это может понадобиться на практике? Об этом читайте чуть ниже. План на сегодня таков:

Простая реализация Singleton

Один из самых простых способов реализовать паттерн Singleton на языке Java выглядит так:

Public final class Singleton { private static Singleton _instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (_instance == null) _instance = new Singleton(); return _instance; } }

Теперь приведу некоторые объяснения по поводу реализации шаблона.

Конструктор класса необходимо объявить с модификатором видимости private. Это предотвратит создание экземпляров класса как с помощью класса Singleton, так и с помощью его наследников. В связи с этим к объявлению класса смело можно дописать модификатор final.

Метод getInstance() создаст ровно один экземпляр класса Singleton. Этот метод объявлен как synchronized. Сделано это вот почему. В многопоточных программах при одновременном вызове метода getInstance() из нескольких потоков можно создать несколько экземпляров класса Singleton. А должен остаться только один!

От модификатора synchronized можно избавиться. Для этого _instance нужно проинициализировать:

Private static final Singleton _instance = new Singleton(),

а в методе getInstance() убрать конструкцию "if". Тогда инициализация произойдет во время загрузки класса.

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

Синглтон с double-checked locking

Проблема с lazy инициализацией остается только в том, что синхронизация нужна по идее только один раз, чтобы несколько потоков не вошли в критическую секцию одновременно. Но после создания экземпляра класса от синхронизации хотелось бы избавиться.

Самый распространенный способ избавиться от лишней синхронизации - это double-checked locking, который выглядит таким образом:

Public final class Singleton { private static volatile Singleton _instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (_instance == null) synchronized (Singleton.class) { if (_instance == null) _instance = new Singleton(); } return _instance; } }

Кстати, до вресии Java SE 1.5 этот код не работал. Если хотите знать почему - читайте .

Синглтон с Instance Holder

А вот еще один заслуживающий внимания вариант реализации шаблона Одиночка с ленивой инициализацией:

Public final class Singleton { private Singleton() {} private static class Holder { private static final Singleton _instance = new Singleton(); } public static Singleton getInstance() { return Holder._instance; } }

Объект будет проинициализирован при первом вызове метода getInstance(). То есть мы перенесли проблему с синхронизацией на уровень загрузчика классов (class loader).

Но что если в приложении несколько Class Loader"ов или вообще у нас распределенная система с несколькими виртуальнми машинами Java? Тогда все сложно. Поговорим об этом в другой раз.

А вообще сейчас модно говорить, что реально крутой вариант паттерна Одиночка выглядит так:

Public enum Singleton { INSTANCE; }

Вот собственно и все... Ах, да! Зачем нам это нужно.

Практический пример синглтона

Мне приходится чаще всего использовать этот паттерн при работе с конфигурацией. Иногда конфигурацию программы удобно хранить в файле. Допустим, это будет простой текстовый файл "props.txt" со строками типа "ключ=значение". Нам нужно гарантировать, что конфигурация в программе будет в единственном экземпляре. Вторую мы бы и так не создали, но нужно запретить это делать пользователю класса. Итак,

Import java.util.*; import java.io.*; public class Configuration { private static Configuration _instance = null; private Properties props = null; private Configuration() { props = new Properties(); try { FileInputStream fis = new FileInputStream(new File("props.txt")); props.load(fis); } catch (Exception e) { // обработайте ошибку чтения конфигурации } } public synchronized static Configuration getInstance() { if (_instance == null) _instance = new Configuration(); return _instance; } // получить значение свойства по имени public synchronized String getProperty(String key) { String value = null; if (props.containsKey(key)) value = (String) props.get(key); else { // сообщите о том, что свойство не найдено } return value; } }

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

String propValue = Configuration.getInstance().getProperty(propKey).

Если имена свойств в "props.txt" меняться не будут, можно описать их в классе таким образом:

Public static final String PROP_KEY = "propKey",

а значения получать так:

String propValue = Configuration.getInstance() .getProperty(Configuration.PROP_KEY).

Паттерн Singleton полезен не только при работе с конфигурациями. Его можно использовать также при написании ConnectionPool, Factory и других вещей.

Вот теперь точно все.

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

Паттерн - пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.

Определение паттерна

Само описание паттерна достаточно простое - класс должен гарантированно иметь лишь один объект, и к этому объекту должен быть предоставлен глобальный доступ. Скорее всего, причина его популярности как раз и кроется в этой простоте - всего лишь один класс, ничего сложного. Это, наверное, самый простой для изучения и реализации паттерн. Если вы встретите человека, который только что узнал о существовании паттернов проектирования, можете быть уверены, что он уже знает про Singleton. Проблема заключается в том, что когда из инструментов у вас есть только молоток, всё вокруг выглядит как гвозди. Из-за этого “Одиночкой” часто злоупотребляют.

Простейшая реализация

Как уже говорилось выше, в этом нет ничего сложного:

  • Сделайте конструктор класса приватным, чтобы не было возможности создать экземпляр класса извне.
  • Храните экземпляр класса в private static поле.
  • Предоставьте метод, который будет давать доступ к этому объекту.
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } }

Принцип единственной обязанности

В объектно-ориентированном программировании существует правило хорошего тона - “Принцип едиственной обязанности” (Single Responsibility Principle, первая буква в аббревиатуре SOLID). Согласно этому правилу, каждый класс должен отвечать лишь за один какой-то аспект. Совершенно очевидно, что любой Singleton-класс отвечает сразу за две вещи: за то, что класс имеет лишь один объект, и за реализацию того, для чего этот класс вообще был создан.

Принцип единственной обязанности был создан не просто так - если класс отвечает за несколько действий, то, внося изменения в один аспект поведения класса, можно затронуть и другой, что может сильно усложнить разработку. Так же разработку усложняет тот факт, что переиспользование (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” часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.

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

Если почитать литературу на эту тему, то можно встретить различную критику данного подхода. Приведу список недостатков :
  1. Синглтон нарушает SRP (Single Responsibility Principle) - класс синглтона, помимо того чтобы выполнять свои непосредственные обязанности, занимается еще и контролированием количества своих экземпляров.
  2. Зависимость обычного класса от синглтона не видна в публичном контракте класса. Так как обычно экземпляр синглтона не передается в параметрах метода, а получается напрямую, через getInstance(), то для выявления зависимости класса от синглтона надо залезть в тело каждого метода - просто просмотреть публичный контракт объекта недостаточно. Как следствие: сложность рефакторинга при последующей замене синглтона на объект, содержащий несколько экземпляров.
  3. Глобальное состояние. Про вред глобальных переменных вроде бы уже все знают, но тут та же самая проблема. Когда мы получаем доступ к экземпляру класса, мы не знаем текущее состояние этого класса, и кто и когда его менял, и это состояние может быть вовсе не таким, как ожидается. Иными словами, корректность работы с синглтоном зависит от порядка обращений к нему, что вызывает неявную зависимость подсистем друг от друга и, как следствие, серьезно усложняет разработку.
  4. Наличие синглтона понижает тестируемость приложения в целом и классов, которые используют синглтон, в частности. Во-первых, вместо синглтона нельзя подпихнуть Mock-объект, а во-вторых, если синглтон имеет интерфейс для изменения своего состояния, то тесты начинают зависеть друг от друга.
Таким образом, при наличии данных проблем многие делают вывод о том, что использование этого паттерна следует избегать. В целом, я согласен с приведенными проблемами, однако я не согласен с тем, что на основании данных проблем можно делать вывод о том, что не стоит использовать синглтоны. Давайте рассмотрим более детально, что я имею ввиду и как можно избежать указанных проблем даже при использовании синглтона.

Реализация

Первое, что хотелось бы отметить: синглтон - это реализация, а не интерфейс. Что это значит? Это значит, что класс по возможности должен использовать некий интерфейс, а то, будет там синглтон или нет, это он не знает и знать не должен, т.к. всякое явное использование синглтона и будет приводить к указанным проблемам. На словах выглядит хорошо, давайте посмотрим, как это должно выглядеть в жизни.

Для реализации данной идеи мы воспользуемся мощным подходом, который называется Dependency Injection. Суть его состоит в том, что мы неким образом заливаем реализацию в класс, при этом класс, использующий интерфейс, не заботится о том, кто и когда это будет делать. Его эти вопросы вообще не интересуют. Все, что он должен знать, это как правильно использовать предоставленный функционал. Интерфейс функционала при этом может быть как абстрактный интерфейс, так и конкретный класс. В нашем конкретном случае это неважно.

Идея есть, давайте реализуем на языке C++. Тут нам помогут шаблоны и возможность их специализации. Для начала определим класс, который будет содержать указатель на необходимый экземпляр:
template struct An { An() { clear(); } T* operator->() { return get0(); } const T* operator->() const { return get0(); } void operator=(T* t) { data = t; } bool isEmpty() const { return data == 0; } void clear() { data = 0; } void init() { if (isEmpty()) reinit(); } void reinit() { anFill(*this); } private: T* get0() const { const_cast(this)->init(); return data; } T* data; };
Описанный класс решает несколько задач. Во-первых, он хранит указатель на необходимый экземпляр класса. Во-вторых, при отсутствии экземпляра вызывается функция anFill, которая заполняет нужным экземпляром в случае отсутствия такового (метод reinit). При обращении к классу происходит автоматическая инициализация экземпляром и его вызов. Посмотрим на реализацию функции anFill:
template void anFill(An& a) { throw std::runtime_error(std::string("Cannot find implementation for interface: ") + typeid(T).name()); }
Таким образом по умолчанию данная функция кидает исключение с целью предотвращения использования незадекларированной функции.

Примеры использования

Теперь предположим, что у нас есть класс:
struct X { X() : counter(0) {} void action() { std::cout << ++ counter << ": in action" << std::endl; } int counter; };
Мы хотим сделать его синглтоном для использования в различных контекстах. Для этого специализируем функцию anFill для нашего класса X:
template<> void anFill(An& a) { static X x; a = &x; }
В данном случае мы использовали простейший синглтон и для наших рассуждений конкретная реализация не имеет значения. Стоит отметить, что данная реализация не является потокобезопасной (вопросы многопоточности будут рассмотрены в другой статье). Теперь мы можем использовать класс X следующим образом:
An x; x->action();
Или проще:
An()->action();
Что выведет на экран:
1: in action
При повторном вызове action мы увидим:
2: in action
Что говорит о том, что у нас сохраняется состояние и экземпляр класса X ровно один. Теперь усложним немного пример. Для этого создадим новый класс Y, который будет содержать использование класса X:
struct Y { An x; void doAction() { x->action(); } };
Теперь если мы хотим использовать экземпляр по умолчанию, то нам просто можно сделать следующее:
Y y; y.doAction();
Что после предыдущих вызовов выведет на экран:
3: in action
Теперь предположим, что мы захотели использовать другой экземпляр класса. Это сделать очень легко:
X x; y.x = &x; y.doAction();
Т.е. мы заполняем класс Y нашим (известным) экземпляром и вызываем соответствующую функцию. На экране мы получим:
1: in action
Разберем теперь случай с абстракными интерфейсами. Создадим абстрактный базовый класс:
struct I { virtual ~I() {} virtual void action() = 0; };
Определим 2 различные реализации этого интерфейса:
struct Impl1: I { virtual void action() { std::cout << "in Impl1" << std::endl; } }; struct Impl2: I { virtual void action() { std::cout << "in Impl2" << std::endl; } };
По умолчанию будем заполнять, используя первую реализацию Impl1:
template<> void anFill(An& a) { static Impl1 i; a = &i; }
Таким образом, следующий код:
An i; i->action();
Даст вывод:
in Impl1
Создадим класс, использующий наш интерфейс:
struct Z { An i; void doAction() { i->action(); } };
Теперь мы хотим поменять реализацию. Тогда делаем следующее:
Z z; Impl2 i; z.i = &i; z.doAction();
Что дает в результате:
in Impl2

Развитие идеи

В целом на этом можно было бы закончить. Однако стоит добавить немножко полезных макросов для облегчения жизни:
#define PROTO_IFACE(D_iface) \ template<> void anFill(An& a) #define DECLARE_IMPL(D_iface) \ PROTO_IFACE(D_iface); #define BIND_TO_IMPL_SINGLE(D_iface, D_impl) \ PROTO_IFACE(D_iface) { a = &single(); } #define BIND_TO_SELF_SINGLE(D_impl) \ BIND_TO_IMPL_SINGLE(D_impl, D_impl)
Многие могут сказать, что макросы - это зло. Ответственно заявляю, что с данным фактом я знаком. Тем не менее, это часть языка и ее можно использовать, к тому же я не подвержен догмам и предрассудкам.

Макрос DECLARE_IMPL декларирует заполнение, отличное от заполнения по умолчанию. Фактически эта строчка говорит о том, что для этого класса будет происходить автоматическое заполнение неким значением в случае отсутствия явной инициализации. Макрос BIND_TO_IMPL_SINGLE будет использоваться в CPP файле для реализации. Он использует функцию single, которая возвращает экземпляр синглтона:
template T& single() { static T t; return t; }
Использование макроса BIND_TO_SELF_SINGLE говорит о том, что для класса будет использоваться экземпляр его самого. Очевидно, что в случае абстракного класса этот макрос неприменим и необходимо использовать BIND_TO_IMPL_SINGLE с заданием реализации класса. Данная реализация может быть скрыта и объявлена только в CPP файле.

Теперь рассмотрим использование уже на конкретном примере, например конфигурации:
// IConfiguration.hpp struct IConfiguration { virtual ~IConfiguration() {} virtual int getConnectionsLimit() = 0; virtual void setConnectionLimit(int limit) = 0; virtual std::string getUserName() = 0; virtual void setUserName(const std::string& name) = 0; }; DECLARE_IMPL(IConfiguration) // Configuration.cpp struct Configuration: IConfiguration { Configuration() : m_connectionLimit(0) {} virtual int getConnectionsLimit() { return m_connectionLimit; } virtual void setConnectionLimit(int limit) { m_connectionLimit = limit; } virtual std::string getUserName() { return m_userName; } virtual void setUserName(const std::string& name) { m_userName = name; } private: int m_connectionLimit; std::string m_userName; }; BIND_TO_IMPL_SINGLE(IConfiguration, Configuration);
Далее можно использовать в других классах:
struct ConnectionManager { An conf; void connect() { if (m_connectionCount == conf->getConnectionsLimit()) throw std::runtime_error("Number of connections exceeds the limit"); ... } private: int m_connectionCount; };

Выводы

В качестве итога я бы отметил следующее:
  1. Явное задание зависимости от интерфейса: теперь не надо искать зависимости, они все прописаны в декларации класса и это является частью его интерфейса.
  2. Обеспечение доступа к экземпляру синглтона и интерфейс класса разнесены в разные объекты. Таким образом каждый решает свою задачу, тем самым сохраняя SRP.
  3. В случае наличия нескольких конфигураций можно легко заливать нужный экземпляр в класс ConnectionManager без каких-либо проблем.
  4. Тестируемость класса: можно сделать mock-объект и проверить, например, правильность работы условия при вызове метода connect:
    struct MockConfiguration: IConfiguration { virtual int getConnectionsLimit() { return 10; } virtual void setConnectionLimit(int limit) { throw std::runtime_error("not implemented in mock"); } virtual std::string getUserName() { throw std::runtime_error("not implemented in mock"); } virtual void setUserName(const std::string& name) { throw std::runtime_error("not implemented in mock"); } }; void test() { // preparing ConnectionManager manager; MockConfiguration mock; manager.conf = &mock; // testing try { manager.connect(); } catch(std::runtime_error& e) { //... } }
Таким образом описанный подход избавляет от проблем, указанных в начале этой статьи. В последующих статьях я хотел бы затронуть важные вопросы, связанные с временем жизни и многопоточностью.

Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться:)

Что такое статический класс?

Для начала вспомним что такое статический класс и для чего он нужен. В любом CLI-совместимом языке используется следующая парадигма инкапсуляции глобальных переменных: глобальных перменных нет . Все члены, в том числе и статические, могут быть объявлены только в рамках какого-либо класса, а сами классы могут (но не должны ) быть сгруппированы в каком-либо пространстве имен. И если раньше приходилось иммитировать поведение статического класса с помощью закрытого конструктора, то в.NET Framework 2.0 была добавлена поддержка статических классов на уровне платформы. Основное отличие статического класса от обычного, нестатического, в том, что невозможно создать экземпляр этого класса с помощью оператора new . Статические классы по сути являются некой разновидностью простанства имен - только в отличие от последних предназначены для размещения статических переменных и методов а не типов.

Что такое Singleton (Одиночка)?

Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр , и предоставляет к нему глобальную точку доступа . Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи - в сети существует масса подробной информации о нем (например и ). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.

А если нет разницы - зачем плодить больше?

Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Singleton
Static class
Количество точек доступа
Одна (и только одна) точка доступа - статическое поле Instance
N (зависит от количества публичных членов класса и методов)
Наследование классов
Возможно, но не всегда (об этом - ниже)
Невозможно - статические классы не могут быть экземплярными, поскольку нельзя создавать экземпляры объекты статических классов
Наследование интерфейсов
Возможно, безо всяких ограничений

Возможность передачи в качестве параметров
Возможно, поскольку Singleton предоставляет реальный объект
Отсутствует
Контроль времени жизни объекта
Возможно - например, отложенная инициализация (или создание по требованию )
Невозможно по той же причине, по которой невозможно наследование классов
Использование абстрактной фабрики для создания экземпляра класса
Возможно
Невозможно по причине осутствия самой возможности создания экземпляра
Сериализация
Возможно
Неприменима по причине отсутствия экземпляра

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

Singleton в «канонической» реализации:
public class Session { private static Session _instance; // Реализация паттерна... public static Session Instance { get { // ... return _instance; } } public IUser GetUser() { // ... } public bool IsSessionExpired() { // ... } public Guid SessionID { get { // ... } } }

Статический класс:
public static class Session { // Точка доступа 1 public static IUser GetUser() { // ... } // Точка доступа 2 public static bool IsSessionExpired() { // ... } // ... // Точка доступа N public static Guid SessionID { get { // ... } } }

Наследование классов
С наследованием статических классов все просто - оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
public class Singleton where T: class { private static T _instance; protected Singleton() { } private static T CreateInstance() { ConstructorInfo cInfo = typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type, new ParameterModifier); return (T)cInfo.Invoke(null); } public static T Instance { get { if (_instance == null) { _instance = CreateInstance(); } return _instance; } } } public class Session: Singleton { public IUser GetUser() { // ... } public bool IsSessionExpired() { // ... } public Guid SessionID { get { // ... } } }
А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено - это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
public class Session: CoreObject { private Session() { } public static Session Instance { get { return Singleton.Instance; } } }
Наследование интерфейсов
Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное - избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
// Этот класс является синглтоном и реализует интерфейс ISession public class Session: CoreObject, ISession { private Session() { } public static Session Instance { get { return Singleton.Instance; } } } // Этот класс не является синглтоном и вообще может быть объявлен и реализован в другой сборке // полностью скрывая детали реализации public class VpnSession: ISession { } public interface ISessionManager { ISession GetSession(Guid sessionID); // Принимает интерфейс ISession, следуя принципам уменьшения связности bool IsSessionExpired(ISession session); }
Возможность передачи в качестве параметров
Для статических классов это не поддерживается - можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection ). Синглтон же по сути является обычным экземпляром объекта:
// ... ISessionManager _sessionManager; // ... bool isExpired = _sessionManager.IsSessionExpired(Session.Instance);
Контроль времени жизни объекта
Время жизни статического класса ограничено временем жизни домена - если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример - отложенная инициализация:
public class Singleton where T: class { // ... public static T Instance { get { if (_instance == null) { // Создание "по требованию" _instance = CreateInstance(); } return _instance; } } }
Можно также добавить операцию удаления экземпляра синглтона:
public class Singleton where T: class { // ... public static T Instance { // ... } // Очень опасная операция! public void RemoveInstance() { _instance = null; } }
Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования - например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
public class Singleton where T: class { // ... public static T Instance { get { if (!IsAlive) { // Удаление по условию RemoveInstance(); } if (_instance == null) { // Создание "по требованию" _instance = CreateInstance(); } return _instance; } } private void RemoveInstance() { _instance = null; } }
Использование абстрактной фабрики для создания экземпляра класса
Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
public interface IAbstractFactory { T Create(); bool IsSupported(); } public class Singleton where T: class { private static T _instance; private static IAbstractFactory _factory; protected Singleton(IAbstractFactory factory) { _factory = factory; } public static T Instance { get { if (_instance == null) { _instance = _factory.Create(); } return _instance; } } } // Вариант с прямым наследованием от синглтона public class Session: Singleton { protected Session() : base(new ConcreteFactory()) { } // ... }
Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
public class Session: CoreObject, ISession { private class SessionSingleton: Singleton { protected SessionSingleton() : base(new ConcreteFactory2()) { } } private Session() : base(new CoreContext()) { } public static Session Instance { get { return SessionSingleton.Instance; } } // ... }
Сериализация
Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.

Так что же использовать Синглтон или Статический класс?

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

Использование синглотона оправдано, когда:

  • Необходимо наследование классов или интерфейсов или делегаровать конструирование объектов фабрике
  • Необходимо использование экземпляров класса
  • Необходимо контролировать время жизни объекта (хоть это и очень редкая задача для синглтона)
  • Необходимо сериализовать объект (такая задача гипотетически возможна, но трудно представить себе сценарии использования)
Использование статических классов целесообразно тогда , когда у вас нет необходимости реализовывать ни один из сценариев перечисленных для синглтона. Основное назначение статических классов все-таки в группировке логически схожих методов, констант, полей и свойств. Например: System.Math , System.BitConverter , System.Buffer , System.Convert и т.д.
mob_info