ポリモーフィズムを利用した多種データの保存と復元方法の考察

# はじめに

例えば、お絵描きソフトを想定した場合。画面上に表示するオブジェクトは色々な種類が想定されます。例えば、円であったり長方形であったり三角形であったりです。当然それらは異なるデータを持ちますが、扱いを簡単にするためにポリモーフィズムにて、その扱いを抽象化する設計を目指すと思います。

例えば、こんな `protocol`を想定してみます。全てのオブジェクトを`Shape`として扱う事とします。`UIView`のように、空の`View`にあまり意味がないので、`Shape`はルートクラスではなく、`protocol`とします。

```.swift
protocol Shape {
}
class Circle: Shape {
    var center: CGPoint
    var radius: CGFloat
    // ...
}
class Rectangle: Shape {
    var origin: CGPoint
    var size: CGSize
    // ...
}
class RoundedRectangle: Rectangle {
    var cornerRadius: CGFloat
    // ...
}
```

さて、これらを`Array`で保持する`shapes`を想定して、これらを保存したい場合を想定して、その実現方法を考えてみる事とします。

もっとも最初に思いつくのは、`Codable`を利用して、アーカイブする方法です。便宜上`shapes`を`Contents`というクラスが扱うようにして、`JSONEncoder`などで`Shape`を単純にアーカイブ＆アンアーカイブしてみると、ある問題に気がつくはずです。

```.swift
protocol Shape: Codable { // <-- here!!
}
class Circle: Shape {
}
class Rectangle: Shape {
}
class Contents: Codable {
    var shapes: [Shape]
}
```

```.swift
let a = Circle(center: CGPoint(x: 10, y: 20), radius: 30)
let b = Rectangle(origin: CGPoint(x: 40, y: 50), size: CGSize(width: 60, height: 70))
let contents = Contents(shapes: [a, b])
let encoder = JSONEncoder()
let data: Data = try encoder.encode(contents)
let decoder = JSONDecoder()
let object = try decoder.decode(Contents.self, from: data)
```

保存された`Shape`を復元して確認すると、具象クラスの`Circle`や`Rectangle`の情報が劣化して保存されていない事に気がつくはずです。実際は保存はされているのですが、復元時に劣化しています。

しかし復元時に個々の`Shape`の元々のタイプが何であったのか、知っていなければ、`Contents `が`Shape`の型を個別に復元しようになんともなりません。`Shape`を保存するときに、その型もなんらかの方法で保存するべきなのかもしれません。と、いう訳でこの問題が面倒な問題である事に気がつくわけです。

本記事では、このように、ポリモーフィズムの恩恵をうけつついかにオブジェクトの保存と復元を行うかについて記述を行います。

## NSObject, NSCoding

それでは旧来そもそもどうやってポリモーフィズムの権化を保存、復元していたのでしょうか？`AppKit`/`UIKit`ベースであれば、`NSObject`を基底クラスとして`NSCoding`に準拠、そして`NSKeyedArchiver`と`NSKeyedUnarchiver`で保存復元していました。

では、この方針で試して見ましょう。

```.swift
protocol Shape: NSObject, NSCoding {
	init?(coder: NSCoder)
	func encode(with coder: NSCoder)
}

class Circle: NSObject, Shape {
	var center: CGPoint
	var radius: CGFloat
	init(center: CGPoint, radius: CGFloat) {
		self.center = center
		self.radius = radius
		super.init()
	}
	required init?(coder: NSCoder) {
		self.center = coder.decodeObject(forKey: "center") as! CGPoint
		self.radius = coder.decodeObject(forKey: "radius") as! CGFloat
		super.init()
	}
	func encode(with coder: NSCoder) {
		coder.encode(self.center, forKey: "center")
		coder.encode(self.radius, forKey: "radius")
	}
	override var description: String { "{Circle: \(self.center)-\(self.radius)}" }
}

class Rectangle: NSObject, Shape {
	var origin: CGPoint
	var size: CGSize
	init(origin: CGPoint, size: CGSize) {
		self.origin = origin
		self.size = size
		super.init()
	}
	required init?(coder: NSCoder) {
		self.origin = coder.decodeObject(forKey: "origin") as! CGPoint
		self.size = coder.decodeObject(forKey: "size") as! CGSize
		super.init()
	}
	func encode(with coder: NSCoder) {
		coder.encode(self.origin, forKey: "origin")
		coder.encode(self.size, forKey: "size")
	}
	override var description: String { "{Rectangle: origin=\(self.origin), size=\(self.size)}" }
}

class Contents: NSObject, NSCoding {
	var shapes: [Shape]
	init(shapes: [Shape]) {
		self.shapes = shapes
	}
	required init?(coder: NSCoder) {
		self.shapes = coder.decodeObject(forKey: "shapes") as! [Shape]
	}
	func encode(with coder: NSCoder) {
		coder.encode(self.shapes, forKey: "shapes")
	}
	override var description: String { "{Contents:" + self.shapes.map { $0.description }.joined(separator: ",") + "}" }
}
```

これで、`[Shape]`の保存と復元は実現できました。保存や復元にここにオブジェクトの型を`switch`文などで仕分けする必要もなく、さすが古の技……と言いたいところですが、やはり`NSObject`を基底にしなければならないという制限付きです。

```.swift
let data = try! NSKeyedArchiver.archivedData(withRootObject: contents, requiringSecureCoding: false)
let object = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
```

## Codable

しかし、やはり今の時代、`NSObject` から解放されたいと思うのも仕方ありません。では、もう一度最初の`Codable`でなんとかならないか、考え直してみることにしましょう。

問題はデコード時に個々の具象クラスの型がわからない事に起因します。

```.swift
let data = ...
let object = try? decoder.decode(Shape.self, from: data) // <-- ???
```

そこで、`nestedUnkeyedContainer`を利用して、unkeyed containerを利用して、エンコード時に`Shape`の具象クラス名を文字列として、一緒にエンコードしてやろうという作戦です。`Shape`の具象クラス名は`"\(type(of: shape))"`で文字列にできます。

そして、ここがトリッキーなのですが、通常なら、`container.encode(shape)`なのですが、これはなぜかエラーになるので、`shape`インスタンス側にやらせます。`shape.encode(to: container.superEncoder())`。

`型名`のエンコード、`shape`エンコードを繰り返すので、keyed container ではなく Unkeyed container を利用します。`shape.encode()`には `Encoder`を渡してあげないといけないので、`container.superEncoder()`を渡してあげます。正直、この`superEncoder()`はこれで動くかどうかは試してみるまでわかりませんでした。

```.swift
class Contents: Codable {
    // ...
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var subcontainer = container.nestedUnkeyedContainer(forKey: .shapes)
        for shape in self.shapes {
            let typeString: String = "\(type(of: shape))"
             try subcontainer.encode(typeString)
             try shape.encode(to: subcontainer.superEncoder())
        }
    }
    // ...
}
```

さて、今度はデコード側の処理です。`unkeyed container`から型名を取得して、型に応じてデコードします。さらっと言ってしまいましたが、型の文字列からその型のオブジェクトをインスタンス化してあげる必要があります。`NSObject`ベースであれば、`NSClassFromString()`を使うてもあるかもおしれませんが、せっかく`NSObject`と決別して、この道を選んだので、もう少し足掻いてみる事にします。

実は以前、ランタイムオブジェクトを色々調べていた時にこの手に便利なコードを書いたのを思い出したので、それを利用します。詳細は後述いたします。とにかく、これで、`Shape`の具象クラス名からそのインスタンスを生成できます。

```.swift
class Contents: Codable {
    // ...
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        var subcontainer = try container.nestedUnkeyedContainer(forKey: .shapes)
        let classes = Runtime.classes(conformTo: Shape.Type.self)
        var shapes = [Shape]()
        while (!subcontainer.isAtEnd) {
            let type = try subcontainer.decode(String.self)
            if let shapeType = classes.filter({ "\($0.self)" == type }).first as? Shape.Type {
                let shape = try shapeType.init(from: subcontainer.superDecoder())
                shapes.append(shape)
            }
        }
        self.shapes = shapes
    }
    // ...
}
```

どさくさに紛れて、Runtime のクラス情報などを取得するユーティリティコードを紹介いたします。このコードで、Runtimeに含まれる全クラス、ある特定のクラスのサブクラス、特定のプロトコルに準拠したクラスにアクセスする事ができます。

例えば、`UIView`の全サブクラスを`AnyClass`として取得するには以下のように記述します。

```.swift
let classes: [AnyClass] = Runtime.subclasses(of: UIView.self)
```

今回は、`Shape`プロトコルの具象クラスを全て取得するので、こんな風に書きます。

```.swift
let classes: [AnyClass] = Runtime.classes(conformTo: Shape.Type.self)
```

この中から、目的の具象クラスを探し出して、`AnyClass`として用意します。そして、`init()`でインスタンス化します。

```.swift
let shapeType: AnyClass = ...
let decoder: Decoder = ...
let shape = try shapeType.init(from: decoder))
```

おさらいですが、今回`Shape`の`init`は`protocol`でこんな感じで定義してあるので、それに習います。

```.swift
protocol Shape: Codable, CustomStringConvertible {
	init(from decoder: Decoder) throws
    // ...
}
```

後半の説明は雑になってしまいましたが`encode`時に具象クラスの型名も同時にエンコードしてあるので、`decode`時は型名から、インスタンスを生成し、`switch`文で`case`の羅列を用意することなく、`Shape`の保存と復元を実現する事ができました。

Runtime ユーティリティのコードを上げておきます。`gist`のコメントには簡単ですが、もう少し説明があります。

#### Runtime.swift

https://gist.github.com/codelynx/bb72bf0bed58a327ce1dbe6639d0369b

```.swift
public class Runtime {
    
    public static func allClasses() -> [AnyClass] {
        let numberOfClasses = Int(objc_getClassList(nil, 0))
        if numberOfClasses > 0 {
            let classesPtr = UnsafeMutablePointer<AnyClass>.allocate(capacity: numberOfClasses)
            let autoreleasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classesPtr)
            let count = objc_getClassList(autoreleasingClasses, Int32(numberOfClasses))
            assert(numberOfClasses == count)
            defer { classesPtr.deallocate() }
            let classes = (0 ..< numberOfClasses).map { classesPtr[$0] }
            return classes
        }
        return []
    }
    
    public static func subclasses(of `class`: AnyClass) -> [AnyClass] {
        return self.allClasses().filter {
            var ancestor: AnyClass? = $0
            while let type = ancestor {
                if ObjectIdentifier(type) == ObjectIdentifier(`class`) { return true }
                ancestor = class_getSuperclass(type)
            }
            return false
        }
    }
    
    public static func classes(conformToProtocol `protocol`: Protocol) -> [AnyClass] {
        let classes = self.allClasses().filter { `class` in
            var subject: AnyClass? = `class`
            while let `class` = subject {
                if class_conformsToProtocol(`class`, `protocol`) { print(String(describing: `class`)); return true }
                subject = class_getSuperclass(`class`)
            }
            return false
        }
        return classes
    }
    
    public static func classes<T>(conformTo: T.Type) -> [AnyClass] {
        return self.allClasses().filter { $0 is T }
    }
}
```

## まとめ

コードが完成してすぐにこの記事を書き始めたので、よく検証されていない部分があるかと思います。不具合などを発見した場合は、修正リクエストなりいただけると幸いです。

プロダクションで利用する場合は、クラス名の変更や削除など、より注意深く実装する必要があるかと思います。

## コード

https://gist.github.com/codelynx/428b27b3cfd58b8c7382346f1a4bc415
https://gist.github.com/codelynx/9a336a96b76a39b0922457c0cc84835d
https://gist.github.com/codelynx/f8c4d8f394ee64483cd05e60332253e2

## ライセンス

ライセンスはMITライセンスとさせていただきます。

## 参考にした記事

https://qiita.com/Yokemura/items/11440a9ab87183db3bb7

## 環境

執筆している時点での環境は以下の通りです。

```
$ /usr/bin/xcodebuild -version
Xcode 12.5.1
Build version 12E507
$ swift --version
Apple Swift version 5.4.2 (swiftlang-1205.0.28.2 clang-1205.0.19.57)
Target: arm64-apple-darwin20.6.0
```