LoginSignup
0
0

More than 5 years have passed since last update.

swiftでenumのstateパターン(contextをAssociatedValueで扱う版)

Last updated at Posted at 2017-12-26

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は含まれていない
  • 死ぬ
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0