最近、Modelの設計をちゃんと勉強したいなと思うようになり「ドメイン駆動設計入門」という本を読み始め、今は6章を読んでいるところです。 サンプルコードはC#で書かれていますが、とても分かりやすく、iOSを勉強している人にも是非オススメしたい一冊です。
特に、2章 (主に2.5節) の 値オブジェクト についての内容が勉強になったので、Swiftのコードを使って紹介します。
改善前
例えば、ToDoアプリのドメインモデルとして、以下のようなModelがあるとします。
final class ToDo {
let id: String
private(set) var title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
func changeTitle(_ title: String) {
self.title = title
}
}
id
はリポジトリから割り当てられる識別子で、title
はToDoの内容です。こんな感じで使えます。
var todo = ToDo(id: UUID().uuidString, title: "散歩する")
print(todo.title) // 散歩する
todo.changeTitle("ランニングする")
print(todo.title) // ランニングする
このModelについて考えるべきポイントは、
id
やtitle
の型をStringにしているが、これは本当に適切か?
という点です。
改善後
id
やtitle
の型を、それぞれStringをラップした値オブジェクトに書き換えてみます。
値オブジェクトは、システム固有の値を表すイミュータブルなオブジェクトです。
struct ToDoID {
let value: String
init(_ value: String) {
self.value = value
}
}
struct ToDoTitle {
let value: String
init(_ value: String) {
self.value = value
}
}
final class ToDo {
let id: ToDoID
private(set) var title: ToDoTitle
init(id: ToDoID, title: ToDoTitle) {
self.id = id
self.title = title
}
func changeTitle(_ title: ToDoTitle) {
self.title = title
}
}
var todo = ToDo(id: ToDoID(UUID().uuidString), title: ToDoTitle("散歩する"))
print(todo.title.value) // 散歩する
todo.changeTitle(ToDoTitle("ランニングする"))
print(todo.title.value) // ランニングする
コードが長くなっただけ? 本当にそうでしょうか。
値オブジェクトの強み
ドメイン駆動設計入門によると、値オブジェクトの強みは以下の4つです。
- 表現力
- 誤った代入を防ぐ
- 不正な値を存在させない
- ロジックの分散を防ぐ
これらの強みが、値オブジェクトを採用する判断基準となります。
では、それぞれの観点で 改善前のModel と 改善後のModel を比較していきましょう。
(1) 表現力
改善前は、型から得られる情報が「文字列であること」だけなので、そのデータが何を表しているのか、これだけでは分かりません。
改善後は、型を見れば何を表しているデータかすぐに分かります。
例として、引数がタイトルで、IDを戻り値として返す関数を考えてみましょう。
// 改善前
func hoge(title: String) -> String
// 改善後
func hoge(title: ToDoTitle) -> ToDoID
このように、表現力の差が一目瞭然です。
(2) 誤った代入を防ぐ
先ほどの例で言うと、引数で誤ってIDを渡してしまったら。
// 改善前
let id = hoge(title: todo.id) // 何事もなくコンパイル通る
// 改善後
let id = hoge(title: todo.id) // コンパイルエラー
タイトルがStringだった場合、明らかに渡すデータを間違えていてもコンパイラは教えてくれません。他にもこんな例も考えられます。
let title = piyo(id: user.id)
ToDo
のIDを渡さないといけない関数に、User
のIDを渡してしまいました。
動作確認時の異変ですぐに気づいて笑って済めばいいですが、こんなのが原因で個人情報の漏洩などに繋がれば洒落になりません。
(3) 不正な値を存在させない
「ToDoのタイトルは1文字以上30文字以下」という制限をつけたくなったとします。
改善後のコードの場合は、値オブジェクトにバリデーション処理を書くだけです。
struct ToDoTitle {
let value: String
init(_ value: String) throws {
+ guard case 1...30 = value.count else { /* エラーを投げる */ }
self.value = value
}
}
これによって、ToDoTitle
は1〜30文字の範囲外の不正な値を取ることが絶対にないと保証できます。そもそも不正な値が存在できないからです。
対して、改善前のコードはどうでしょうか?
Stringである以上、不正な値を取らないと断言するのは難しいです。 ToDo
内部のバリデーションに抜かりがあったら? 今は抜かりなくても、タイトルの変更を含む新規メソッドを追加した際にうっかりバリデーション処理を書き忘れる、なんてことも考えられます。
(4) ロジックの分散を防ぐ
もし改善前のコードにバリデーション処理を書くとなると、こうなります。
final class ToDo {
let id: String
private(set) var title: String
init(id: String, title: String) throws {
+ guard case 1...30 = title.count else { /* エラーを投げる */ }
self.id = id
self.title = title
}
func changeTitle(_ title: String) throws {
+ guard case 1...30 = title.count else { /* エラーを投げる */ }
self.title = title
}
}
バリデーションのロジックが2箇所に散在してしまいます。まだ2箇所なのでマシですが、Modelの規模が大きくなると大変なことになりそうだということが容易に想像できます。
このようなロジックの分散を値オブジェクトにまとめることで、変更に強い柔軟なコードが出来上がります。
また、今回作成した値オブジェクトにはinit
以外のメソッドを書いていませんが、振る舞いを持たせられるのも値オブジェクトの強みです。
まとめ
プリミティブ型は、あまりに汎用的で表現力が乏しいのが特徴です。
前述の強みが活かせる場面では、積極的に値オブジェクトを採用していこうと思います。
参考