что такое объект синглтон
Singleton (Одиночка) или статический класс?
Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться 🙂
Что такое статический класс?
Что такое Singleton (Одиночка)?
Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи — в сети существует масса подробной информации о нем (например здесь и здесь). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.
А если нет разницы — зачем плодить больше?
Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Singleton | Static class | |
---|---|---|
Количество точек доступа | Одна (и только одна) точка доступа — статическое поле Instance | N (зависит от количества публичных членов класса и методов) |
Наследование классов | Возможно, но не всегда (об этом — ниже) | Невозможно — статические классы не могут быть экземплярными, поскольку нельзя создавать экземпляры объекты статических классов |
Наследование интерфейсов | Возможно, безо всяких ограничений | Невозможно по той же причине, по которой невозможно наследование классов |
Возможность передачи в качестве параметров | Возможно, поскольку Singleton предоставляет реальный объект | Отсутствует |
Контроль времени жизни объекта | Возможно — например, отложенная инициализация (или создание по требованию) | Невозможно по той же причине, по которой невозможно наследование классов |
Использование абстрактной фабрики для создания экземпляра класса | Возможно | Невозможно по причине осутствия самой возможности создания экземпляра |
Сериализация | Возможно | Неприменима по причине отсутствия экземпляра |
Рассмотрим подробнее перечисленные выше критерии.
Количество точек доступа
Конечно же имеются ввиду внешние точки доступа, другими словами — публичный контракт взаимодействия класса и его клиентов. Это удобнее проиллюстрировать с помощью кода:
Singleton в «канонической» реализации:
Наследование классов
С наследованием статических классов все просто — оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено — это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
Наследование интерфейсов
Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное — избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
Возможность передачи в качестве параметров
Для статических классов это не поддерживается — можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection). Синглтон же по сути является обычным экземпляром объекта:
Контроль времени жизни объекта
Время жизни статического класса ограничено временем жизни домена — если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример — отложенная инициализация:
Можно также добавить операцию удаления экземпляра синглтона:
Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования — например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
Использование абстрактной фабрики для создания экземпляра класса
Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
Сериализация
Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.
Так что же использовать Синглтон или Статический класс?
В любом случае выбор решения зависит от разработчика и от специфики решаемой им задачи. Но, в любом случае, можно сделать следующие выводы:
Использование паттерна синглтон
Введение
Реализация
Первое, что хотелось бы отметить: синглтон — это реализация, а не интерфейс. Что это значит? Это значит, что класс по возможности должен использовать некий интерфейс, а то, будет там синглтон или нет, это он не знает и знать не должен, т.к. всякое явное использование синглтона и будет приводить к указанным проблемам. На словах выглядит хорошо, давайте посмотрим, как это должно выглядеть в жизни.
Для реализации данной идеи мы воспользуемся мощным подходом, который называется Dependency Injection. Суть его состоит в том, что мы неким образом заливаем реализацию в класс, при этом класс, использующий интерфейс, не заботится о том, кто и когда это будет делать. Его эти вопросы вообще не интересуют. Все, что он должен знать, это как правильно использовать предоставленный функционал. Интерфейс функционала при этом может быть как абстрактный интерфейс, так и конкретный класс. В нашем конкретном случае это неважно.
Идея есть, давайте реализуем на языке C++. Тут нам помогут шаблоны и возможность их специализации. Для начала определим класс, который будет содержать указатель на необходимый экземпляр:
Описанный класс решает несколько задач. Во-первых, он хранит указатель на необходимый экземпляр класса. Во-вторых, при отсутствии экземпляра вызывается функция anFill, которая заполняет нужным экземпляром в случае отсутствия такового (метод reinit). При обращении к классу происходит автоматическая инициализация экземпляром и его вызов. Посмотрим на реализацию функции anFill:
Таким образом по умолчанию данная функция кидает исключение с целью предотвращения использования незадекларированной функции.
Примеры использования
Теперь предположим, что у нас есть класс:
Мы хотим сделать его синглтоном для использования в различных контекстах. Для этого специализируем функцию anFill для нашего класса X:
В данном случае мы использовали простейший синглтон и для наших рассуждений конкретная реализация не имеет значения. Стоит отметить, что данная реализация не является потокобезопасной (вопросы многопоточности будут рассмотрены в другой статье). Теперь мы можем использовать класс X следующим образом:
Что выведет на экран:
При повторном вызове action мы увидим:
Что говорит о том, что у нас сохраняется состояние и экземпляр класса X ровно один. Теперь усложним немного пример. Для этого создадим новый класс Y, который будет содержать использование класса X:
Теперь если мы хотим использовать экземпляр по умолчанию, то нам просто можно сделать следующее:
Что после предыдущих вызовов выведет на экран:
Теперь предположим, что мы захотели использовать другой экземпляр класса. Это сделать очень легко:
Т.е. мы заполняем класс Y нашим (известным) экземпляром и вызываем соответствующую функцию. На экране мы получим:
Разберем теперь случай с абстракными интерфейсами. Создадим абстрактный базовый класс:
Определим 2 различные реализации этого интерфейса:
По умолчанию будем заполнять, используя первую реализацию Impl1:
Таким образом, следующий код:
Создадим класс, использующий наш интерфейс:
Теперь мы хотим поменять реализацию. Тогда делаем следующее:
Что дает в результате:
Развитие идеи
В целом на этом можно было бы закончить. Однако стоит добавить немножко полезных макросов для облегчения жизни:
Многие могут сказать, что макросы — это зло. Ответственно заявляю, что с данным фактом я знаком. Тем не менее, это часть языка и ее можно использовать, к тому же я не подвержен догмам и предрассудкам.
Макрос DECLARE_IMPL декларирует заполнение, отличное от заполнения по умолчанию. Фактически эта строчка говорит о том, что для этого класса будет происходить автоматическое заполнение неким значением в случае отсутствия явной инициализации. Макрос BIND_TO_IMPL_SINGLE будет использоваться в CPP файле для реализации. Он использует функцию single, которая возвращает экземпляр синглтона:
Использование макроса BIND_TO_SELF_SINGLE говорит о том, что для класса будет использоваться экземпляр его самого. Очевидно, что в случае абстракного класса этот макрос неприменим и необходимо использовать BIND_TO_IMPL_SINGLE с заданием реализации класса. Данная реализация может быть скрыта и объявлена только в CPP файле.
Теперь рассмотрим использование уже на конкретном примере, например конфигурации:
Далее можно использовать в других классах:
Выводы
Таким образом описанный подход избавляет от проблем, указанных в начале этой статьи. В последующих статьях я хотел бы затронуть важные вопросы, связанные с временем жизни и многопоточностью.
Синглтон (Перевод с английского главы «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: и другие связанные методы.
Синглтон и время жизни объекта
Эта статья является продолжением моей первой статьи “Использование паттерна синглтон” [0]. Сначала я хотел все, что связано со временем жизни, изложить в этой статье, но объем материала оказался велик, поэтому решил разбить ее на несколько частей. Это — продолжение целого цикла статей про использование различных шаблонов и методик. Данная статья посвящена времени жизни и развитию использования синглтона. Перед прочтением второй статьи настоятельно рекомендуется ознакомиться с моей первой статьей [0].
В предыдущей статье была использована следующая реализация для синглтона:
На примере синглтона можно с легкостью показать, как делаются такие ошибки. Открываем статью в Википедии [1] и находим реализацию для C++:
Умные указатели
К счастью, в C++ есть замечательное средство, которое называется «умный указатель». Их умность заключается в том, что, хотя они и ведут себя как обычные указатели, при этом контролируют время жизни объектов. Для этого они используют счетчик, который самостоятельно подсчитывает количество ссылок на объект. При достижении счетчиком нуля объект автоматически уничтожается. Будем использовать умный указатель из стандартной библиотеки std::shared_ptr заголовочного файла memory. Стоит отметить, что такой класс доступен для современных компиляторов, которые поддерживают стандарт C++0x. Для тех, кто использует старый компилятор, можно использовать boost::shared_ptr. Интерфейсы у них абсолютно идентичны.
Этим условиям удовлетворяет следующая реализация:
При этом синглтон перепишется в следующем виде:
Вспомогательные макросы будут такими:
Небольшим изменениям подвергся макрос BIND_TO_IMPL_SINGLE, который теперь использует вместо функции single функцию anSingle, которая, в свою очередь, возвращает уже заполненный экземпляр An. О других макросах я расскажу позже.
Использование синглтона
Теперь рассмотрим использование описанного класса для реализации синглтона:
Теперь это можно использовать следующим образом:
Что на экране даст цифру 2, т.к. для реализации использовался класс Y.
Контроль времени жизни
Рассмотрим теперь пример, который показывает важность использования умных указателей для синглтонов. Для этого разберем следующий код:
Теперь посмотрим, что выведется на экран при таком вызове функции out:
Давайте теперь посмотрим, как можно сделать то же самое, но с контролем времени жизни объектов. Для этого будем использовать наш класс An:
Посмотрим, что теперь выведется на экран:
Итого
На этом первая часть статьи, посвященная времени жизни объектов, подошла к концу. В следующей части (или частях) будут разобраны остальные порождающие шаблоны проектирования с использованием разработанного функционала и сделаны общие выводы.
Многие спрашивают, а в чем, собственно, смысл? Почему нельзя просто сделать синглтон? Зачем использовать какие-то дополнительные конструкции, которые ясности не добавляют, а лишь усложняют код. В принципе, при внимательном прочтении первой статьи [0] уже можно понять, что данный подход более гибок и устраняет ряд существенных недостатков синглтона. В следующей статье будет отчетливо понятно, зачем я это так расписывал, т.к. в ней речь уже будет идти не только о синглтоне. А через статью будет вообще понятно, что синглтон тут абсолютно не при чем. Все, что я пытаюсь показать — это использование Dependency inversion principle [4] (см. также The Principles of OOD [5]). Собственно, именно после того, как я увидел впервые этот подход на Java, мне стало обидно, что в C++ это слабо используют (в принципе, есть фреймворки, которые предоставляют подобный функционал, но хотелось бы чего-то более легковесного и практичного). Приведенная реализация — это лишь маленький шажок в этом направлении, который уже приносит огромную пользу.
Update
Почитав комментарии я понял, что есть некоторые моменты, которые стоит прояснить, т.к. многие не знакомы с принципом обращения зависимостей (dependency inversion principle, DIP или inversion of control, IoC). Рассмотрим следующий пример: у нас есть база данных, в которой содержится необходимая нам информация, например список пользователей:
У нас есть класс, который выдает нужную нам информацию, в том числе и необходимого пользователя:
Здесь мы создаем член aDatabase, который говорит о том, что ему необходима некая база данных. Ему не важно знать, что это будет за база данных, ему не нужно знать, кто и когда это будет заполнять/заливать. Но класс UserManager знает, что ему туда зальют то, что нужно. Все, что он говорит, это: «дайте мне нужную реализацию, я не знаю какую, и я сделаю все, что вам нужно от этой базы данных, например, предоставлю необходимую информацию о пользователе из этой базы данных».
Теперь мы делаем хитрый трюк. Так как у нас есть одна лишь база данных, которая содержит всю нашу информацию, то мы говорим: ок, раз есть только одна база данных, давайте сделаем синглтон, и чтобы не париться каждый раз в заливке реализации, сделаем так, чтобы синглтон сам заливался:
Т.е. мы создаем реализацию MyDatabase и говорим, что для синглтона будем использовать именно ее, используя макрос BIND_TO_IMPL_SINGLE. Тогда следующий код автоматически будет использовать MyDatabase:
С течением времени оказалось, что у нас есть еще одна база данных, в которой тоже есть пользователи, но, скажем, для другой организации:
Мы конечно же хотим использовать наш UserManager, но уже с другой базой данных. Нет проблем:
И как по волшебству, теперь мы берем пользователя из другой базы данных! Это достаточно грубый пример, но он отчетливо показывает принцип обращения зависимостей: это когда объекту UserManager заливают реализацию IDatabase вместо традиционного подхода, когда UserManager сам ищет себе необходимую реализацию. В рассмотренной статье используется этот принцип, при этом синглтон для реализации берется как частный случай.
Паттерны проектирования, взгляд iOS разработчика. Часть 0. Синглтон-Одиночка
Я почув і забув.
Я записав і запам’ятав.
Я зробив і зрозумів.
Я навчив іншого, тепер я майстер.
(В. В. Бублик)
Небольшое вступление.
Я не зря вынес в начало поста цитату на украинском языке. Дело в том, что именно эти слова я услышал от своего преподавателя программирования на втором курсе университета, и именно в таком виде я вспоминаю эти слова до сих пор. Как вы можете догадаться, эта цитата является отсылкой к высказыванию Конфуция, но в ней есть очень важное дополнение о достижении мастерства.
И именно эти слова и сподвигли меня на написание данной серии постов. Дело в том, что я — начинающий iOS разработчик, и я очень хочу разобраться в паттернах проектирования. И я не придумал лучшего способа, чем взять книгу «Паттерны проектирования» Эрика и Элизабет Фримен, и написать примеры каждого паттерна на Objective-C и Swift. Таким образом я смогу лучше понять суть каждого паттерна, а также особенности обоих языков.
Содержание:
Итак, начнем с самого простого на мой взгляд паттерна.
Одиночка, он же — синглтон.
Итак, разберемся что такое синглтон в Objective-C и Swift на примерах из книги.
Давайте сначала узнаем как вообще создать объект какого-нибудь класса. Очень просто:
И тут авторы подводят нас к мысли, что приведенным выше способом можно создать сколько угодно объектов этого класса. Таким образом, первое что нужно сделать на пути к синглтону — запретить создание объектов нашего класса извне. В этом нам поможет приватный инициализатор.
И если в swift это реализуется тривиально:
Таким образом попытка создать объект нашего класса извне вызовет ошибку на этапе компиляции. Окей. Теперь и в objective-c у нас есть запрет на создание объектов нашего класса. Правда это еще не совсем приватный инициализатор, но мы к этому вернемся через пару секунд.
Итак, по сути мы получили класс, объекты которого не могут создаваться, потому что конструктор — приватный. И что со всем этим делать? Будем создавать объект нашего класса внутри нашего же класса. И будем использовать для этого статический метод (метод класса, а не объекта):
И если для swift опять все просто и понятно, то с objective-c возникает проблема с инициализацией:
(да, и не забудьте вынести метод + (instancetype)sharedInstance в файл заголовка, он должен быть публичным)
Теперь все компилируется и мы можем получать объекты нашего класса таким способом:
Наш синглтон почти готов. Осталось только исправить статический метод так, чтобы объект создавался только один раз:
Как видите, для этого нам понадобилась статическая переменная, в которой и будет храниться единожды созданный объект нашего класса. Каждый раз при вызове нашего статического метода она проверяется на nil и, если объект уже создан и записан в эту переменную — он не создается заново. Наш синглтон готов, ура! 🙂
Теперь немного примеров из жизни из книги.
Итак, у нас есть шоколадная фабрика и для приготовления мы используем высокотехнологичный нагреватель шоколада с молоком (я просто обожаю молочный шоколад), который будет управляться нашим программным кодом:
Как видите — нагреватель сначала заполняется смесью ( fill ), затем доводит ее до кипения ( boil ), и после — передает ее на изготовление молочных шоколадок ( drain ). Для избежания проблем нам нужно быть уверенными, что в нашей программе присутствует только один экземпляр нашего класса, который управляет нашим нагревателем, поэтому внесем изменения в программный код:
Итак, все отлично. Мы на 100% уверены (точно на 100%?), что у нас есть только один объект нашего класса и никаких непредвиденных ситуаций на фабрике не произойдет. И если наш код на objective-c выглядит довольно неплохо, то swift выглядит недостаточно swifty. Попробуем его немного переписать:
А как же многопоточность?
Все будет работать хорошо ровно до того момента, как мы захотим применить в нашей программе работу с потоками. Как же сделать наш синглтон потокобезопасным?
И опять же: в swift, как оказывается, совершенно не нужно выполнять каких-либо дополнительных действий. Константа уже потокобезопасна, ведь значение в нее может быть записано только один раз и это сделает тот поток, который доберется до нее первым.
А вот в objective-c необходимо внести коррективы в наш статический метод:
Блок внутри dispatch_once гарантированно выполнится только один раз, когда самый первый поток до него доберется, все остальные потоки — будут ждать, когда закончится выполнение блока.
Итоги подведем.
Итак, мы разобрались как правильно писать синглтоны на objective-c и swift. Приведу вам итоговый код класса Singleton на обоих языках:
Хочу попросить всех читателей и комментаторов: если вы увидели какую-нибудь неточность или знаете как какой-либо из приведенных мною кусков кода написать правильнее/красивее/корректнее — скажите об этом. Я пишу здесь совсем не для того, чтоб учить других, а для того, чтоб научиться самому. Ведь пока мы учимся — мы остаемся молодыми.