🆔

Id型の実装について [Dart]

2024/09/02に公開

Id型の必要性

アプリケーションを作る際によくあるクラスの実装として、クラスがidフィールドを持たせるのが一般的かと思います。DDDなどのコンテキストでは、そのようなクラスのことをEntityと呼んだりします。
idをクラスに持たせると、あるクラスから別のクラスを指すような、参照関係を表現できるますし、データが変更されたり、一旦永続化されて再度取得した場合などでも、クラスを追跡できるようになるため、idフィールドを持たせるというのは、設計上重要です。

さてidをクラスのフィールドして表現する場合、Stringやintといったプリミティブ型で表現する方法と、専用の型を作って、表現する方法があります。
私は基本的にId専用の型を作っています。Id専用の型を作ると言うことは、つまりUserのidを表現するクラスとしてUserIdクラスを作成するみたいな実装です。

ここまでするのは手間がかかるのですが、以下の理由から、idには専用の型があった方がいいという結論になったため、必ず型をつけるようにしています。

Idの混同を避けられる

Id型が必要な理由として最も重要なのは、Stringなどでidを表現すると、自分の扱っているidがどのEntityのidなのかわからなくなってしまうことがあるからです。
例えば、なんらかのメソッドがUserId, OrderIdを必要とする場合、誤ってUserIdとOrderIdを逆に渡してしまって、期待する結果にならないバグを引き起こしてしまうことがあります。実際にありました。
idはプログラミング際によく使うものなので、型で厳密に定義しておいた方が、後々のロジックのミスなどが少なくなるので、将来への投資と思ってId型は必ず定義するようにしてます。

生成等にロジックを持たせられる

またId型として表現を行う利点には、Idの生成ロジックを強制することができる点もメリットとしてあります。
例えば、OrderIdのfactoryメソッドにcreate()というものを用意することをよくやっています。
OrderIdはクライアント側でUuidで作成するみたいな仕様にしていた場合、

factory OrderId.create() {
  return OrderId(Uuid().v4());
}

のように書くことで、idの生成ロジックを表現できます。

Id型の実装

Id型の実装は今まではクラスを使って書いていました。クラスを使って書く場合、Jsonでシリアライズする場合などに、Converterを使ったりせねばならず、結構な手間でした。テンプレートを用意して、Idクラスを生成することで誤魔化していましたが、それでも記述量などが増えてしまい、億劫な感じでした。
しかし最近のDart言語ではextension typeという機能が導入されて、簡単にId型を表現できるようになりました。これまで書いていた、JsonConverter等も書かなくて良くなり、しかもString型を拡張するような形で、メソッド等も使えます。
よって最近ではextension typeでId型を表すやり方に移行しています。

書き方はこんな感じです。

extension type UserId(String value) implements String {
  factory UserId.create() {
    return UserId(Uuid().v4());
  }
}

Id型について気をつけること

Id型はEntityを識別するための型に徹するということが重要です。
Id型が識別以外の役割を持ち始めたら、それは単一責任原則に違反している可能性があります。
例えば、UserIdにユーザが決められるユーザー名を持たせる、OrderIdに注文の順序を持たせて表示の際のソートに使うなど、最初はちょっとしたことなのでと思ってIdにやらせると、Idの責務が肥大化してしまうことがあります。

Idが識別以外の役割をしそうになったら、コードを見直して、その役割を別の場所に書けないか考えてみると良いかと思います。

Discussion