swiftでenumでstateパターンで紹介したものアレンジ版。
あっちはいちいち引数でcontextを渡す必要があったが、こっちは内部で持つようになったので、いちいち渡す必要はなくなった。
よりオブジェクト指向的だ。
あえて改良版とは言わないのは、こっちのほうがイケていると完全には思っていないからだ。デメリットも多い。ミスるとメモリリークする。
Mario.swift
class Mario {
var description: String {
return "\(state.description)マリオ\(isDead ? "の死体" : "")"
}
private(set) var isDead = false {
willSet {
if newValue {
print("\(description)は死ぬ")
}
}
}
private var state: State! {
willSet { print("\(description) -> ", terminator: "") }
didSet { print("\(description)") }
}
init() {
self.state = .normal(State.Context(self))
print("\(description)が初期化された")
}
deinit {
print("\(description)は開放された")
}
func onHit() {
print("\(description)が敵に当たった")
state.onHit()
}
enum State: MarioStateImplement {
case normal(State.Context)
case small(State.Context)
var description: String { return delegate.description }
/// 状態の実装の移譲という意味でのデリゲート
/// UIKitで使われる狭義のデリゲートのことではない
var delegate: MarioStateImplement {
switch self {
case .normal(let context):
return Normal.shared(context)
case .small(let context):
return Small.shared(context)
}
}
func onHit() {
delegate.onHit()
}
/// MarioStateImpelementが動作するときのContext
/// コイツには2つの役割がある
/// 1.
/// MarioStateImpelementがMarioのprivateなAPIにアクセスするための橋渡し
/// 2.
/// MarioStateImpelementがMarioを直接参照しないための緩衝材
/// enumのAssociatedValuesは強参照であり、
/// 仮にNormal.sharedなどがassociateValuesでMarioを直接参照すると
/// 循環参照が発生してしまいメモリリークする。
/// そうならないようにコイツが間に入り、Marioをweak参照させる
class Context {
var state: Mario.State {
get { return mario.state }
set { mario.state = newValue }
}
var isDead: Bool {
get { return mario.isDead }
set { mario.isDead = newValue }
}
private unowned let mario: Mario
init(_ mario: Mario) {
self.mario = mario
}
}
}
}
MarioStateNormal.swift
extension Mario.State {
enum Normal: MarioStateImplement {
case shared(Context)
var description: String { return "スーパー" }
// AssociatedValuesを参照するためのもの
// 強引な感じが否めない
private var context: Context {
switch self {
case .shared(let context):
return context
}
}
func onHit() {
context.state = .small(context)
}
}
}
MarioStateSmall.swift
extension Mario.State {
enum Small: MarioStateImplement {
case shared(Context)
var description: String { return "チビ" }
private var context: Context {
switch self {
case .shared(let context):
return context
}
}
func onHit() {
context.isDead = true
}
}
}
MarioStateImplement.swift
protocol MarioStateImplement {
var description: String { get }
func onHit()
}
Main.swift
do {
print("Game start")
let mario = Mario()
while !mario.isDead {
mario.onHit()
}
print("Game over")
}
↓
実行結果
Game start
スーパーマリオが初期化された
スーパーマリオが敵に当たった
スーパーマリオ -> チビマリオ
チビマリオが敵に当たった
チビマリオは死ぬ
Game over
チビマリオの死体は開放された
メリット
- 引数を渡さなくなったので、onHitの引数が0担って楽になったね!
- 例えば
protocol MarioStatefulBehavior { func onHit() }
をのようなprotocolを作りたくなったとして、MarioもStateもNormalもSmallもそれをImplementsできる
デメリット
- Marioがメモリリークしないように気をつけなければならない
- Mario#stateを暗黙アンラップしなくてはいけない
- Normal#contextの実装がイケてない
ちなみに
可視性を下げたいのと別ファイルで書きたいのは両立しない
今回の例だと、Mario.State.ContextをMario.swiftで定義しなくてはならない。そうしないと、private var Mario#state
にアクセスできないのだ。
これは、AssociatedValueを使ったかどうかとは関係ない(前回の例ではそのへんは諦めてprivateを使ってないだけ)。
当該scope の extensionの内であれば、privateにもアクセスさせてほしいと感じるのは私だけだろうか。
PlaygroundでCustomStringConvertible使うと死ぬのナンデ?
以下、推測
- 必ず死ぬわけじゃない
- Marioのdescriptionが動作するためにはstateの初期化が必要
- stateの初期化にはNormal初期化が必要
- Normalの初期化にはContextの初期化が必要
- Contextの初期化にはMarioの初期化(の内、全properyの初期化)が必要
- playgroundはMarioのinitの内、全propertyの初期化が済んだ時点ですぐにdescriptionを使う(推測)
- 全propertyの初期化に、暗黙アンラップ型のstateは含まれていない
- 死ぬ