ソフトウェア設計

SwiftのMonostateパターン:Singletonの代替になり得るのか

Singletonを本当にこのまま使い続けてよいのかと思った方へ

読了 5 分
SwiftのMonostateパターン:Singletonの代替になり得るのかのカバー画像

Singletonを本当にこのまま使い続けてよいのかと思った方へ

iOS開発をしていると、Singletonを一度も使ったことがない人はいないでしょう。私もそうでした。SomeManager.shared このようなコードが、どのプロジェクトにも必ず数個はあったものです。

ところが、ある時からこの.sharedが少しずつ扱いづらくなりました。テスト時に状態が絡み合い、チームメンバーがあちこちから自由にアクセスしてしまうのです。

そんな中で知ったのがMonostateパターンです。今回は、SwiftにおけるMonostateとは何か、そして本当にSingletonの代替になり得るのか、実際に使ってみた経験を共有します。

先に結論を言うと、Monostateは「状態は1つに共有しながら、インスタンスは複数作れるようにする」パターンです。目的はSingletonと同じですが、アプローチは正反対です。ただし万能ではなく、状況によって向き不向きがあります。以下で順に見ていきましょう。

Monostateパターンとは一体何ですか?

Singletonの核心は「インスタンスを1つだけ作る」ことです。イニシャライザを制限し、sharedだけを使うようにします。

Monostateはその反対です。インスタンスはいくつでも作れます。その代わり、内部の状態(データ)をすべてstaticで共有します。

つまり、オブジェクトAを作ってもBを作っても、両者が見るデータは同じです。外からは普通のオブジェクトに見えますが、内部では1つの状態につながっています。

Swiftで簡単に書くと、次のような形です。

struct Settings {
    private static var _volume = 50   // 状態を staticで共有
    var volume: Int {                 // インスタンスのように使う
        get { Settings._volume }
        set { Settings._volume = newValue }
    }
}
// 異なるインスタンスだが状態は1つ
var a = Settings(); a.volume = 80
print(Settings().volume)  // 80

Settings()を新しく作っても、volumeは80になります。インスタンスは複数でも状態は1つ。これがMonostateのすべてです。

インスタンスは3つでも、見ている状態は1つです
インスタンスは3つでも、見ている状態は1つです

Singletonとは何が違うのでしょうか?

最も大きな違いは、利用する側のコードです。

Singletonでは、利用側が常に.sharedを意識する必要があります。「これはSingletonだ」と考えながら使うわけです。Monostateなら、普通のオブジェクトのようにSettings()を作って使うだけなので、利用側が内部実装を知る必要はありません。

表にまとめると、次のようになります。

区分 Singleton Monostate
インスタンス数 1つだけ 複数作成可能
共有するもの インスタンス自体 状態(staticデータ)
利用側 .sharedを明示 普通のオブジェクトのように使う
継承 難しい 比較的自由

特に面白いのが継承です。Singletonは継承しづらい一方、Monostateは通常の型なので、より柔軟に拡張できます。


では、Singletonの代替になり得るのでしょうか?

正直に言うと、なる場合もあれば、ならない場合もあります。

まずはメリットです。利用側のコードがすっきりし、.sharedというグローバルなアクセスポイントがコードのあちこちに現れません。インターフェースも普通のオブジェクトと同じなので、後から構造を変える負担も小さくなります。

Monostateは「グローバル状態を隠す」パターンです。便利そうに見えますが、隠れたグローバル状態はそれ自体が諸刃の剣です。

問題はここです。普通のオブジェクトに見えるため、チームメンバーが「これは単なるローカルオブジェクトだろう」と作ったところ、実は状態がグローバルに共有されていた、ということが起こります。かえって混乱しやすいのです。

また、staticで共有するため、マルチスレッド環境では並行処理の問題にも同じように注意が必要です。この点はSingletonと変わりません。

私の経験では、最近のSwiftなら、Monostateより先に依存性注入を検討することをおすすめします。テストがしやすく、依存関係も明確になるからです。

最近のSwiftプロジェクトなら、まずDIを選びます
最近のSwiftプロジェクトなら、まずDIを選びます

では、どんなときに使うとよいのでしょうか?

まとめると、次のような場合です。

  1. 既存のSingletonコードが多すぎて、段階的に取り除きたいとき
  2. 利用側のインターフェースを、普通のオブジェクトのようにすっきり保ちたいとき
  3. 継承や拡張が必要なグローバル状態を扱うとき

反対に、新しいプロジェクトをゼロから作るのであれば、Monostateよりも依存性注入や明示的な状態管理を先に検討することをおすすめします。

私はパターンを道具箱に入れておき、状況に合うときに取り出して使います
私はパターンを道具箱に入れておき、状況に合うときに取り出して使います

結局のところ、Monostateは「Singletonの完全な上位互換」ではなく、「性格の異なるもう1つの選択肢」です。道具箱に入れておき、状況に合うときに使えばよいのです。

今日の内容が、.sharedの乱用に疲れた方への小さなヒントになればうれしいです。むやみに乗り換えるのではなく、自分たちのプロジェクトに何が合うのか、一度考えてみてください。

あわせて読みたい