From 2186f55be6e7e46e647bfda58c0dd19866ac10d4 Mon Sep 17 00:00:00 2001 From: sadnessOjisan Date: Thu, 8 Feb 2024 01:48:36 +0900 Subject: [PATCH] fix --- .../20240208-do-you-use-entity/index.md | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/contents/20240208-do-you-use-entity/index.md b/src/contents/20240208-do-you-use-entity/index.md index 38dd7b23..307c8a9b 100644 --- a/src/contents/20240208-do-you-use-entity/index.md +++ b/src/contents/20240208-do-you-use-entity/index.md @@ -9,7 +9,7 @@ isFavorite: false isProtect: false --- -定期的に DDD やらクリーンアーキテクチャなどを題材にした記事が盛り上がっているのを見ていると、いま長年の疑問を書けば答えてくれるのではと思って書いてみる。 +定期的に DDD やクリーンアーキテクチャなどを題材にした記事が盛り上がっているのを見ていると、いま長年の疑問を書けば答えてくれるのではと思って書いてみる。 何に困っているかというと、 - いわゆるレポジトリ層が持つ create/update 関数の引数は Entity で待ち受けるべきか、プレーンなオブジェクトで待ち受けるべきか分からない @@ -17,7 +17,7 @@ isProtect: false だ。 -OGP は最近ハマっているネギ油だ。自分の設計はだいたいこんな感じだ。 +OGP は最近ハマっているネギ油だ。汚い同心円は自分の設計に通ずるものがある。 ## 謝罪 @@ -34,7 +34,7 @@ OGP は最近ハマっているネギ油だ。自分の設計はだいたいこ ここでいうレイヤードアーキテクチャとは、クリーンアーキテクチャ本に出てくる同心円の図のアレを指した言葉だと思ってくれて良い。 - Entity が中心にあり、ビジネスルールを持つ。 -- その外側にユースケースやサービスがアプリケーションのロジックを持つ。 +- その外側のレイヤーがアプリケーションのロジックを持つ。 - その外側に HTTP 通信や DB との IO といった機能を持つ。 - 内側は外側の事情を知ってはいけない。 @@ -99,7 +99,7 @@ class Repository { Entity は同一性の比較のために id を必要とするからだ。 LastIDを取るとなると、複数ユーザーが同時に create userすることを考えて、ロックを取る仕組みを作ったり、単純に通信が1往復増えるので嬉しくない。 実際ISUCONでもここを潰すと予選突破できる年があった。 -なので auto incrementな場合はこの手法は使えない。 +なので auto incrementな場合はこの手法を使いたくない動機が生まれる。 一方で ID 生成に制限がないのであればアプリケーション側で UUID を作って、それをもとに Entity を作れば解決できると思う。 けど primary key を UUID にするのは容量・検索・ソート・パフォーマンスの面から問題がないとは言えないので使いたくない。 @@ -132,7 +132,7 @@ class Repository { こうすれば id は不要で、DBの id 生成にお任せできる。 ただ、こうすると今度はビジネスルールの適用が Entity 経由でできなくなる。 -きっとコードとしては、 +その検証をしたければそのロジックをどこかにおく必要があり、きっとコードとしては、 ```ts const userService = new UserService(new UserRepository(mysqlDriver)); @@ -141,18 +141,6 @@ router.post("users", (req, res) => { userService.save({ name, age }); }); -class UserSerivice { - private: _repo: UserRepository - - save({name, age}: UserInput){ - this._repo.save({name, age}) - } -} -``` - -といった感じになるだろうが、この設計に 20歳以上かどうかという検証をさせたいのであれば、おそらく - -```ts class UserSerivice { private: _repo: UserRepository @@ -174,8 +162,7 @@ class UserSerivice { これはこれでドメインモデル貧血症などと呼ばれたりしてよくないものとされている。 それに加えてEntityにロジックがあれば、生成と検証が必ずセットになるので、こういうロジックはEntityに書いておいたほうが良いだろう。 -ただ、TypeScript に限って話せば、そもそも脱クラスという設計もあって、そういう Entity をただの型定義として定義してしまう設計手法もあるので、絶対にないとは言い切れない設計ではある。 -後述する Opaque を使うことで生成と検証を必ずセットにできる。 +そして、Plain なオブジェクトを引数に取るべきかが分からなくなった。 ## データを更新するときにビジネスルールを適用する方法がわからない @@ -183,7 +170,7 @@ class UserSerivice { 例えばユーザー情報の更新となると、名前だけ・年齢だけというパターンがありえ、そもそもとして Entity のフィールドが全て埋まらない状況で保存しないといけなくなる。 こうなるとEntityは作れない。 -それに対する解決策の一つには、更新ということは更新対象のIDが分かっているということを使って、その ID で DBからデータをゲットすることだ。 +それに対する解決策の一つは、更新ということは更新対象のIDが分かっているということを使って、その ID で DBからデータをゲットすることだ。 そしてEntityを作り出し、更新対象を set し、もう一度DBに保存するとすると良い。 だがこれは当然のことながら DB に対するアクセスが1往復増えている。 なのでその余計な往復を避けたければ、Entityではなくプレーンなオブジェクトを使うべきとなる。 @@ -191,9 +178,9 @@ class UserSerivice { ## 自分なりの解決策 -いわゆるレイヤードアーキテクチャで実装した時にはいつも上記のことに頭を悩ましているが、これまでに自分がとった解決策も紹介しようと思う。 +いわゆるレイヤードアーキテクチャで実装した時にはいつも上記のことに頭を悩ましているが、これまでに自分がとった解決策もあるので紹介しようと思う。 -### サブクラス +### EntityのサブクラスとしてDTOもClassで定義する まずは自分があまり筋良くなかったなと反省しているやり方だ。 @@ -236,7 +223,9 @@ export class UserDTO extends UserBase {} このやり方をするとDTO の歯抜けなオブジェクトのために `?` 付きのフィールド定義が必要だったり、protectedやoverrideを付けないといけなかったりで、複雑性が増したと思う。 自分1人の開発ならアリとも思ったが、このコードを引き継いだ人はギョッとしそうだなと思ってしまい、使うのをやめてしまった。 -### Value Object +### Plain なオブジェクトとEntity でValue Objectを今日する + +こちらは Repository に Plain なオブジェクトを使う場合のやり方だ。 #### Class を使う場合 @@ -312,14 +301,14 @@ const toUserAge = (age: number) => { ### 細かいことは気にせず、自分と同僚とテストを信じる -自分は ValueObject 派だったのだが、ビジネスルールの移譲がこれだけで表現できないこともある。 +自分は PlainObject + ValueObject 派だったのだが、ビジネスルールの移譲がこれだけで表現できないこともある。 例えば、別のフィールドを参照したりする場合や状態遷移がある場合だ。 CtoCの投稿システムだと、下書き -> 審査中 -> 公開 or 差し戻し のような遷移に伴って、審査中にはレビュアーフィールドが必須になるなどするビジネスルールだと、ValueObject だけでこのルールを表現することは難しい。 (ユニオン型やEnumを駆使すればできないこともないとは思うが、TSやFP文化圏以外ではあまりやらない気がするので一般的な回答とは言えないと思う) -どうしても投稿状態ごとのswitch文の中で独自のロジックを適用させるようなコードをサービス層に書くことになると思う。 +Entity にルールを集約させていないのなら、どうしても投稿状態ごとのswitch文の中で独自のロジックを適用させるようなコードをサービス層に書くことになると思う。 つまりプレーンなオブジェクトを使っている環境で、ルールを適用させるのが難しくなる。 -ちなみにこの時は redux を持ち出して state machine を書き、getter も setter も redux の action として扱うみたいな設計をしたことがあるのだが、当然そんな変な工夫は、後から読む人からすると怒ると思う。(本当にごめんなさい) +ちなみにさっきの審査ロジックを実装した時は redux を持ち出して state machine を書き、getter も setter も redux の action として扱うみたいな設計をしたことがあるのだが、当然そんな変な工夫は、後から読む人からすると怒ると思う。(本当にごめんなさい) なので最近はもう「同一のビジネスルールを徹底して適用するためには〜」と考えるのをやめた。 ルールを書く場所が散らばろうが、Plain な Object を使いながら、お守り程度の Value Object を作りながら、全部 Service にロジックを書くことが増えた。 なるべくサービスにロジックを集約させてそのサービスを使い回し、お互いにレビューで指摘しあえば問題は軽減できる。 @@ -328,8 +317,9 @@ CtoCの投稿システムだと、下書き -> 審査中 -> 公開 or 差し戻 ## とはいえ教科書的にやれると嬉しい -なので皆さんはどのようにしているのか知りたい。 -とっちらかった文章になったので自分が知りたいことをまとめると、いわゆるレイヤードアーキテクチャで設計している場面で、ユーザーのリクエストをもとにデータを作成・更新するエンドポイントを開発する時、 +とはいえ妥協している自覚はあるので、正解があるなら知りたいとずっと思っていた。 +なので皆さんはどのようにしているのかが気になっている。 +とっちらかった文章になったので最後に自分が知りたいことをまとめると、いわゆるレイヤードアーキテクチャで設計している場面で、ユーザーのリクエストをもとにデータを作成・更新するエンドポイントを開発する時、 - ビジネスロジックはEntityに集約させているか - ビジネスロジックをEntityに集約させてそれをデータ作成・更新時に使うとなると、DBに対してパフォーマンス面でトレードオフが発生するが、どう解決しているか。