воскресенье, 17 апреля 2016 г.

Singleton в C#

В этом посте мы рассмотрим следующие вещи :

  • Что такое паттерн проектирования Singleton (одиночка) ?
  • Как запрограммировать "одиночку" в C# ?
  • Эффекты многопоточности на паттерне Singleton
  • Реализация двойной блокировки при программировании "одиночки"
  • Раннее создание экземпляра "одиночки"
  • Полноценное "ленивое" создание "одиночки"
  • Программирование "одиночки" с применением обобщений С#
  • Создание одиночки через Lazy<T>
  • Пути взлома "одиночки"
  • Использование "одиночки"
  • Singleton как антипаттерн
  • Различия между статическим классом и паттерном Singleton
  • Заключение

Паттерн проектирования Singleton:

Шаблон (паттерн) проектирования Singleton гарантирует единственность экземпляра класса и предоставляет глобальный доступ к его свойставм и методам.

Стандартная реализация шаблона Singleton

Чтобы создать ровно один экземпляр "одиночки", мы делаем конструктор приватным. Теперь мы можем инстанцировать класс только внутри самого класса.
Мы создаем статическое поле, которое будет содержать созданный экземпляр класса.
Затем мы создаем статический метод, который представляет экземпляр класса для внешнего доступа. В методе проверяется существует ли экземпляр класса, и если нет, то создает и возвращает его, а в противном случае просто возвращает созданный ранее объект.

 private static Singleton _instance;
 
 private Singleton() {}
 
 public static Singleton GetInstance()
 {

  if(_instance == null)
                {
   _instance = new Singleton();
  }
  return _instance;

 }

Проблемы со стандартной реализацией

Стандартная реализация прекрасно работает в однопотоковой среде. В многопотоковой среде метод GetInstance() даст сбой :

public static Singleton GetInstance()
{

   if(_instance == null)
   {
      _instance = new Singleton();

   }
  return _instance;
}
Если два потока вызовут условие в операторе "if" в один и тот же момент времени, то будет создано два экземпляра класса Singleton.

Решение проблемы многопоточности в стандартной реализации одиночки

Мы можем синхронизировать код метода таким образом, что только одна нить будет владеть кодом в один момент времени. В коде ниже нить блокирует объект и проверяет, не был ли уже создан ранее экземпляр объекта.

  public sealed class Singleton
    {
        private static Singleton _instance = null;
        private static readonly object _lock = new object();
 
        private Singleton() {}
 
        public static Singleton GetInstance
        {
            get
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                    return _instance;
                }
            }
        }
}
Такой код решает проблему. Но это замедляет работу, потому что все прочие потоки ожидают завершения блокировки. Для этого мы используем следующие подходы :

1 : Двойная проверка при блокировке

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

 public sealed class Singleton
    {
        private static Singleton _instance = null;
        private static readonly object _lock = new object();
 
        private Singleton() {}
 
        public static Singleton GetInstance
        {
            get
            {
                if (_instance == null)
                {
                    lock (_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new Singleton();
                        }
 
                    }
                }
                return _instance;
            }
        }
   }
Примечание. Подобный вопрос попался мне на экзамене по С# на сертификат Microsoft.

2 : Заранее созданный экземпляр

Здесь мы создаем экземпляр "одиночки" во время загрузки класса. Потокобезопасность осуществляется в этом случае без блокирования кода оператором lock.

public class Singleton 
{
 //создание экземпляра немедленно
 private static Singleton _instance = new Singleton();
 
 private Singleton() {}
 
 public static Singleton Default
 {
     get { return _instance; } //просто возвращаем экземпляр
 }

}

3 : Полноценное отложенное создание экземпляра

В этом примере инстанцирование происходит при первом обращении к статическому свойству Default класса Singleton.

 public sealed class Singleton
  {
    private Singleton()
    {
    }
 
    public static Singleton Default { get { return GetInstance._instance; } }
        
    private class GetInstance
    {
        // Явный статический конструктор сообщит компилятору
        // не инициализировать поле заранее
        // и поле будет инициализировано при первом к нему обращении 
        static GetInstance()
        {
        }
 
        internal static readonly Singleton _instance = new Singleton();
    }
 }
 

4 : Реализация паттерна Singleton с применением обобщений

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

 public class Singleton<T> where T : new()
    {
        private static T _instance = new T();
 
        public T Default()
        {
            get { return _instance; }
        }
    }
 
...
    Singleton<SimpleType> instance = new Singleton<SimpleType>();
    SimpleType simple = instance.Default;

5 : Применение Lazy<T> type

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

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _lazy =
        new Lazy<Singleton>(() => new Singleton());
    
    public static Singleton Default{ get { return _lazy.Value; } }
 
    private Singleton()
    {
    }
}

Обходные пути для взлома Singleton - вопросы, которые задают на собеседовании :

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

– клонирование
– рефлексия
– использование класса singleton в качестве подкласса

Как избежать создания экземпляра Singleton через клонирование объекта ?

Через метод clone() можно создать копию объекта.

Чтобы предотвратить создание клонов экземпляра "одиночки", необходимо сделать следующее :

– Реализовать метод MethodwiseClone()
– Переопределить метод clone() и возбудить оттуда исключение CloneNotSupportedException.
 protected object MemberwiseClone()
    {
        throw new Exception("Cloning a singleton object is not allowed");
    }

 

Как предотвратить создание экземпляра Singleton через рефлексию ?

Чтобы предотвратить создание экземпляра через рефлексию, надо возбудить исключение из конструктора, если экземпляр уже имеется.

 private Singleton()
    {
        if (_instance != null)
        {
            throw new Exception("Cannot create singleton instance through reflection");
        }
    }

 

Как предотвратить создания экземпляра "одиночки" в качестве подкласса ?

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

Для чего используется паттерн проектиорвания Singleton :

Логирование :

Шаблон Singleton хорошо подходит для реализации протоколирование, если мы планируем единственный файл лога для всех сообщений.

Кеширование:

Шаблон Singleton может быть использован в реализации механизма кеширования для предоставления глобального доступа к кешу.

Почему "одиночка" считается антипаттерном?

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

Отличия "одиночки" от статического класса :

– В C# в статическом классе нельзя реализовать интерфейс. В случае, если "одиночка" должен реализовать интерфейс, например для использования в IoC, вы можете не использовать статический класс
– Singleton можно клонировать, объект статического класса клонировать нельзя
– Объект Singleton хранится в куче, статический - в стеке
– Можно реализовать отложенную или асинхронную инициализацию "одиночки", а статический класс инициализируется при первой загрузке.

Заключение:

Итак, мы обусдили:
  • Различные способы программирования синглтона в C#
  • Использование паттерна синглтона
  • Почему синглтон является антипаттерном?
  • Различия между статическим классовм и синглтоном

0 коммент.:

Отправить комментарий