Andrey on .NET | Порождающие шаблоны: Одиночка (Singleton)

Порождающие шаблоны: Одиночка (Singleton)

Название шаблона

Одиночка (Singleton).

Тип

Порождающий шаблон проектирования (Creational).

Описание

Singleton patternКласс, реализующий данный шаблон:

  • гарантирует, что можно создать только один его экземпляр;
  • и предоставляет точку доступа для получения этого экземпляра.

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

При проектировании приложения стоит учесть и минимизировать возможные негативные последствия использования Одиночки. Они являются проявлением его "глобализации". В частности:

  1. Многие части приложения становятся зависимы от него и, косвенно, друг от друга. Это усложняет внесение изменений в дальнейшем. Облегчить ситуацию можно используя Внедрение зависимостей.
  2. Приложение становится сложнее тестировать, т.к. данные, полученные от Одиночки, могут быть созданы в другом модуле.

Схожие шаблоны и их отличия

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

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

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

Реализация шаблона в общем виде

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

Примеры реализации

Рассмотрим один интересный момент: единственный экземпляр класса будет размещен в статической переменной. В .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-версия с вложенным классом остается, как говорится, для самостоятельной работы.

Комментарии (18) -

Нужно будет посмотреть как инициализируются поля класов помеченых BeforeFieldInit в Mono. Не факт что все существующие реализации .NET FW строго соблюдают спецификацию ECMA 335.

@ xna: С Mono (пока) не доводилось работать. Если посмотрите – напишите в комментариях, будет интересно. Только момент – смотрите на не помеченные. Помеченные могут быть инициализированы в любой время до первого обращения. И инициализация именно в этот момент не будет нарушением.

За пост, спасибо. Последний пример не очень удачный. Поясню, наследование слишком сильная связь, чтобы использовать её для singleton'а. Как вариант, применять singleton в качестве хранилища для вашего объекта. Вот удачный пример:

/// <summary>

/// Used for classes that are single instances per appdomain
/// </summary>
public static class Singleton
{
  private static class Storage<T>
  {
    internal static T s_instance;
  }

  [SuppressMessage("Microsoft.Reliability", "CA2002")]
  public static T GetInstance<T>(Func<T> op)
  {
    if (Storage<T>.s_instance == null)
    {
      lock (typeof(Storage<T>))
      {
        if (Storage<T>.s_instance == null)
        {
          T temp = op();
          System.Threading.Thread.MemoryBarrier();
          Storage<T>.s_instance = temp;
        }
      }
    }
    return Storage<T>.s_instance;
  }

  public static T GetInstance<T>()
    where T : new()
  {
    return GetInstance(() => new T());
  }
}

С Lazy будет покороче, но это тоже на дом читателям.

@ osmirnov: Все же не соглашусь с вами, относительно неудачи с примером. Да, наследование сильная связь, которая выражается в зависимости наследника от изменений базового класса. Однако в чем она (зависимость) выражается в данном случае? По сути, наследование используется чтобы добавить статическое после в класс. Это больше похоже на надстройку, т.к. мы не привязываемся ни к полям ни к методам (конструктор все же не в счет).

Так в чем же по вашему опасность связи в данном случае?

Привет.
Сейчас пытаюсь читать "Archtecting Application for the Enterprise" Дино Эспозито со товарищи. Мутновато и скучновато. А ваши статьи прямо хороши. Пишите еще. Я ваш читатель.
Спасибо.

Капитан 02.10.2010 4:39:42

Автор, ваша статья капитан очевидность.

@ Mabanza: Спасибо за отзыв.

@ Капитан: На ваши лавры не претендую Smile


(T)typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[0], null).Invoke(null));

Зачем? Разве нельзя сделать Activator.CreateInstance<T>()?

>>гарантирует, что можно создать только один его экземпляр;

Синглтон, контролирует количество своих экземпляров. Их может быть и два и три. Зачем брать и коверкать Гофа. Серия статей бестолковая. Подобного рода статьи не имеют смысла. Гоф не меняется с 95 года. Придумаете что-то новое и полезное - издайте книгу. Не путайте читателей своим неглубоким пониманием вопроса. Лучше рекомендуйте литературу. Этого хватит.

После просмотра вот этого видео:
http://www.youtube.com/watch?v=-FRm3VPhseI
считаю, что шаблон "одиночка" не так уж и полезен...
Как говорится, смотреть и видеть не одно и тоже.


        @ Дмитрий: В принципе без особой разницы. Смотрите: вариант указанный вами не пройдет, т.к. вызываемый конструктор должен быть private. Вариант Activator.CreateInstance(typeof(T), true); сработает, но будет пропускать так же и классы с public конструкторами. Остается вариант почти 1 в 1:

        

(T)Activator.CreateInstance(typeof(T), BindingFlags.Instance | BindingFlags.NonPublic, null, null, null);


        Этот способ так же кидает исключение на отсутствие private или protected конструктора. В итоге, компактнее код получился не особо. И все сведется к методам DefaultBinder-а.

        @ Vladimir: Во-первых, если вы так заботитесь о новичках, то уже пишите или Gang of Four или "Банда Четырех". В общем, указывайте точнее, а то получилась как фамилия.  А так уже можно понять (хотя бы поиском), что вы говорите о книге "Design Patterns Elements of Reusable Software".

Во-вторых, они так же не открыли ничего нового. А только систематизировали.

В-третьих, основная цель Singleton – "Ensure a class only has one instance, and provide a global point of access to it." И только в разделе "последствий" упоминается, что можно, используя тот же подход, переделать код так, чтобы Singleton позволял создавать заданное число экземпляров. Разницу между значениями слов "цель" и "последствия" думаю пояснять не надо?

Ну и последнее, на мой практике сами Одиночки редки (которые пишу сам, а не использую объекты из Фреймворка). А с числом экземпляров больше 1 – вообще не было (причем и с готовыми явно не сталкивался). Стоит ли упоминать такой вариант, забивая голову читающему?

@ asbestos: Раз смотрели подскажите, там видео важно или можно просто слушать? А то любопытно (хотя представляю о чем там), а времени смотреть нет. Есть смысл на флешку кидать и в слушать на ходу?

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

@ osmirnov: Тут есть один интересный момент – как я сказал, несмотря на наследование классы практически не связаны. Класс MySingleton – пользовательский, т.е. наш. Поэтому ничего не мешает наследоваться от него "без примеси" и добавлять Singleton уже к результату. Например, вот так:

public class MyClass { }

public sealed class MyClassSingleton : Singleton<MyClass> { }

public class MyNewClass : MyClass { }
public sealed class MyNewClassSingleton : Singleton<MyNewClass> { }


От вашего варианта особо не отличается (по сути что-то вроде надстройки), но лучше варианта с "встроенным" кодом.

@ Vladimir: для Вас, может, серия статей и бестолковая, но лично для меня, человека, который, можно сказать, только начинает понимать всю эту "кухню" в этих статьях много открытий. Пусть лично для Вас, быть может, и примитивных. Поэтому с Вами категорически не согласен о том, что "Подобного рода статьи не имеют смысла". Опять же лично для меня они имеют смысл.

Дмитрий 10.08.2011 21:07:31

Согласен, для новичков статья крайне занимательная.

Денис 08.09.2011 19:33:42

Суть знал, реализовывал всегда как получалось.
А теперь не буду забивать голову, когда есть готовое решение Lazy.

Спасибо.

Виолетта 29.10.2012 7:22:28

Андрей, приведите, пожалуйста, примеры использования Singleton в базовых классах (BCL)/ (Например, Lazy<T> Class ???)

@ Виолетта: С небольшими оговорками - Application из WPF, OperationContext

Добавить комментарий