Pull to refresh

Singleton (Одиночка) или статический класс?

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

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


Для начала вспомним что такое статический класс и для чего он нужен. В любом 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<T> 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[0],
            new ParameterModifier[0]);

        return (T)cInfo.Invoke(null);
    }

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = CreateInstance();
            }

            return _instance;
        }
    }
}

public class Session : Singleton<Session>
{
    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<Session>.Instance;
        }             
    }
}

Наследование интерфейсов

Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное — избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
// Этот класс является синглтоном и реализует интерфейс ISession
public class Session: CoreObject, ISession
{
    private Session()
    {
    }

    public static Session Instance
    {
        get
        {
            return Singleton<Session>.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<T> where T : class
{
    // ...

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
               // Создание "по требованию"
                _instance = CreateInstance();
            }

            return _instance;
        }
    }
}

Можно также добавить операцию удаления экземпляра синглтона:
public class Singleton<T> where T : class
{
    // ...

    public static T Instance
    {
       // ...
    }

    // Очень опасная операция!
    public void RemoveInstance()
    {
        _instance = null;
    }
}

Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования — например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
public class Singleton<T> 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<T>();

    bool IsSupported<T>();
}

public class Singleton<T> 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<T>();
            }

            return _instance;
        }
    }
}

// Вариант с прямым наследованием от синглтона
public class Session : Singleton<Session>
{
    protected Session()
        : base(new ConcreteFactory())
    {
    }

    // ...
}

Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
public class Session : CoreObject, ISession
{
    private class SessionSingleton : Singleton<Session>
    {
        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 и т.д.
Tags:
Hubs:
Total votes 70: ↑58 and ↓12 +46
Views 173K
Comments Comments 84