что такое синглтон в программировании
Singleton (Одиночка) или статический класс?
Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться 🙂
Что такое статический класс?
Что такое Singleton (Одиночка)?
Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи — в сети существует масса подробной информации о нем (например здесь и здесь). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.
А если нет разницы — зачем плодить больше?
Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Singleton | Static class | |
---|---|---|
Количество точек доступа | Одна (и только одна) точка доступа — статическое поле Instance | N (зависит от количества публичных членов класса и методов) |
Наследование классов | Возможно, но не всегда (об этом — ниже) | Невозможно — статические классы не могут быть экземплярными, поскольку нельзя создавать экземпляры объекты статических классов |
Наследование интерфейсов | Возможно, безо всяких ограничений | Невозможно по той же причине, по которой невозможно наследование классов |
Возможность передачи в качестве параметров | Возможно, поскольку Singleton предоставляет реальный объект | Отсутствует |
Контроль времени жизни объекта | Возможно — например, отложенная инициализация (или создание по требованию) | Невозможно по той же причине, по которой невозможно наследование классов |
Использование абстрактной фабрики для создания экземпляра класса | Возможно | Невозможно по причине осутствия самой возможности создания экземпляра |
Сериализация | Возможно | Неприменима по причине отсутствия экземпляра |
Рассмотрим подробнее перечисленные выше критерии.
Количество точек доступа
Конечно же имеются ввиду внешние точки доступа, другими словами — публичный контракт взаимодействия класса и его клиентов. Это удобнее проиллюстрировать с помощью кода:
Singleton в «канонической» реализации:
Наследование классов
С наследованием статических классов все просто — оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено — это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
Наследование интерфейсов
Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное — избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
Возможность передачи в качестве параметров
Для статических классов это не поддерживается — можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection). Синглтон же по сути является обычным экземпляром объекта:
Контроль времени жизни объекта
Время жизни статического класса ограничено временем жизни домена — если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример — отложенная инициализация:
Можно также добавить операцию удаления экземпляра синглтона:
Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования — например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
Использование абстрактной фабрики для создания экземпляра класса
Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
Сериализация
Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.
Так что же использовать Синглтон или Статический класс?
В любом случае выбор решения зависит от разработчика и от специфики решаемой им задачи. Но, в любом случае, можно сделать следующие выводы:
Синглтон (Перевод с английского главы «Singleton» из книги «Pro Objective-C Design Patterns for iOS» Carlo Chung)
Класс синглтона в объектно-ориентированном приложении всегда возвращает один и тот же экземпляр самого себя. Он обеспечивает глобальную точку доступа для ресурсов, которые предоставляет объект класса. Паттерн с такой функциональностью называется Синглтон.
В этой главе мы изучим возможности реализации и использования паттерна Синглтон в Objective-C и фреймворке Cocoa Touch на iOS.
Что из себя представляет паттерн Синглтон?
Паттерн Синглтон – едва ли не самый простой из паттернов. Его назначение в том, чтобы сделать объект класса единственным экземпляром в системе. В первую очередь нужно запретить создавать более одного экземпляра класса. Для этого можно использовать фабричный метод (глава 4), который должен быть статическим, так как не имеет смысла разрешать экземпляру класса создавать другой единственный экземпляр. Рисунок 7-1 показывает структуру класса простого синглтона.
Рисунок 7-1. Статическая структура паттерна Синглтон.
Примечание. Паттерн Синглтон: Проверяет, что есть только один экземпляр класса и обеспечивает единую точку доступа к нему.*
*Исходное определение, представленное в «Design Patterns» GoF (Addison-Wesley,
1994).
Когда можно использовать паттерн Синглтон?
Метод класса предоставляет возможность разделения без создания его объекта. Единственный экземпляр ресурса поддерживается в методе класса. Однако этот подход имеет недостаток гибкости в случае, если класс требует наследования для обеспечения большей функциональности.
Класс Синглтона может гарантировать единую, согласованную и хорошо известную точку доступа для создания и доступа к единственному объекту класса. Паттерн обеспечивает такую гибкость, что любой из его подклассов может переопределить метод создания экземпляра и иметь полный контроль над созданием объекта класса без изменения кода клиента. Еще лучше то, что реализация метода создания экземпляра может обрабатывать создание объекта динамически. Реальный тип класса может определяться во время выполнения, чтобы быть уверенным, что создается корректный объект. Эта техника будет обсуждаться дальше.
Существует также гибкая версия паттерна Синглтон, в которой фабричный метод всегда возвращает один и тот же экземпляр, но можно в дополнение аллоцировать и инициализировать другие экземпляры. Эта менее строгая версия паттерна обсуждается в разделе «Использование класса NSFileManager» далее в этой главе.
Реализация Синглтона в Objective-C
Есть кое-что, над чем стоит подумать, чтобы спроектировать класс Синглтона правильно. Первый вопрос, которым нужно задаться, — это как удостовериться, что только один экземпляр класса может быть создан? Клиенты в приложении, написанном на других объектно-ориентированных языках, таких, как C++ и Java, не могут создать объект класса, если его конструктор объявлен закрытым. А как обстоят дела в Objective-C?
Любой Objective-C метод является открытым, и язык сам по себе динамически типизированный, поэтому любой класс может послать сообщение другому (вызов метода в C++ и Java) без значительных проверок во время компиляции (только предупреждения компилятора, если метод не объявлен). Также фреймворк Cocoa (включая Cocoa Touch) использует управление памятью методом подсчета ссылок для поддержания времени жизни объекта в памяти. Все эти особенности делают реализацию Синглтона в Objective-C довольно непростой.
В оригинальном примере книги «Паттерны проектирования» пример Синглтона на C++ выглядел, как показано в листинге 7-1.
Листинг 7-1. Исходный пример на C++ паттерна Синглтон из книги «Паттерны проектирования».
Как описано в книге, реализация в C++ проста и прямолинейна. В статическом методе Instance() статическая переменная _instance проверяется на 0 ( NULL ). Если так, то создается новый объект класса Singleton и затем возвращается. Кто-то из вас может подумать, что Objective-C версия не сильно отличается от своего собрата и должна выглядеть, как в листингах 7-2 и 7-3.
Листинг 7–2. Объявление класса Singleton в Singleton.h
Листинг 7–3. Реализация метода sharedInstance Singleton.m
Листинг 7–4. Более подходящая реализация Singleton в Objective-C
Наследование от Синглтона
Если использовать трюк с NSAllocateObject для создания экземпляра, то он станет таким:
Теперь неважно, инстанцируем ли мы класс Singleton или какой-то из его подклассов, эта версия сделает все корректно.
Потокобезопасность
Использование Синглтонов во фреймворке Cocoa Touch
Использование класса UIApplication
Использование класса UIAccelerometer
Использование класса NSFileManager
Если нужно реализовать «строгий» синглтон, необходима реализация, похожая на пример, описанный в предыдущих разделах. Иначе – не переопределяйте allocWithZone: и другие связанные методы.
Одиночка
Одиночка — это порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
Одиночка решает сразу две проблемы, нарушая принцип единственной ответственности класса.
Гарантирует наличие единственного экземпляра класса. Чаще всего это полезно для доступа к какому-то общему ресурсу, например, базе данных.
Представьте, что вы создали объект, а через некоторое время пробуете создать ещё один. В этом случае хотелось бы получить старый объект, вместо создания нового.
Такое поведение невозможно реализовать с помощью обычного конструктора, так как конструктор класса всегда возвращает новый объект.
Клиенты могут не подозревать, что работают с одним и тем же объектом.
Предоставляет глобальную точку доступа. Это не просто глобальная переменная, через которую можно достучаться к определённому объекту. Глобальные переменные не защищены от записи, поэтому любой код может подменять их значения без вашего ведома.
Но есть и другой нюанс. Неплохо бы хранить в одном месте и код, который решает проблему №1, а также иметь к нему простой и доступный интерфейс.
Интересно, что в наше время паттерн стал настолько известен, что теперь люди называют «одиночками» даже те классы, которые решают лишь одну из проблем, перечисленных выше.
Все реализации одиночки сводятся к тому, чтобы скрыть конструктор по умолчанию и создать публичный статический метод, который и будет контролировать жизненный цикл объекта-одиночки.
Если у вас есть доступ к классу одиночки, значит, будет доступ и к этому статическому методу. Из какой точки кода вы бы его ни вызвали, он всегда будет отдавать один и тот же объект.
Правительство государства — хороший пример одиночки. В государстве может быть только одно официальное правительство. Вне зависимости от того, кто конкретно заседает в правительстве, оно имеет глобальную точку доступа «Правительство страны N».
Конструктор одиночки должен быть скрыт от клиентов. Вызов метода getInstance должен стать единственным способом получить объект этого класса.
В этом примере роль Одиночки отыгрывает класс подключения к базе данных.
Когда в программе должен быть единственный экземпляр какого-то класса, доступный всем клиентам (например, общий доступ к базе данных из разных частей программы).
Одиночка скрывает от клиентов все способы создания нового объекта, кроме специального метода. Этот метод либо создаёт объект, либо отдаёт существующий объект, если он уже был создан.
Когда вам хочется иметь больше контроля над глобальными переменными.
В отличие от глобальных переменных, Одиночка гарантирует, что никакой другой код не заменит созданный экземпляр класса, поэтому вы всегда уверены в наличии лишь одного объекта-одиночки.
Тем не менее, в любой момент вы можете расширить это ограничение и позволить любое количество объектов-одиночек, поменяв код в одном месте (метод getInstance ).
Добавьте в класс приватное статическое поле, которое будет содержать одиночный объект.
Объявите статический создающий метод, который будет использоваться для получения одиночки.
Добавьте «ленивую инициализацию» (создание объекта при первом вызове метода) в создающий метод одиночки.
Сделайте конструктор класса приватным.
В клиентском коде замените вызовы конструктора одиночка вызовами его создающего метода.
- Гарантирует наличие единственного экземпляра класса. Предоставляет к нему глобальную точку доступа. Реализует отложенную инициализацию объекта-одиночки.
- Нарушает принцип единственной ответственности класса. Маскирует плохой дизайн. Проблемы мультипоточности. Требует постоянного создания Mock-объектов при юнит-тестировании.
Фасад можно сделать Одиночкой, так как обычно нужен только один объект-фасад.
Паттерн Легковес может напоминать Одиночку, если для конкретной задачи у вас получилось свести количество объектов к одному. Но помните, что между паттернами есть два кардинальных отличия:
Не втыкай в транспорте
Лучше почитай нашу книгу о паттернах проектирования.
Теперь это удобно делать даже во время поездок в общественном транспорте.
Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.
Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью
Авторизуйтесь
Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью
Паттерн «Одиночка» — пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.
Определение паттерна
Само описание паттерна достаточно простое — класс должен гарантированно иметь лишь один объект, и к этому объекту должен быть предоставлен глобальный доступ. Скорее всего, причина его популярности как раз и кроется в этой простоте — всего лишь один класс, ничего сложного. Это, наверное, самый простой для изучения и реализации паттерн. Если вы встретите человека, который только что узнал о существовании паттернов проектирования, можете быть уверены, что он уже знает про Singleton. Проблема заключается в том, что когда из инструментов у вас есть только молоток, всё вокруг выглядит как гвозди. Из-за этого «Одиночкой» часто злоупотребляют.
Простейшая реализация
Как уже говорилось выше, в этом нет ничего сложного:
Принцип единственной обязанности
В объектно-ориентированном программировании существует правило хорошего тона — «Принцип едиственной обязанности» (Single Responsibility Principle, первая буква в аббревиатуре SOLID). Согласно этому правилу, каждый класс должен отвечать лишь за один какой-то аспект. Совершенно очевидно, что любой Singleton-класс отвечает сразу за две вещи: за то, что класс имеет лишь один объект, и за реализацию того, для чего этот класс вообще был создан.
Принцип единственной обязанности был создан не просто так — если класс отвечает за несколько действий, то, внося изменения в один аспект поведения класса, можно затронуть и другой, что может сильно усложнить разработку. Так же разработку усложняет тот факт, что переиспользование (reusability) класса практически невозможно. Поэтому хорошим шагом было бы, во-первых, вынести отслеживание того, является ли экземпляр класса единственным, из класса куда-либо во вне, а во-вторых, сделать так, чтобы у класса, в зависимости от контекста, появилась возможность перестать быть Singleton’ом, что позволило бы использовать его в разных ситуациях, в зависимости от необходимости (т.е. с одним экземпляром, с неограниченным количество экземпляров, с ограниченным набором экземпляров и так далее).
Тестирование
Один из главных минусов паттерна «Одиночка» — он сильно затрудняет юнит-тестирование. «Одиночка» привносит в программу глобальное состояние, поэтому вы не можете просто взять и изолировать классы, которые полагаются на Singleton. Поэтому, если вы хотите протестировать какой-то класс, то вы обязаны вместе с ним тестировать и Singleton, но это ещё полбеды. Состояние «Одиночки» может меняться, что порождает следующие проблемы:
На эту тему есть отличный доклад с «Google Tech Talks»:
Скрытые зависимости
Обычно, если классу нужно что-то для работы, это сразу понятно из его методов и конструкторов. Когда очевидно, какие зависимости есть у класса, гораздо проще их предоставить. Более того, в таком случае вы можете использовать вместо реально необходимых зависимостей заглушки для тестирования. Если же класс использует Singleton, это может быть совершенно не очевидно. Всё становится гораздо хуже, если экземпляру класса для работы необходима определённая инициализация (например, вызов метода init(. ) или вроде того). Ещё хуже, если у вас существует несколько Singleton’ов, которые должны быть созданы и инициализированы в определённом порядке.
Загрузчик класса
Если говорить о Java, то обеспечение существования лишь одного экземпляра класса, которое так необходимо для Singleton, становится всё сложнее. Проблема в том, что классическая реализация не проверяет, существует ли один экземпляр на JVM, он лишь удостоверяется, что существует один экземпляр на classloader. Если вы пишете небольшое клиентское приложение, в котором используется лишь один classloader, то никаких проблем не возникнет. Однако если вы используете несколько загрузчиков класса или ваше приложение должно работать на сервере (где может быть запущено несколько экземпляров приложения в разных загрузчиках классов), то всё становится очень печально.
Десериализация
Ещё один интересный момент заключается в том, что на самом деле стандартная реализация Singleton не запрещает создавать новые объекты. Она запрещает создавать новые объекты через конструктор. А ведь существуют и другие способы создать экземпляр класса, и один из них — сериализация и десериализация. Полной защиты от намеренного создания второго экземпляра Singleton’а можно добиться только с помощью использования enum’а с единственным состоянием, но это — неоправданное злоупотребление возможностями языка, ведь очевидно, что enum был придуман не для этого.
Потоконебезопасность
Один из популярных вариантов реализации Singleton содержит ленивую инициализацию. Это значит, что объект класса создаётся не в самом начале, а лишь когда будет получено первое обращение к нему. Добиться этого совсем не сложно:
Однако здесь начинаются проблемы с потоками, которые могут создавать несколько различных объектов. Происходит это примерно так:
Есть два способа решить эту проблему. Первый — пометить как synchronised не весь метод, а только блок, где создаётся объект:
Второй путь — использовать паттерн «Lazy Initialization Holder». Это решение основано на том, что вложенные классы не инициализируются до первого их использования (как раз то, что нам нужно):
Рефлексия
Мы запрещаем создавать несколько экземпляров класса, помечая конструктор приватным. Тем не менее, используя рефлексию, можно без особого труда изменить видимость конструктора с private на public прямо во время исполнения:
Конечно, если вы используете Singleton только в своём приложении, переживать не о чем. А вот если вы разрабатываете модуль, который затем будет использоваться в сторонних приложениях, то из-за этого могут возникнуть проблемы. Какие именно, зависит от того, что делает ваш «Одиночка» — это могут быть как и риски, связанные с безопасностью, так и просто непредсказуемое поведение модуля.
Заключение
Несмотря на то, что паттерн Singleton очень известный и популярный, у него есть множество серьёзных недостатков. Чем дальше, тем больше этих недостатков выявляется, и оригинальные паттерны из книги GOF «Design Patterns» часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.