Swift4 - Codable : ポリモーフィズムとなんとか折り合いをつける

Swift4のCodableが便利そうということで飛びついた結果、継承+アレイの組み合わせが混じっているととんでもなく面倒なことが起こるということが発覚したわけですが。
http://qiita.com/Yokemura/items/3a704db0bcadec381549

そもそも、継承が存在する時点で親子ともに手作業でencode/decodeを書かなきゃいけないようで
https://stackoverflow.com/questions/44553934/using-decodable-in-swift-4-with-inheritance
正直継承関係のあるオブジェクトをCodableの世界に持ち込むのはやめたほうがいいなと思いました。ひとつやふたつだったらいいですけど、それをアレイにぶち込んでなんたら・・・とかちょっと無理ですね。実際ちょっと書こうとしましたが複雑すぎてあきらめました。それならもうNSCodingの世界に戻った方がましです。

ひとまずサブクラスを使ってポリモーフィズムわっしょい、ってのは厄介そうってことで、ごくプリミティブにこんな風にするのもまあ悪くはないと思ったのですが、これはまあ格好悪いしインスタンスのタイプを見て分岐しているところとかが全部書き直しになるので避けたいなあと。

enum EventType {
    case Note
    case PitchBend
}

struct SequenceEvent : Codable {
    var type : EventType

    // Common
    var time : Double

    // For Note
    var noteNo : Int
    var velocity : Float
    var duration : Float

    // For PitchBend
    var depth : Float    
}

そこでふと思ったのが、ポリモーフィズムはさておき継承が厄介ってところもあったので、プロトコルを使ってこんな感じにしたらどうかな、と。そもそも共通プロパティはtimeくらいしかないというのもあるし。

protocol SequenceEvent : Codable {
    var time : Double {get set};
}

struct SequenceEventNote : SequenceEvent {
    var time : Double
    var noteNo : Int
    var velocity : Float
    var duration : Float

    // クラス継承の関係ではないので、親をデコード/エンコードしてから自分を・・・みたいな処理は不要
}

struct SequenceEventPitchBend : SequenceEvent {
    var time : Double
    var depth : Float

    // 同上
}

とりあえずこれなら var events = Array<SequenceEvent> という形で、同じ規約に沿った異なるオブジェクトをひとつのアレイに入れることはできるわけです。

で、これを保持する側のencode/decodeはどうなるか。

こうなりました。

class Song : Codable {
    var events : [SequenceEvent] = [];
    var name : String = "Song"

    init(name: String) {
        self.name = name
    }

    enum CodingKeys: String, CodingKey {
        case name
        case events
    }

    required init(from decoder: Decoder) throws {
        let container = try? decoder.container(keyedBy: CodingKeys.self)  // 普通にContainerを取得
        if let con = container {
            self.name = try con.decode(String.self, forKey: .name)  // これは普通にdecode

            var unkeyed = try con.nestedUnkeyedContainer(forKey: .events); // arrayの中身を個別にdecodeするため unkeyed containerを取得

            var decodedArray: [SequenceEvent] = []
            while (!unkeyed.isAtEnd) { // Sequenceプロトコルには準拠してないのでforeachなどは使えない模様
                // *** 以下、もうちょっと抽象化できそう ***
                // SequenceEventNoteへデコードしてみる
                let anEvent = try? unkeyed.decode(SequenceEventNote.self)
                if anEvent != nil {
                    decodedArray.append(anEvent!)
                    continue
                }
                // 上記が失敗したらSequenceEventPitchBendへデコードしてみる
                let aPitchEvent = try? unkeyed.decode(SequenceEventPitchBend.self)
                if aPitchEvent != nil {
                    decodedArray.append(aPitchEvent!)
                    continue
                }
                // 両方失敗した場合は知らん
            }
            self.events = decodedArray
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(name, forKey: .name) // ここもnameは普通にエンコード

        var evs = container.nestedUnkeyedContainer(forKey: .events)  // .eventsキーに割り当てる形で unkeyed containerを用意

        try self.events.forEach { // 型を調べて個別にエンコード
            switch $0 {
            case is SequenceEventNote:
                try evs.encode($0 as! SequenceEventNote) //as!は要らないかも
            case is SequenceEventPitchBend:
                try evs.encode($0 as! SequenceEventPitchBend) //as!は要らないかも
            default:
                try evs.encodeNil()
            }
        }
    }
}

長い。
が、そんなに複雑ではないです。
アレイを Array<Type>の形でまるごとencode/decodeするかわりに、unkeyed containerを引っ張り出してきて、それを使って個別に処理しています。

あんまりスマートではないですが、比較的修正範囲も小さいのでまあ逃げ道としてはアリかなあ、という感じですね。

この場合、(元)親クラスが持っていたのが time くらいということで親側の重みが小さかったのでこれが良さそうですが、逆に親側の重みが大きい場合、冒頭のポリモーフィズムを放棄するパターンの方がいいかもです。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.