先日開催されたiOSDC Japan 2019のルーキーズLT枠で値オブジェクトのCodable対応という発表をさせて頂きました。
こちらに関して @takasek さんより、以下のようなご指摘・別解の共有を頂いたので、そちらについて記載させて頂きます。
すみません、いくつか気になったことが!
— takasek (@takasek) September 10, 2019
protocol ValueObjectを作ると、本来のDDD的なValue Object(familyNameとgivenNameを合わせて型表現するなど)の意味が歪んでしまうのを懸念しています
また、protocol ValueObjectでやりたいことはRawRepresentableで実現可能に見えますがどうでしょうか?
ValueObjectプロトコルという命名について
今回は以下の観点からValueObject
という名前をつけました。
- 値オブジェクトを利用して不適切な利用を防ぎたい
-
singleValueContainer
を利用した実装を共通化したい
しかしながら、ご指摘の通り複数のパラメータを保持するようなケース(例えば位置を表す緯度と経度を保持するような値オブジェクト)では適用できません。
今回はユーザーIDや写真IDといった一意な識別子
を表すIdentifiable
が適切だったかと思います。
また、一意な識別子という事でDictionary
のキーに使う事も想定されるので、Equatable
よりHashable
が適切かと思いましたので、以下のように変更してみました。
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
)とRawValue
がEncodable, Decodable, Hashable
プロトコルに適合していると、それぞれのプロトコルのデフォルト実装がされるようです。
一方でCustomStringConvertible
プロトコルはRawRepresentable
でデフォルト実装はされていないので、Identifiable
プロトコルを作成する場合には、デフォルト実装が必要になります。
上記を元に新しく作成したIdentifiable
プロトコルがこちらです。
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にて登壇する機会を提供頂きありがとうございました。
スタッフの皆様、参加者の皆様ありがとうございました!!