そのシステム要件にIDは必要か?
『システム要件をありのままに定義したい』エンジニアです。
みなさんはIDはどのように管理していますか?
- とりあえずRDBのAuto Incrementで連番にしている。
- DBテーブルの全てにサロゲートキーとしてIDを定義している。
などという人が多いのではないでしょうか?
ただ、IDをどう定義するかはシステムの振る舞いにも大きく影響しますし、つまりはユーザーへ届ける価値にも影響します。
分かりやすいところだと、例えば注文管理システムなどだとユーザーがIDで呼び合うことがあると思います。『130番の注文はもう配送した?』という会話は出来そうですが、『01ARZ3NDEKTSV4RRFFQ69G5FAVの注文はもう配送した?』は現実的ではないでしょう。
また、そもそもユーザーやPMがIDを認識していないケースもあります。この時にIDを使ったビジネスロジックを組むと、彼らの認識と異なる挙動になる恐れがあります。
本記事では、GitHubの一部の仕様を例にとって、どのように考えてIDを定義すれば良いのかをまとめてみます。
システム要件からIDの在り方を定義する version 1
さて、皆さんおなじみのGitHubですが、Accountの仕様の一部としてこういうものがあります。
- ユーザーはAccountを作る際にusernameを自分で設定できる。(私であればuryyyyyyy)
- ユーザーはusernameを使ったURLでAccountを問い合わせることができる。(例: https://github.com/uryyyyyyy )
- Accountは他のAccountをfollowすることができる。
さて、ここまでの仕様をそのままコードにするとこんな感じになります。
@JvmInline
value class Username(val value: String) {
companion object {
fun from(value: String): Username {
if (!value.isURLValid()) {
throw IllegalArgumentException("UsernameはURLとして有効な形式でなければなりません")
}
return Username(value)
}
}
}
data class Account(
val username: Username, // システム全体でユニーク。IDとみなせる
val displayName: String, // 重複可の表示名
// emailなど色々
)
// 誰が誰をfollowしているか
data class AccountFollow(
val username: Username,
val followedUsername: Username,
)
usernameはユーザーが自分で設定することができるが、URLに出来る形式で、かつURLにするという仕様のため他と被らないものでなければいけない。
ここまでの仕様ではusernameはシステム全体でユニークなので、これをAccountのID(=識別子)とみなして良いと思います。別途IDという値を用意しなくても、どのUserを指すのかが明確に分かるからです。
また、Accountが誰にfollowされているかもAccountFollowクラスにあるようにUsernameで定義できます。
ちなみにこのAccountFollowの情報もデータベースに保存することになると思いますが、上記の仕様ではこれにIDを用意する必要はないと思います。なぜならこの情報に対してIDが必要な仕様(例えばURLを持つなど)が無いからです。followを止めるときも、『誰が誰のフォローを止めたいか』が分かれば良いのでAccountの識別子だけで十分です。
システム要件からIDの在り方を定義する version 2
ただ、GitHubにはこのような仕様もあります。
- ユーザーはAccountのusernameを変更することができる。
ここで、上記の設計のままだと困ったことが起きます。
もしfollowerのいるAccountがusernameを変えた場合にどうなるのでしょうか?全てのAccountFollow情報を書き換えなくてはいけないでしょう。
follow情報だけならなんとかなるかもしれませんが、本来GitHubにはリポジトリやコミットなどあらゆるところにAccountが出てきますし、ユーザーからの問い合わせもusernameで識別しているはずです。これらを全て変えないといけないのは現実的ではないでしょう。
ここではじめて、『usernameとは別のIDというプロパティがあったほうが良さそう』という知見を得ることになります。それでは上記のコードを書き換えてみましょう。
@JvmInline
value class AccountId(val value: ???)
@JvmInline
value class Username(val value: String) {
companion object {
fun from(value: String): Username {
if (!value.isURLValid()) {
throw IllegalArgumentException("UsernameはURLとして有効な形式でなければなりません")
}
return Username(value)
}
}
}
data class Account(
val Id: AccountId, // システム全体でユニーク
val username: Username, // システム全体でユニーク
val displayName: String, // 重複可の表示名
// emailなど色々
) {
// usernameは変わることがある
fun changeUsername(newUsername: Username): User {
return this.copy(username = newUsername)
}
}
// 誰が誰をfollowしているか
data class AccountFollow(
val accountId: AccountId,
val followedAccountId: AccountId,
)
さて、ようやくIDというプロパティが定義されたのですが、では次にこのIDはどう採番されるべきでしょうか?
上記の仕様を見る限り、『定義は特にない』だと思います。なぜなら利用者から見えない値なので何でも良いからです。
強いて言えば、大量の新規Userが来ることが想定されるのであれば、シーケンシャルな番号だと採番処理が遅くなる、採番した値が衝突しやすいとこれまた遅くなりユーザー体験を損なう、などの理由からIDのフォーマットを定義しても良いでしょう。
自分であればこんな感じでドメインロジック上に定義します。DB側で採番させても良いのですが、その場合はAccount作成時に、IDが存在しないAccountインスタンスという不整合な状態が生まれうるためです。
@JvmInline
value class AccountId(val value: String) {
companion object {
fun generate(): AccountId {
// 現状は特に制約はないが、衝突を防ぐためにUUIDを利用する
return AccountId(UUID.randomUUID().toString())
}
}
}
以上のように、あくまでシステムが満たすべき要件を踏まえたうえで「IDは必要なのか?必要ならばどのような値が良いのか?」を考えると良いと思います。
IDは常に付けたほうが良いのでは?
中には『DBのテーブル全てにIDを付けるべき』などの声があるかもしれません。上記の例だと、『usernameの変更は出来ない』という仕様であってもUserはIDを持つべきとか、UserFollowにもIDを持っておくべきという主張です。
これは大きく2パターン考慮すべき事があると思っていて、『技術的な理由に過ぎないか』と『将来のシステム要件の変更を想定しているのか』です。
まず前者の技術的な理由、例えば使っているフレームワークの制約、データレイク側の要求、クエリの効率性などの観点からであれば、サロゲートキーでもってIDというプロパティを導入するのはアリだと思います。
ただし、この場合はビジネスロジックとは切り離して実装するのをオススメします。
なぜならシステム要件に関係ないからです。関係ない仕様がビジネスロジックに紛れ込んでしまうと、コードが不要に複雑化し、場合によってはユーザーの想定と違う挙動を生んでしまうかもしれません。
次に後者の『将来のシステム要件の変更を想定して』の場合ですが、こちらはチーム内で議論すると良いと思います。
あなたがもし「将来的にUserはusernameを変えたくなるかもしれない」と思ったのであれば、その仮説が妥当かどうかを話し合うことで、よりシステムや顧客ニーズへの理解が深まるでしょう。
いずれにしろ、IDという概念はソフトウェア上ではよく使う概念ではありますが、なぜIDを使うのか、は意識しておくべきだと思います。
Discussion