Singleton es un patrón de diseño creacional que garantiza que tan solo exista un objeto de su tipo y proporciona un único punto de acceso a él para cualquier otro código.
El patrón tiene prácticamente los mismos pros y contras que las variables globales. Aunque son muy útiles, rompen la modularidad de tu código.
No se puede utilizar una clase que dependa del Singleton en otro contexto. Tendrás que llevar también la clase Singleton. La mayoría de las veces, esta limitación aparece durante la creación de pruebas de unidad.
Ejemplo conceptual
Este ejemplo ilustra la estructura del patrón de diseño Singleton y se centra en las siguientes preguntas:
¿De qué clases se compone?
¿Qué papeles juegan esas clases?
¿De qué forma se relacionan los elementos del patrón?
Después de conocer la estructura del patrón, será más fácil comprender el siguiente ejemplo basado en un caso de uso real de Swift.
Example.swift: Ejemplo conceptual
import XCTest
/// The Singleton class defines the `shared` field that lets clients access the
/// unique singleton instance.
class Singleton {
/// The static field that controls the access to the singleton instance.
///
/// This implementation let you extend the Singleton class while keeping
/// just one instance of each subclass around.
static var shared: Singleton = {
let instance = Singleton()
// ... configure the instance
// ...
return instance
}()
/// The Singleton's initializer should always be private to prevent direct
/// construction calls with the `new` operator.
private init() {}
/// Finally, any singleton should define some business logic, which can be
/// executed on its instance.
func someBusinessLogic() -> String {
// ...
return "Result of the 'someBusinessLogic' call"
}
}
/// Singletons should not be cloneable.
extension Singleton: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return self
}
}
/// The client code.
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.")
}
}
// ...
}
/// Let's see how it all works together.
class SingletonConceptual: XCTestCase {
func testSingletonConceptual() {
Client.someClientCode()
}
}
Output.txt: Resultado de la ejecución
Singleton works, both variables contain the same instance.
Ejemplo del mundo real
Example.swift: Ejemplo del mundo real
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]) {
/// handle new messages in the base class
}
func accept(removed messages: [Message]) {
/// handle 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'")
/// handle new messages in the child class
}
override func accept(removed messages: [Message]) {
print("MessagesListVC accepted 'removed messages'")
/// handle 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'")
/// handle new messages in the child class
}
override func accept(removed messages: [Message]) {
print("ChatVC accepted 'removed messages'")
/// handle 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: Resultado de la ejecución
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'