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