
Singleton を Swift で
Singleton は、 生成に関するデザインパターンの一つで、 この種類のオブジェクトがただ一つだけ存在することを保証し、 他のコードに対して唯一のアクセス・ポイントを提供します。
Singleton には、 大域変数とほぼ同じ長所と短所があります。 両方とも随分と便利ですが、 コードのモジュール性を犠牲にしています。
シングルトンのクラスに依存しているあるクラスを使う場合、 シングルトンのクラスも一緒に使う必要があります。 ほとんどの場合、 この制限は、 ユニット・テストの作成で問題となります。
複雑度:
人気度:
使用例: 多くの開発者は、 Singleton をアンチ・パターンと見なしています。 Swift コードでの使用が減少したのはこのためです。
見つけ方: Singleton は、 キャッシュされた同一オブジェクトを返す静的生成メソッドで識別できます。
以下の例は
Swift Playgroundsで利用できます。
Playgroundバージョンを作成してくれた
Alejandro Mohamadに感謝します。
概念的な例
この例は、 Singleton デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
- どういうクラスからできているか?
- それぞれのクラスの役割は?
- パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の Swift でのユースケースが理解しやすくなります。
Example.swift: 概念的な例
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: 実行結果
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]) {
/// 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: 実行結果
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'