Singleton(シングルトン) — クラスの単一インスタンスを保証し、それへのグローバルなアクセスを提供する生成パターンです。Singletonはモバイル開発において共有リソース(ネットワーククライアント、データベース、設定マネージャー)に広く使用されています。このパターンはGoF(1994)の古典的な書籍で説明されており、最も認知されているパターンの一つです。詳細はRefactoring Guru: Singletonをご覧ください。
重要なポイント
Singleton — GoF(Gang of Four)が1994年に説明した生成デザインパターンです。このパターンは2つの問題を解決します:クラスのインスタンス化を単一のオブジェクトに制限し、そのオブジェクトへのグローバルアクセスを提供します。Singletonは、一意でなければならないリソース(セッションファクトリー、イメージキャッシュ、データベース接続マネージャー、CrashlyticsやAnalyticsクライアント)に役立ちます。
Singletonの実装には、プライベートコンストラクター(外部からの作成を禁止)、単一インスタンスを持つ静的フィールド、および静的アクセスメソッド(shared、instance、getInstance)が必要です。クライアントはオブジェクト作成を気にせずにSingleton.shared.method()を呼び出します。このパターンはiOSとAndroidで人気があります:URLSession.shared、UserDefaults.standard、FirebaseApp.sharedInstance — すべてSingletonです。ただし、Singletonの過剰な使用はGlobal Stateアンチパターンにつながります。
Singletonの問題 — 隠れた依存関係(クラスが暗黙的にSingletonオブジェクトに依存)、テストの複雑さ(追加の努力なしではテストでインスタンスを置き換えられない)、単一責任の原則の違反(Singletonが自身のインスタンスとビジネスロジックの両方を管理)。現代のモバイル開発では、単一インスタンスの管理にDI(Dagger、Hilt、Swinject)を好みます — DIコンテナはオブジェクトを一度作成し、コンストラクターを介して注入します。
Swift Singletonは、プライベートイニシャライザーを持つ静的sharedプロパティを通じて実装されます。Swift 3以降、静的プロパティの遅延初期化はスレッドセーフであることが保証されています — コンパイラがdispatch_onceを介して自動的に同期を追加します。static let shared = Class()を宣言し、init()をプライベートにするだけで十分です。Swiftは初期化後のシングルスレッドアクセスに追加の同期を必要としません。
final class NetworkManager {
// スレッドセーフなSingleton
static let shared = NetworkManager()
private init() {
URLSessionConfiguration.default.timeoutIntervalForRequest = 30
}
private var cache = NSCache<NSString, NSData>()
func fetchData(from url: URL) async throws -> Data {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
return cached as Data
}
let (data, _) = try await URLSession.shared.data(from: url)
cache.setObject(data as NSData, forKey: key)
return data
}
}
// 使用方法
let data = try await NetworkManager.shared.fetchData(from: url)
AppleのSingleton — iOS SDKの多くのオブジェクトがSingletonを使用しています:UIApplication.shared、UIScreen.main、FileManager.default、NotificationCenter.default、UserDefaults.standard。Appleは物理的に一意なサービス(1つの画面、1つのアプリケーション)にSingletonを使用しています。開発者はこのパターンを自身のサービスにコピーします。SwiftUIでは、SingletonへのグローバルアクセスはEnvironmentと@EnvironmentObjectに置き換えられ、テスタビリティが向上します。
Kotlin Singleton — 最も簡単な方法:objectキーワードが最初のアクセス時に遅延初期化されるシングルトンクラスを宣言します。Kotlin objectはスレッドセーフであり、追加の同期は必要ありません。コンストラクターパラメーターを持つSingletonが必要な場合、lazyデリゲートとともにcompanion objectが使用されます。Androidでは、ApplicationコンテキストやApplication.onCreate()を通じて初期化されるサービスにSingletonがしばしば必要です。
// オプション1:object — パラメーターなしのシンプルなSingleton
object AppPreferences {
private val prefs = Application.instance
.getSharedPreferences("app", Context.MODE_PRIVATE)
var isFirstLaunch: Boolean
get() = prefs.getBoolean("first_launch", true)
set(value) = prefs.edit { putBoolean("first_launch", value) }
}
// オプション2:companion object — パラメーター付きのSingleton
class ApiClient private constructor(baseUrl: String) {
companion object {
@Volatile
private var instance: ApiClient? = null
fun getInstance(baseUrl: String): ApiClient {
return instance ?: this.synchronized {
instance ?: ApiClient(baseUrl).also { instance = it }
}
}
}
fun request(endpoint: String): String { /* ... */ }
}
Android SDKのSingleton — 多くのAndroidシステムサービスがSingletonを実装しています:context.getSystemService()、Room.databaseBuilder()、Retrofit.Builder()。例にはSharedPreferences、MediaPlayer、AudioManagerが含まれます。Androidアプリケーションでは、Singletonはリポジトリ、マネージャー、ファクトリーによく使用されます。GoogleはSingletonをDI(Hilt、Koin)に置き換えることを推奨しています。Singletonスコープ(Scope.Singletonまたは@Singleton)がコンテナによって管理され、クラスはテスト可能なままです。
Thread safety — マルチスレッド環境でのSingletonにとって重要な要件です。同期がないと、2つのスレッドが同時にinstance == nullをチェックし、2つのインスタンスを作成する可能性があります。解決策は、最初の作成時にロックし、初期化後に解放することです。Swiftでは、静的プロパティ(static let)はデフォルトでスレッドセーフです。Kotlinでは、objectはスレッドセーフです。KotlinでJavaスタイルを行うには、synchronizedまたは@Volatile + double-check lockingが使用されます。
| 言語 | メカニズム | スレッドセーフ | 遅延初期化 |
|---|---|---|---|
| Swift | static let | dispatch_once(自動) | はい、最初のアクセス時 |
| Kotlin object | Object宣言 | クラス初期化子はスレッドセーフ | はい、最初のアクセス時 |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | はい、lazyまたはsynchronized経由 |
| Java | synchronized + volatile | Double-checked locking | はい、getInstance()内 |
Double-checked locking — Singletonの遅延初期化のためのパターンです。最初のチェックは同期なし(インスタンスが既に存在する場合は高速)、2番目はsynchronized内(1つのスレッドのみが作成)。@Volatileは全てのスレッドに対する変更の可視性を保証します。volatileがないと、別のスレッドが部分的に構築されたオブジェクトを見る可能性があります。Kotlinでは、LazyThreadSafetyMode.SYNCHRONIZEDを使用したlazyデリゲートが自動的にdouble-checked lockingを実装します。
Dependency Injection — 単一インスタンスを管理するためのSingletonの代替手段です。DIコンテナ(Dagger、Hilt、Koin、Swinject)はSingletonスコープでオブジェクトを一度作成し、コンストラクターを介して注入します。クラスは自身のSingletonステータスを知りません — コンテナが決定します。コードはテスト可能になります:テストではDIモジュールがモックモジュールに置き換えられます。DIの利点:コンストラクターでの明示的な依存関係、オーバーライド可能性、統一されたライフサイクル。
Singletonが正当化される場合 — システムレベルのオブジェクト:Crashlytics、Analytics、Logging。これらのサービスはAppDelegate/Applicationで一度初期化され、至る所で使用されます。それらにはDIは過剰です。Singletonはイメージキャッシュ(NSCache、Coil、Glide)にも便利で、グローバルアクセスがパフォーマンスによって正当化されます。それ以外のすべてにはDIが推奨されます:依存関係を可視化し、テストとリファクタリングを簡素化します。
ハイブリッドアプローチ — テスト用にオーバーライド可能なSingleton。Swiftでは、プロトコル+テストが置き換え可能な静的プロパティ(例:URLSession用のURLProtocol経由)。Kotlinでは、注入可能なプロパティを持つオープンクラスで、テストがリフレクションまたはセッターを通じてモックを設定します。このアプローチはSingletonのシンプルさを維持しつつ、テスト機能を提供します。GoogleはAndroidにHiltを推奨、AppleはiOSにDIを強制しません — 選択はチーム次第です。
よくある質問
いいえ、SingletonはGoFパターンですが、その頻繁な誤用がGlobal Stateアンチパターンに変えます。Singletonは物理的に一意のリソース(画面、プリンター、ファイルシステム)には正当化されます。Singletonがデータ管理に使用されると問題が発生します:隠れた依存関係、テストの複雑さ、単一責任の原則の違反。現代の代替手段はSingletonスコープのDIです。
3つのアプローチ:(1)プロトコル経由 — Singletonがプロトコルを実装し、テストが実装を交換;(2)DI経由 — Singletonがコンストラクターを介して依存関係として注入;(3)resetメソッド経由 — テストで状態をリセットするメソッドを持つ(テストビルドのみ)。最初のアプローチが推奨され、3番目はプロダクションにとって危険です。Swiftではテストでのランタイム操作を通じてsharedプロパティを置き換えられます。
Kotlin objectはバイトコードレベルでSingletonを作成する言語構造です。プライベートコンストラクターとgetInstance()を持つJava実装とは異なり、objectはスレッドセーフ、遅延初期化を保証し、継承を禁止します。Java Singletonはマルチスレッド環境での正しい動作に手動同期(synchronized)とvolatileを必要とします。Kotlin objectはAndroidで最も安全で簡潔な方法です。
Singletonの継承はパターンを壊します:Singletonクラスが継承可能な場合、サブクラスが2番目のインスタンスを作成し、一意性に違反する可能性があります。Swiftでは、final classが継承を禁止します。Kotlin objectは継承できません(objectはsealed)。バリエーションを持つSingletonが必要な場合、SingletonスコープのDIコンテナを使用してください:単一インスタンスを保証し、インターフェースを通じた継承をサポートします。
パラメーターはinit(context: Application)またはgetInstance(param)を介して渡されます。Kotlin objectはパラメーターを受け付けません — ファクトリメソッドgetInstance(param)を持つcompanion objectを使用してください。Hiltが問題を解決します:@Singleton + @Inject constructor(context: Application) — DIコンテナが自動的にApplicationコンテキストを注入します。Retrofitクライアントの場合、パラメーター(baseUrl、interceptors)はDIモジュール内のビルダーを通じて渡されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。