【Swiftで学ぶ】シングルトンパターン

この記事では、オブジェクト指向プログラミングにおけるシングルトンパターン(以下、シングルトン)について具体例をSwiftを用いて解説します。シングルトンの理解と適切な利用方法を学ぶことができます。

# シングルトンとは？
一言でいうと、**オブジェクト指向プログラミングにおけるデザインパターンの1つ**です。
特定のクラスのインスタンスが1つしか生成されていないことを保証しています。

# 対峙する問題
シングルトンは以下の問題を解決します。
## クラスのインスタンスが1つだけであることを保証する
データベースやファイルなどの共有資源へのアクセスを制御します。
例えば、ログインしているユーザーの情報やトークンなど、アプリ全体で使われるデータを制御するのに使います。

## インスタンスへの大域アクセス・ポイントを提供する
グローバル変数はとても便利ですが、どこからでも変数の内容を変更でき、任意のタイミングで何の値を保持しているか分からなくなる場合があります。
クラスも同様で、どこからでもインスタンスを生成することができます。
シングルトンはインスタンスが他のコードによって変更されるのを防ぎます。


# 方法
実装には共通した2つのステップがあります。
- 他のオブジェクトがシングルトンのクラスを使用しないように、デフォルトのコンストラクターを `private` にします
- コンストラクターとして機能する静的作成メソッドを作成します。非公開のコンストラクターを呼び出してオブジェクトを作成し、静的なフィールドに保存します

## 例1
例えば、以下のようなコードがあります。：

```swift:ログが共有されない例
import Foundation

class Logger {
    var logs: [String] = []
    
    func log(message: String) {
        logs.append(message)
    }
    
    func printLogs() {
        print(logs)
    }
}

let logger1 = Logger()
logger1.log(message: "First log")
logger1.printLogs() // ["First log"]

let logger2 = Logger()
logger2.log(message: "Second log")
logger2.printLogs() // ["Second log"]

// logger1とlogger2は別々のインスタンスなので、logsが共有されず、値がリセットされている。
```
https://swiftfiddle.com/b5q2c4jlj5es5iiz5vdlrrac2q

上記の例では、Loggerクラスのインスタンスが複数作成されています。ログの情報が各インスタンスで独立しています。
そのため、Logger1とLogger2のログは共有されません。

では、この問題をシングルトンで解決しましょう。

```swift:ログが共有される(シングルトン)の例
import Foundation

class Logger {
    static let shared = Logger()
    
    var logs: [String] = []
    
    private init() {} // 外部からのインスタンス生成を防ぐ
    
    func log(message: String) {
        logs.append(message)
    }
    
    func printLogs() {
        print(logs)
    }
}

Logger.shared.log(message: "First log")
Logger.shared.printLogs() // ["First log"]

Logger.shared.log(message: "Second log")
Logger.shared.printLogs() // ["First log", "Second log"]

// sharedインスタンスを使用しているので、ログが共有される。
```
https://swiftfiddle.com/6otbd5lcdvfszlml7jorvemmqe

上記の例では、Loggerクラスの唯一のインスタンスが `shared` プロパティとして定義されます。

## 例2
もう1つ見ていきましょう。
```swift
import Foundation

class Logger {
    var logs: [String] = []
    
    func log(message: String) {
        logs.append(message)
    }
    
    func printLogs() {
        print(logs)
    }
}

final class Sub {
    private let logger = Logger()
    func functionSubA() {
        logger.log(message: "Log from functionSubA")
    }
}

final class Main {
    private let logger = Logger()
    private let sub = Sub()
    func functionA() {
        Sub().functionSubA()
        logger.log(message: "Log from functionA")
    }
    
    func functionB() {
        logger.log(message: "Log from functionB")
    }
    
    func output() {
        // どこからでも同じLoggerインスタンスにアクセス
        logger.log(message: "First log")
        logger.printLogs()
        
        functionA()
        functionB()
        
        logger.printLogs()
    }
}

Main().output()
// 出力結果：
// ["First log"]
// ["First log", "Log from functionA", "Log from functionB"]

// Log from functionSubA がない😱
```
https://swiftfiddle.com/vrl6nlwevbg7xlf4vputjcebji

上記のコードでは、Main クラス内で Loggerクラスのインスタンスを作成しています。またSubクラスでも同じようにLoggerクラスのインスタンスを作成しています。

出力では、`["First log", "Log from functionA", "Log from functionB"]` となっており、functionSubA() メソッドがうまく処理されていません。これは、Subクラスのインスタンスが毎回新しく生成されているからです。

この問題をシングルトンで解決すると以下のようになります。:

```swift
import Foundation

class Logger {
    static let shared = Logger()
    
    var logs: [String] = []
    
    private init() {} // 外部からのインスタンス生成を防ぐ
    
    func log(message: String) {
        logs.append(message)
    }
    
    func printLogs() {
        print(logs)
    }
}

final class Sub {
    func functionSubA() {
        Logger.shared.log(message: "Log from functionSubA")
    }
}

final class Main {
    func functionA() {
        Sub().functionSubA()
        Logger.shared.log(message: "Log from functionA")
    }
    
    func functionB() {
        Logger.shared.log(message: "Log from functionB")
    }
    
    func output() {
        // どこからでも同じLoggerインスタンスにアクセス
        Logger.shared.log(message: "First log")
        Logger.shared.printLogs()
        
        functionA()
        functionB()
        
        Logger.shared.printLogs()
    }
}

Main().output()
// 出力結果：
// ["First log"]
// ["First log", "Log from functionSubA", "Log from functionA", "Log from functionB"]
```
https://swiftfiddle.com/qlbncskacjforiuxicxruedeva

上記の例では、Loggerクラスの唯一のインスタンスが `shared` プロパティとして定義され、Mainクラス以外で参照されていても、同じインスタンスを参照することができます。

# シングルトンの問題点
シングルトンは2つの問題点を同時に解決しようとするため、**単一責任の原則**に違反します。マルチスレッド環境において、 複数のスレッドがシングルトン・オブジェクトを複数回生成しないように特別な処理が必要になります。他にも、ユニットテストが困難になる問題もあります。

これらの問題を解決する場合は、以下の記事が参考になるかもしれません。

https://qiita.com/kntkymt/items/b68b3c26b6d77dee1c47


# 参考文献・関連記事
https://qiita.com/pecoms/items/389ad9f52230331a3c93

https://refactoring.guru/ja/design-patterns/singleton