Название шаблона
Одиночка (Singleton).
Тип
Порождающий шаблон проектирования (Creational).
Описание
Класс, реализующий данный шаблон:
- гарантирует, что можно создать только один его экземпляр;
- и предоставляет точку доступа для получения этого экземпляра.
Одиночка используется в случае, если в системе необходим объект только в единственном экземпляре. Например ведение отладочной информации, реализация сессий, кэш приложения, менеджер печати, доступ к аппаратному обеспечению и т. д. Нередко он используется вместе с другими шаблонами (Абстрактной фабрикой, Строителем и Прототипом) для обеспечения уникальности их экземпляра.
При проектировании приложения стоит учесть и минимизировать возможные негативные последствия использования Одиночки. Они являются проявлением его "глобализации". В частности:
- Многие части приложения становятся зависимы от него и, косвенно, друг от друга. Это усложняет внесение изменений в дальнейшем. Облегчить ситуацию можно используя Внедрение зависимостей.
- Приложение становится сложнее тестировать, т.к. данные, полученные от Одиночки, могут быть созданы в другом модуле.
Схожие шаблоны и их отличия
Альтернативой Одиночке является статический класс, который имеет следующие ограничения:
- он не может быть наследником других классов и реализовывать интерфейсы;
- нельзя создать экземпляр статического класса. Следовательно, его невозможно использовать в качестве параметра или возвращаемого значения.
Как видно из описания, Одиночка является более универсальным решением. Но если необходимо просто собрать ряд методов "под одной крышей", то для такой задачи лучше подходит статический класс.
Реализация шаблона в общем виде
- объявляем только закрытый конструктор, чтобы запретить создание экземпляров извне;
- в закрытом поле размещаем единственный экземпляр класса;
- предоставляем доступ к нему через свойство, открытое только для чтения;
- клиентский код использует это свойство для получения общего экземпляра класса.
Примеры реализации
Рассмотрим один интересный момент: единственный экземпляр класса будет размещен в статической переменной. В .NET это обеспечит потокобезопасность его инициализации.
Однако, остается еще одна проблема: тип класса, содержащего статическую переменную, получит отметку BeforeFieldInit. При ее наличии, нет гарантий, в какой момент будут инициализированы статические поля класса (ECMA 335: спецификация CLI, раздел 8.9.5).
Таким образом, нельзя определить, когда будет создан экземпляр Одиночки. Но есть несколько подходов, гарантирующих, что это произойдет только при первом обращении:
- для размещения экземпляра Одиночки создается вложенный класс со статическим конструктором. Его наличие заставит компилятор не добавлять отметку BeforeFieldInit. В этом случае, согласно спецификации, инициализация будет при первом обращении.
- в .NET 4 можно использовать класс Lazy.
Рассмотрим реализации подробнее. При изучении кода, обратите внимание, что:
- код инициализации класса потокобезопасный;
- private-конструктор, по сути, запрещает создание наследников. При этом класс, для большей наглядности, можно отметить как закрытый (sealed);
- для поля _instance, хранящего экземпляр класса, используется ключевое слово readonly. Это защитит его от возможности случайного изменения в методах класса.
1. Реализация с вложенным классом
/// <summary>Thread-safe singleton.</summary>
public sealed class Singleton
{
/// <summary>Initializes a new instance of the <see cref="Singleton"/> class.</summary>
private Singleton() { }
/// <summary>Gets the singleton instance.</summary>
public static Singleton Instance { get { return InstanceHolder._instance; } }
/// <summary>Using nested class as instance storage.</summary>
protected class InstanceHolder
{
/// <summary> Explicit static constructor to tell C# compiler
/// not to mark type as beforefieldinit.</summary>
static InstanceHolder() { }
/// <summary>The one and only instance of the Singleton class.</summary>
internal static readonly Singleton _instance = new Singleton();
}
}
2. Реализация для .NET 4 с использованием класса Lazy
/// <summary>Thread-safe .NET4 lazy singleton.</summary>
public sealed class Singleton
{
/// <summary>The one and only instance of the Singleton class.</summary>
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());
/// <summary>Initializes a new instance of the <see cref="Singleton"/> class.
/// For internal use only.</summary>
private Singleton() { }
/// <summary>Gets the singleton instance.</summary>
public static Singleton Instance { get { return Singleton._instance.Value; } }
}
И в завершении, еще один вариант реализации. Это generic-класс, наследование от которого делает из его потомка Одиночку. За основу возьмем версию с классом Lazy. Для вызова private конструктора используем Reflection. Что лучше использовать: написать пару строк кода или наследование – решать вам.
3. Реализация generic-класса для шаблона Singleton
/// <summary>Thread-safe singleton.</summary>
public class Singleton<T> where T : class
{
/// <summary>The one and only instance of the Singleton class.</summary>
private static readonly Lazy<T> _instance = new Lazy<T>(
() => (T)typeof(T).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null, new Type[0], null).Invoke(null));
/// <summary>Gets the singleton instance.</summary>
public static T Instance { get { return Singleton<T>._instance.Value; } }
}
Важный момент: при использовании в своем классе необходимо объявить private конструктор, даже если он пустой.
public sealed class MySingleton : Singleton<MySingleton>
{
private MySingleton() { }
public void DemoMethod()
{
Console.WriteLine("MySingleton Test");
}
}
class Program
{
static void Main(string[] args)
{
MySingleton instance = MySingleton.Instance;
instance.DemoMethod();
}
}
Generic-версия с вложенным классом остается, как говорится, для самостоятельной работы.