Swift における可変シングルトン設計の個人的考察

5 min read読了の目安(約4900字

先日、Twitter で次の Swift の質問をしたところ、思いのほか多くの回答をいただいたので、自分の考えを簡単にまとめます。

Q. どちらのクラスがより良い設計と思うか、理由をお答え下さい
(なぜシングルトンを使うのかは一旦置いておいて、使う前提での2択の話です URL)

// typealias Izon = IzonProtocol とする

class Shared1 {
    static let shared = Shared1()
    var izon: Izon!
    private init() {}
    static func setup(izon: Izon) {
        shared.izon = izon
    }
}
class Shared2 {
    static var shared: Shared2!
    var izon: Izon
    private init(izon: Izon) {
        self.izon = izon
    }
    static func setup(izon: Izon) {
        shared = Shared2.init(izon: izon)
    }
}

私の雑な解答 TLDR

解説

本題のコードは、アクセス修飾子の使用が雑であったり、非安全な Implicitly Unwrapped Optional (IUO) を使っている、Izon ダサい などツッコミどころが多く、解釈の方法が多岐にわたるため、ここでは問題を単純化した場合について考えてみます。

コードの単純化

まず、コードを安全側に倒すべく、 letprivate(set) と通常の Optional を使って、もう少し見慣れた形に書き直します。

class Shared1 {
    static let shared = Shared1()
    private(set) var izon: Izon?
    private init() {}
    static func setup(izon: Izon) {
        shared.izon = izon
    }
}

class Shared2 {
    private(set) static var shared: Shared2?
    let izon: Izon
    private init(izon: Izon) {
        self.izon = izon
    }
    static func setup(izon: Izon) {
        shared = Shared2.init(izon: izon)
    }
}

クラスのコア部分の抽出

次に、static 部分を extension スコープに追いやり、クラスのコア部分である「メンバ変数」と「イニシャライザ」に着目 します。

// クラスのコア部分(メンバ変数とイニシャライザ)

class Shared1 {
    private(set) var izon: Izon?
    private init() {}
}

class Shared2 {
    let izon: Izon
    private init(izon: Izon) {
        self.izon = izon
    }
}
// クラスのその他 (static) の実装 (NOTE: extension に移動できる)

extension Shared1 {
    static let shared = Shared1()
    static func setup(izon: Izon) {
        shared.izon = izon
    }
}

extension Shared2 {
    private(set) static var shared: Shared2?
    static func setup(izon: Izon) {
        shared = Shared2.init(izon: izon)
    }
}

すると、 class Shared1class Shared2 がだいぶ読みやすくなりました。
この時点ですでに、どちらのコードを普段から(好んで)書いているか、おぼろげながら見えてくると思います。
そう、 class 内に var を書かなくて済む class Shared2 の方が馴染みがありそうですね。

この話をさらに分かりやすくするために、 izon の他にもたくさんの引数を与えてみましょう。

class Shared1 {
    private(set) var izon: Izon?
    private(set) var izon2: Izon2?
    private(set) var izon3: Izon3?
    ...
    private init() {}
}

class Shared2 {
    let izon: Izon
    let izon2: Izon2
    let izon3: Izon3
    ...
    private init(izon: Izon, izon2: Izon2, izon3: Izon3, ...) {
        self.izon = izon
        self.izon2 = izon2
        self.izon3 = izon3
        ...
    }
}

// NOTE:
// static func setup(izon: Izon, izon2: Izon2, izon3: Izon3, ...) になる

すると、なるほど、 class Shared2 は相変わらず見慣れた書き方だけど、 class Shared1 には大量の var + Optional メンバ変数が生える、良くないパターン になりそうだ、ということが想像できます。

ここでなぜ、上の class Shared1 の型の設計が良くないか、について考えてみます。
それは 代数的データ型 の観点から、

  • class Shared1「複数の Optional (直和) の struct (直積) 型」 が、
  • class Shared2 の単純な struct (直積) 型

に比べて 不要な状態のパターンを含んでしまい複雑化している 、と言えるからです。

class Shared1初期化の init 実装を手抜きしている分、そのツケが、可変メンバ変数の Optional となって表れている と言えます。

もちろん、この複数 Optional 問題については、次のように一箇所にまとめて回避することも一応可能ではあります。

class Shared1 {
    private(set) var izons: (Izon, Izon2, Izon3, ...)?

    private init() {}
}

しかし、これもやはり class Shared2 に比べて馴染みの薄い、苦しい書き方と言わざるを得ません。

原因は明らかに、shared の可変性 (var + Optional) を static 側でなく、メンバ変数側に委ねてしまっている ことです。
そのため、static func setup の)izon 引数の数の拡張に対して、(class Shared1 の)メンバ変数が柔軟に拡張できない というデメリットをもたらしています。

extension (static 部分) を眺める

さて、先ほどの話に戻り、 extension に追いやられた static 実装にもう一度目を向けてみます。

// クラスのその他 (static) の実装

extension Shared1 {
    static let shared = Shared1()
    static func setup(izon: Izon) {
        shared.izon = izon
    }
}

extension Shared2 {
    private(set) static var shared: Shared2?
    static func setup(izon: Izon) {
        shared = Shared2.init(izon: izon)
    }
}

この場合、どちらの実装がより良いと言えるでしょう?
個人的には、この場合も Shared2 の方が shared が Optional であることが明記されていて、static func setup を一度呼んでおく必要がありそう、ということがコードの雰囲気から伝わる ので好みです。
Shared1 の場合、ここでも Optional の情報がメンバ変数側に隠蔽されて分かりにくいデメリットがある)

ただし、 Twitter上の多くの回答 (返信、引用リツイート) にもあるように、 static func setup を (N ≠ 1) 回呼んだ場合の、 shared そのものの入れ替え」「中身 (izon) のみの差し替え」「値がセットされていない(IUOならクラッシュ)」 等の動作の違いなども実践上では大きく悪影響を及ぼす恐れがあり、デバッギングのしやすさを考慮してもどちらか一方が良いとは必ずしも言い切れず、答えはそう単純にいかないように思います。

まとめ

そういうわけで、どちらに転んでも可変シングルトン問題が地獄なら、せめてクラスのコア部分の設計だけでも綺麗になれる Shared2 の方を個人的に推したい と思います。

(回答的に一番近かったのは https://twitter.com/phenan/status/1397897545406316551 )

ただし、この推奨はあくまで static func setup がプログラム起動直後の1回の呼び出しのみで人力保証されていて、(N ≠ 1) 回呼び出しリスクがほとんど無視できる場合を仮定しています。