Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться :)
Для начала вспомним что такое статический класс и для чего он нужен. В любом CLI-совместимом языке используется следующая парадигма инкапсуляции глобальных переменных: глобальных перменных нет. Все члены, в том числе и статические, могут быть объявлены только в рамках какого-либо класса, а сами классы могут (но не должны) быть сгруппированы в каком-либо пространстве имен. И если раньше приходилось иммитировать поведение статического класса с помощью закрытого конструктора, то в .NET Framework 2.0 была добавлена поддержка статических классов на уровне платформы. Основное отличие статического класса от обычного, нестатического, в том, что невозможно создать экземпляр этого класса с помощью оператора new. Статические классы по сути являются некой разновидностью простанства имен — только в отличие от последних предназначены для размещения статических переменных и методов а не типов.
Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи — в сети существует масса подробной информации о нем (например здесь и здесь). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.
Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Рассмотрим подробнее перечисленные выше критерии.
Конечно же имеются ввиду внешние точки доступа, другими словами — публичный контракт взаимодействия класса и его клиентов. Это удобнее проиллюстрировать с помощью кода:
Singleton в «канонической» реализации:
Статический класс:
С наследованием статических классов все просто — оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено — это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное — избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
Для статических классов это не поддерживается — можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection). Синглтон же по сути является обычным экземпляром объекта:
Время жизни статического класса ограничено временем жизни домена — если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример — отложенная инициализация:
Можно также добавить операцию удаления экземпляра синглтона:
Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования — например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.
В любом случае выбор решения зависит от разработчика и от специфики решаемой им задачи. Но, в любом случае, можно сделать следующие выводы:
Использование синглотона оправдано, когда:
Использование статических классов целесообразно тогда, когда у вас нет необходимости реализовывать ни один из сценариев перечисленных для синглтона. Основное назначение статических классов все-таки в группировке логически схожих методов, констант, полей и свойств. Например: System.Math, System.BitConverter, System.Buffer, System.Convert и т.д.
Что такое статический класс?
Для начала вспомним что такое статический класс и для чего он нужен. В любом 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 и т.д.