Swift における可変シングルトン設計の個人的考察
先日、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
ダサい
コードの単純化
まず、コードを安全側に倒すべく、 let
、private(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 Shared1
と class 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) 回呼び出しリスクがほとんど無視できる場合を仮定しています。
Discussion