Swiftで開発していると、static let sharedの1行で作られたシングルトンを本当によく見かけます。
初めてiOSアプリを作るときは、ネットワークマネージャー、ユーザーセッション、キャッシュ管理まで、すべてをシングルトンで埋め尽くしがちです。
便利だからです。どこからでもManager.sharedを呼べばよいのですから。
しかしプロジェクトが大きくなると、奇妙なことが起こり始めます。テストができません。画面を1つ直しただけなのに、無関係な場所でバグが発生します。
シングルトンがアンチパターンと呼ばれるのには、理由があります。
この記事では、SwiftのSingleton Patternとは何か、なぜ多用するとアンチパターンと見なされるのか、それでも使ってよい場面はいつかを、実務の観点から整理します。
結論から言えば、シングルトン自体が悪いのではありません。「どこからでもグローバルに状態を変更できるよう開いておく方式」が問題です。そのためsharedを無分別に多用すると、テストが難しくなり、依存性が隠れ、並行処理の問題まで発生します。
SwiftのSingleton Patternとは、いったい何ですか?
シングルトンとは、アプリ全体でインスタンスが必ず1つだけ存在することを保証するデザインパターンです。
Swiftでは非常に短く作れます。これが魅力であり、落とし穴でもあります。
final class NetworkManager {
static let shared = NetworkManager() // アプリに1つだけ
private init() {} // 外部からの生成を禁止
func request(_ url: URL) { /* ... */ }
}
static letはSwiftによってスレッドセーフに一度だけ初期化されます。そのため、別途lockがなくてもインスタンス生成自体は安全です。
private init()で外部からの生成を防ぐことも重要です。これがなければ、単なるグローバルオブジェクトであってシングルトンではありません。
ここまではとてもきれいです。問題は、どこからでも呼び出せることから始まります。
sharedインスタンスがアンチパターンと呼ばれるのはなぜ?
よく指摘される3つの理由を、私が経験した順に説明します。
1つ目は、テストが地獄になることです。
コード内でNetworkManager.sharedを直接呼ぶと、テスト時にモックオブジェクトへ差し替える方法がありません。
実際のサーバーにリクエストが送られたり、テスト同士で状態を共有したりするため、順番が変わるだけで結果が変わります。
2つ目は、依存性が隠れてしまうことです。
ある関数が内部でこっそりUserSession.sharedを使っていると、関数シグネチャだけでは何を必要としているのかわかりません。
協業時には本当に危険です。コードをすべて開いて初めて依存関係が見えるからです。
3つ目は、グローバルな可変状態による並行処理の問題です。
シングルトンがプロパティを持ち、複数のスレッドが同時に読み書きすると、データ競合が発生します。
インスタンス生成が安全でも、その内部の状態変更まで安全とは限りません。この2つを混同するとクラッシュに遭遇します。
まとめると、こうです。
シングルトンが批判される本当の理由は、「1つしか存在しないから」ではなく、「どこからでも取り出して、どこからでも変更できるから」です。
では、シングルトンは絶対に使ってはいけないのでしょうか?
いいえ。今でも特定の状況では使います。
そもそも1つだけ存在するのが自然なものもあります。たとえばUserDefaults.standard、FileManager.default、URLSession.sharedのように、Appleも標準ライブラリでシングルトンを使っています。
基準は次のように考えると便利です。
- 状態をほとんど変更せず、読み取りが中心 → シングルトンでよい
- アプリ全体で本当に1つでなければならない → シングルトンを検討
- テストでは異なる動作が必要 → 依存性注入を推奨
重要なのは、「コード内でsharedを直接呼ばず、外部から注入する」ことです。
同じシングルトンを使っていても、このように変えるだけでテスト容易性は大きく向上します。
protocol Networking { func request(_ url: URL) }
extension NetworkManager: Networking {}
final class FeedViewModel {
private let network: Networking
init(network: Networking = .shared) { // デフォルトはシングルトン、テストでは mock を注入
self.network = network
}
}
通常はデフォルトのシングルトンを使えて便利で、テスト時には偽物のオブジェクトを入れられるため柔軟です。
シングルトンを捨てたのではなく、呼び出し方だけを変えたのです。
実務で守っている3つのルール
最後に、実務で自分に課している基準を共有します。
- 可変状態を複数箇所から変更するシングルトンは作らない。状態が必要なら、所有者を明確にする。
sharedを関数内で直接呼び出さず、イニシャライザやパラメータから注入する。- 同時アクセスがあるシングルトンはactorにするか、直列キューで保護する。
この3つを守るだけで、「シングルトンのせいでプロジェクトが腐る」状況はほぼ防げます。
シングルトンは間違った道具ではなく、簡単すぎるため多用されやすい道具です。
便利だからといってsharedをどこにでもばらまく前に、「これ、あとでテストできる?」と一度だけ自問してください。その1つの問いが、6か月後の自分を救います。

