Edited at

値オブジェクトへのCodable対応 <補足編> #iosdc

先日開催されたiOSDC Japan 2019のルーキーズLT枠で値オブジェクトのCodable対応という発表をさせて頂きました。

こちらに関して @takasek さんより、以下のようなご指摘・別解の共有を頂いたので、そちらについて記載させて頂きます。


ValueObjectプロトコルという命名について

今回は以下の観点からValueObjectという名前をつけました。


  • 値オブジェクトを利用して不適切な利用を防ぎたい


  • singleValueContainerを利用した実装を共通化したい

しかしながら、ご指摘の通り複数のパラメータを保持するようなケース(例えば位置を表す緯度と経度を保持するような値オブジェクト)では適用できません。

今回はユーザーIDや写真IDといった一意な識別子を表すIdentifiableが適切だったかと思います。

また、一意な識別子という事でDictionaryのキーに使う事も想定されるので、EquatableよりHashableが適切かと思いましたので、以下のように変更してみました。


Identifiable.swift


protocol Identifiable: Codable, CustomStringConvertible, Hashable {
associatedtype Value: Codable, CustomStringConvertible, Hashable

var value: Value { get }

init(value: Value)
}

extension Identifiable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(Value.self)
self.init(value: value)
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}

var description: String {
return value.description
}
}



RawRepresentableを用いた別解法

教えて頂いたやり方を早速検証してみようと思ったところ、 @Mt-Hodaka さんが CodableでのValueObject利用 というタイトルで、RawRepresentableを用いた実装方法をまとめて頂いておりました。

こちらも参考にし、先程の例のようにIdentifiableプロトコルを作ってみたいと思います。

まず、Codable, CustomStringConvertible, Hashableのプロトコルを適用していきます。

RawRepresentableのオフィシャルレファレンスに記載の通り、自分自身(Self)とRawValueEncodable, Decodable, Hashableプロトコルに適合していると、それぞれのプロトコルのデフォルト実装がされるようです。

一方でCustomStringConvertibleプロトコルはRawRepresentableでデフォルト実装はされていないので、Identifiableプロトコルを作成する場合には、デフォルト実装が必要になります。

上記を元に新しく作成したIdentifiableプロトコルがこちらです。


Identifiable.swift

protocol Identifiable: RawRepresentable, Codable, CustomStringConvertible, Hashable {}

extension Identifiable where Self.RawValue : CustomStringConvertible {
var description: String {
return rawValue.description
}
}



サンプルコードに適用

struct UserID: Identifiable {

let rawValue: Int
}

struct User: Codable {
let id: UserID
let name: String
let age: Int
}

let json = """
{
"
id" : 1234,
"
name" : "maguhiro",
"
age" : 34
}
"""
.data(using: .utf8)!
let user = try! JSONDecoder().decode(User.self, from: json)
print("\(user.id)") // 1234

let encodeJson = try! JSONEncoder().encode(user)
print(String(bytes: encodeJson, encoding: .utf8)!) // {"id":1234,"name":"maguhiro","age":34}

let a = UserID(rawValue: 1)
let b = UserID(rawValue: 2)
let c = UserID(rawValue: 1)

print(a == b) // false
print(a == c) // true

var map = [UserID: Int]()
map[a] = 2
map[b] = 10
map[c] = 100
print(map) // [2: 10, 1: 100]

自分の発表資料に記載したコードより、シンプルになりましたね!


謝辞

ご指摘頂いた事で考慮が足りていなかった部分を理解することができ、また、自分が知らなかった解法も学ぶことができました。

@takasek さん、 @Mt-Hodaka さんありがとうございました!!

また、今回iOSDC Japan 2019にて登壇する機会を提供頂きありがとうございました。

スタッフの皆様、参加者の皆様ありがとうございました!!