Одиночка на Swift
Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Сложность:
Популярность:
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в Swift-коде.
Признаки применения паттерна: Одиночку можно определить по статическому создающему методу, который возвращает один и тот же объект.
Концептуальный пример
Этот пример показывает структуру паттерна Одиночка, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest /// Класс Одиночка предоставляет поле `shared`, которое позволяет клиентам /// получать доступ к уникальному экземпляру одиночки. class Singleton { /// Статическое поле, управляющие доступом к экземпляру одиночки. /// /// Эта реализация позволяет вам расширять класс Одиночки, сохраняя повсюду /// только один экземпляр каждого подкласса. static var shared: Singleton = { let instance = Singleton() // ... настройка объекта // ... return instance }() /// Инициализатор Одиночки всегда должен быть скрытым, чтобы предотвратить /// прямое создание объекта через инициализатор. private init() {} /// Наконец, любой одиночка должен содержать некоторую бизнес-логику, /// которая может быть выполнена на его экземпляре. func someBusinessLogic() -> String { // ... return "Result of the 'someBusinessLogic' call" } } /// Одиночки не должны быть клонируемыми. extension Singleton: NSCopying { func copy(with zone: NSZone? = nil) -> Any { return self } } /// Клиентский код. class Client { // ... static func someClientCode() { let instance1 = Singleton.shared let instance2 = Singleton.shared if (instance1 === instance2) { print("Singleton works, both variables contain the same instance.") } else { print("Singleton failed, variables contain different instances.") } } // ... } /// Давайте посмотрим как всё это будет работать. class SingletonConceptual: XCTestCase { func testSingletonConceptual() { Client.someClientCode() } } Output.txt: Результат выполнения
Singleton works, both variables contain the same instance. Пример из реальной жизни
Example.swift: Пример из реальной жизни
import XCTest /// Singleton Design Pattern /// /// Intent: Ensure that class has a single instance, and provide a global point /// of access to it. class SingletonRealWorld: XCTestCase { func testSingletonRealWorld() { /// There are two view controllers. /// /// MessagesListVC displays a list of last messages from a user's chats. /// ChatVC displays a chat with a friend. /// /// FriendsChatService fetches messages from a server and provides all /// subscribers (view controllers in our example) with new and removed /// messages. /// /// FriendsChatService is used by both view controllers. It can be /// implemented as an instance of a class as well as a global variable. /// /// In this example, it is important to have only one instance that /// performs resource-intensive work. let listVC = MessagesListVC() let chatVC = ChatVC() listVC.startReceiveMessages() chatVC.startReceiveMessages() /// ... add view controllers to the navigation stack ... } } class BaseVC: UIViewController, MessageSubscriber { func accept(new messages: [Message]) { /// Handles new messages in the base class } func accept(removed messages: [Message]) { /// Hanldes removed messages in the base class } func startReceiveMessages() { /// The singleton can be injected as a dependency. However, from an /// informational perspective, this example calls FriendsChatService /// directly to illustrate the intent of the pattern, which is: "...to /// provide the global point of access to the instance..." FriendsChatService.shared.add(subscriber: self) } } class MessagesListVC: BaseVC { override func accept(new messages: [Message]) { print("MessagesListVC accepted 'new messages'") /// Handles new messages in the child class } override func accept(removed messages: [Message]) { print("MessagesListVC accepted 'removed messages'") /// Handles removed messages in the child class } override func startReceiveMessages() { print("MessagesListVC starts receive messages") super.startReceiveMessages() } } class ChatVC: BaseVC { override func accept(new messages: [Message]) { print("ChatVC accepted 'new messages'") /// Handles new messages in the child class } override func accept(removed messages: [Message]) { print("ChatVC accepted 'removed messages'") /// Handles removed messages in the child class } override func startReceiveMessages() { print("ChatVC starts receive messages") super.startReceiveMessages() } } /// Protocol for call-back events protocol MessageSubscriber { func accept(new messages: [Message]) func accept(removed messages: [Message]) } /// Protocol for communication with a message service protocol MessageService { func add(subscriber: MessageSubscriber) } /// Message domain model struct Message { let id: Int let text: String } class FriendsChatService: MessageService { static let shared = FriendsChatService() private var subscribers = [MessageSubscriber]() func add(subscriber: MessageSubscriber) { /// In this example, fetching starts again by adding a new subscriber subscribers.append(subscriber) /// Please note, the first subscriber will receive messages again when /// the second subscriber is added startFetching() } func startFetching() { /// Set up the network stack, establish a connection... /// ...and retrieve data from a server let newMessages = [Message(id: 0, text: "Text0"), Message(id: 5, text: "Text5"), Message(id: 10, text: "Text10")] let removedMessages = [Message(id: 1, text: "Text0")] /// Send updated data to subscribers receivedNew(messages: newMessages) receivedRemoved(messages: removedMessages) } } private extension FriendsChatService { func receivedNew(messages: [Message]) { subscribers.forEach { item in item.accept(new: messages) } } func receivedRemoved(messages: [Message]) { subscribers.forEach { item in item.accept(removed: messages) } } } Output.txt: Результат выполнения
MessagesListVC starts receive messages MessagesListVC accepted 'new messages' MessagesListVC accepted 'removed messages' ======== At this point, the second subscriber is added ====== ChatVC starts receive messages MessagesListVC accepted 'new messages' ChatVC accepted 'new messages' MessagesListVC accepted 'removed messages' ChatVC accepted 'removed messages'