Swift におけるシングルトン・staticメソッドとの付き合い方

GoFの23のデザインパターンの1つであるシングルトンに対しての、Swiftとしての付き合い方について思うところをつらつらと書いていきます。ちなみに、記事中盤くらいからが本題です( ´・‿・`)

デザインパターンで有名なGoF本はC++・SmallTalkで書かれていますが、最近のモダンな言語においては23のデザインパターンがそのまま有効かどうかは懐疑的です。本記事ではそのうちシングルトンパターンについて焦点を当てていきます。


シングルトンパターンとは?

最も有名なデザインパターンなはずで多くの方は知っていると思いますがあらためて書くと、シングルトンパターンはインスタンスが1個しか生成されないことを保証したい時に使います。

例えば、iOSアプリでは起動時にまず application(_:didFinishLaunchingWithOptions:) というメソッドが呼び出されますが、この引数の applicationUIApplication.shared というシングルトンプロパティで取得できるものと同一です。

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// 起動時の処理
}

application インスタンスが1つのみであることが保証されていて、実行中のアプリケーションは自身の1つのみなので適した使いどころです。

ちなみに UIApplication() と書いて、新しいインスタンスを作ろうとするとコンパイルは通りつつ実行時に次の例外が発生します。これは UIApplication はObjective-C由来のクラスなために初期化処理をコンパイルレベルで禁止できないが故の次善の策です。

'NSInternalInconsistencyException', reason: 'There can only be one UIApplication instance.'

Swiftでのシングルトンの書き方

Swift は最もシングルトンが書きやすい言語の一つなのではないでしょうか。シングルトンの要件を完璧に満たすものが次のようなわずかなコードで書けて素晴らしいです👏

class MyApplication {
// 自動的に遅延初期化される(初回アクセスのタイミングでインスタンス生成)
static let shared = MyApplication()
// 外部からのインスタンス生成をコンパイルレベルで禁止
private init() {}
}

ちなみに、classではなくstructにすると、Copy on Write (CoW)によって暗黙的にコピーが発生して、「インスタンスが1つ」ではなくなってしまいますのでシングルトンの要件を満たせません。

struct Singletonもどき {
var value = "original"
static let shared = Singletonもどき()
}
var instance1 = Singletonもどき.shared
// ここでCoWによるコピーが発生して、`Singletonもどき.shared`とは別物に
instance1.value = "modified"
print(instance1.value) // "modified"
print(Singletonもどき.shared.value) // "original"

ちょっと蛇足ですが、Objective-CはSwiftに比べて少し面倒と言えど、dispatch_once_t というトークンで1回限りの初期化を保証する処理があるため、比較的書きやすいです。

さらに蛇足ですが、Javaなどでは遅延初期化・マルチスレッド対応・パフォーマンスケアなど留意したシングルトンを定義しようとすると、ダブルチェックロック手法が必要で、定義のたびに手間です。

このように、シングルトンは一般的にはそこそこ定義が面倒ですが、Swiftではかなりシンプルに書けます。

では、Swiftでシングルトンはいつ使うのか?

大抵のケースではシングルトンは不要に思う

僕は大抵のケースではベタにstaticメソッドを使うだけで済むのではと思っています。こういう例を見ると、普段シングルトンに求めている要件を満たせることがイメージできるのではないでしょうか。MyStruct.Type 型のオブジェクト(?)は1つしか存在できません。

struct MyStruct {
// staticプロパティ・メソッド扱う分には初期化できても問題ないが禁止した方が分かりやすい。
private init() {}
static var value = "original"
// このようにメソッド定義していく
static func foo() { print("foo") }
}
var myStruct: MyStruct.Type = MyStruct.self
myStruct.foo()
// `myStruct` はインスタンスでは無いので、CoWによるコピーとは無関係
myStruct.value = "modified"
print(myStruct.value) // "modified"
print(MyStruct.value) // "modified"

structで定義していますが、classでもOKです(個人的には、classである必要が無い場合は軽量なstructで済ませるポリシーですが)。あるいは、 enum にすると private init() {} が不要になるのでそれも良いです。こういう時に structenum のどちらを使うかというのは、staticメソッド云々の文脈というより名前空間もどきをどう作るかという時によく話題にのぼるところです。

今のソフトウェアエンジニアの多くがシングルトンパターンに馴染みがあり、かつSwiftではその実装が簡単という事情もありつつも、わざわざデザインパターンを使わずに単純にstaticメソッド・プロパティで済ませた方がシンプルに感じます。

staticメソッド・プロパティで済まないケース

では、staticメソッド・プロパティで済まずにシングルトンパターンが必要なケースはあるのだろうかと、しばらく前に考えたことがあります。当時思い付かなかったので調べてみたら、こういう意見がありました。

「本来staticメソッドとして提供したい機能だがそれではインターフェースを実装できないのでインスタンスメソッドとして提供する。このときに多重インスタンス化を避けるため。」

「へー、なるほど」と一瞬思ったものの、Swiftは以下のコードのようにこの制限は無いです。

protocol MyProtocol {
static func foo()
}
struct MyStruct1: MyProtocol {
static func foo() { print("foo1") }
}
struct MyStruct2: MyProtocol {
static func foo() { print("foo2") }
}
// 引数をProtocolのTypeで受ける
func bar(_ p: MyProtocol.Type) { p.foo() }
// 実装structによって振る舞いを変えられる
bar(MyStruct1.self) // "foo1"
bar(MyStruct2.self) // "foo2"

そのため、この記事がこれこそシングルトンパターンの意義だと主張しているのをそのまま汲み取ると、Swiftではシングルトンパターンを使う意味が無いことになります🤔

インスタンス(メソッド)が必要なケースがある

Swiftではシングルトンはほぼいらない子と思えてきますが、iOSアプリを開発する上ではstatic戦法では済まないことが度々あることに気づきました。

例えば、通知をハンドリングするために UNUserNotificationCenter のdelegateに UNUserNotificationCenterDelegate に準拠した(次のインスタンスメソッドを実装した)インスタンスをセットする必要があります。

この時にdelegateにシングルトンインスタンスは渡せますが、static戦法では解決できないです。

というわけで、こういうフレームワークと組み合わせた場合などにインスタンスがないと困ることもあります。逆に、自身の書くSwiftコードに閉じた範囲ではstatic戦法で済み、今のところシングルトンが本当に必要な場面がやはり分からないです🤔

広義のシングルトン

冒頭に書いた通り、本来シングルトンとは「インスタンスが1個しか生成されないことを保証」するパターンですが、グローバルには複数個のインスタンスが存在しうるものもシングルトンと呼んでいるものも散見されます。

「デフォルトインスタンス」パターン

区別のために勝手に「デフォルトインスタンス」パターンと名付けて説明します。次の例のように、

  • 大抵のケースはこれで良いだろうというものを default インスタントとして定義して、利用側はそれを使い回す
  • あえて default インスタンスとは別インスタンスが欲しいユースケースのためにその生成も許す
class MyObject {
let value: String
// 通常使うデフォルトインスタンス
static let `default` = MyObject(value: "value")
func foo() { print(value) }
init(value: String) {
self.value = value
URLSession.shared
}
}
MyObject.default.foo()
// 違う設定のインスタンスを作りたかったりスレッドごとにインスタンス分けたいなどの事情で別途作成
MyObject(value: "value2").foo()

例としては、ファイル操作を担う FileManager クラスは大抵 FileManager.default で取得した同一インスタンスを使う一方、 FileManager() で別インスタンスを作れるようになっていることなどが該当します。

また、 FileManagerdefault の定義を覗くと、singleton と明記されています👀

FileManagerクラスの`default`プロパティの定義

シングルトンの定義から外れているような気がしましたが、default プロパティ経由で取得したものは常に同一インスタンス」というニュアンスで「シングルトン」と呼んでいるのかなと想像しています🤔
これらを区別したい場合には次のような呼び分けをするのが良いかなと思いました:

  • シングルトンパターン: グローバルに、インスタンスが1個しか生成されないことを保証
  • デフォルトインスタンスパターン: 常に同一インスタンスを取得できるパスを提供しつつ、複数インスタンス生成も許す

ちなみに上に貼ったXcodeから辿った定義に添えられているコメントとは違って、ドキュメントには「The default FileManager object for the file system.」と書かれていて、この表現ならばしっくりです( ´・‿・`)

それぞれ自分で定義する場合のプロパティ名の使い分けは、Cocoaフレームワークに沿って、以下で揃えるのが良いかなと思っています。

  • シングルトン: shared
  • デフォルトインスタンス: default

…と言いたいところですが、同じくデフォルトインスタンス的位置付けのはずの URLSession には shared というインスタンスが定義されているのを見ると、よく分からなくなってきます🤬
(僕はこれは無視して上の2つの命名規則で書いていますが。)

また、この URLSession.shared もデフォルトインスタンスとして有用な例で、「細かいことはどうでも良いからとにかく通信したい時」は URLSession.shared を使い、「例えばバックグランド通信したいなどの特殊な用途」では次のような少し複雑な初期化をしたりします。

let configuration = URLSessionConfiguration.background(withIdentifier: "background")
let session = URLSession(configuration: configuration,
delegate: self,
delegateQueue: OperationQueue.main)

というわけで、広義のシングルトンとも言えるこの「デフォルトインスタンス」パターンは有用に感じることが多いです。

そもそも、シングルトンにしろstaticにしろ、Dependency Injection(DI)しがたいなど柔軟性に欠けるので、アンチパターンでは?

これは常に意識しつつ、どういう設計にするのがバランス良いのか迷っています。例えばDIに関しては、このようにstaticメソッドの引数で他のインスタンスを注入可能にしておけば、実質問題無いです。

この例ではstaticメソッドでDIしていますが、 static varで依存を保持することによって一連のstaticメソッドの依存を一気に制御するパターンもありえます。
(static戦法を使わずに、コンストラクターインジェクションにすれば初期化後の依存を不変にすることができて、より堅牢にできますが。)

なので、こういったstaticメソッド多用パターンはプロダクションコードでも十分ありだと思っています🤔

関連記事

本記事公開2日前(その時点で本記事はすでに書き上げていました)に、ちょうど同様の主旨の記事が上がっていたので載せておきます。使用言語はSwiftではなくC#/VBなこともあり内容に少し差異はありますが、併読すると参考になるかと思います。

まとめ

  • シングルトンパターンを使っている箇所は大抵staticプロパティ・メソッドを束ねたstruct(あるいはenum)で代用できる
  • とはいえSwiftのシングルトンパターンは実装が容易なので、staticで済ませるかシングルトンパターン使うかは好みで決めても良いかもしれない(個人的には実装が容易といえどもデザインパターンを使う意味の無いところでは使わず済ませるのが良い派)
  • Swiftにおいて、インスタンスが1個しか生成されないことを保証するシングルトンの存在意義は限定的だが、「デフォルトインスタンス」パターンは有用な場面が多い
  • staticメソッド中心に組む場合でもDIなど可能であり、もっと積極利用していっても良さそう