はじめに
DDD+CQRSの文脈でコレクション操作に関する記述をどこに格納すべきか悩んでます。
自分なりに整理してみたので投稿してみます。
ファーストクラスコレクションはオブジェクト指向エクササイズの言葉です。
アーキテクチャはオニオンアーキテクチャで考えています。
ドメインサービスは1インターフェース1関数1機能な、どこにも属せない関数の置き場と考えています。
アプリケーションサービスは業務知識外のアプリ固有の機能を置く場所と考えています。(ユースケースとか)
1.特定の条件にマッチした集約を抽出したい
例として以下のドメインで考えます。
[](
@startuml
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
@enduml
)
リポジトリで実装
[](
@startuml
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
interface Iユーザーリポジトリ {
+男でN歳以上のユーザー(int 指定歳): IList<ユーザー>
}
@enduml
)
特定の集約に関することなのでリポジトリに担当してもらいます。
けどリポジトリには永続化に関することだけに専念してもらいたい気もします。
ファーストクラスコレクションで実装
[](
@startuml
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
class ユーザー一覧 {
-一覧:List<ユーザー>
+男でN歳以上のユーザー(int 指定歳): IList<ユーザー>
}
ユーザー一覧 .down.> ユーザー
interface Iユーザーリポジトリ {
+一覧: ユーザー一覧
}
@enduml
)
ファーストクラスコレクションを用意してそこに操作を記述します。
ユーザーが大量に存在するとつらいかも。
ある程度絞った要素に対して追加の操作をする際には便利かも。
ドメインサービスで実装
[](
@startuml
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
interface Iユーザーリポジトリ {
+一覧: IList<ユーザー>
}
interface I男でN歳以上のユーザーを取得するサービス {
+取得(int 指定歳): IList<ユーザー>
}
class 男でN歳以上のユーザーを取得するサービス {
-ユーザーリポジトリ:Iユーザーリポジトリ
+男でN歳以上のユーザーを取得するサービス(Iユーザーリポジトリ)
+取得(int 指定歳): IList<ユーザー>
}
男でN歳以上のユーザーを取得するサービス .right.> Iユーザーリポジトリ
男でN歳以上のユーザーを取得するサービス --|> I男でN歳以上のユーザーを取得するサービス
@enduml
)
サービスとして独立させてしまいます。
実装は、リポジトリを介さず直接保存領域を操作するのでも良いかも。
アプリケーションサービスで実装
[](
@startuml
package ドメイン {
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
interface Iユーザーリポジトリ {
+一覧: IList<ユーザー>
}
}
package アプリケーション {
interface I男でN歳以上のユーザーを取得するサービス {
+取得(int 指定歳): IList<ユーザー>
}
class 男でN歳以上のユーザーを取得するサービス {
-ユーザーリポジトリ:Iユーザーリポジトリ
+男でN歳以上のユーザーを取得するサービス(Iユーザーリポジトリ)
+取得(int 指定歳): IList<ユーザー>
}
男でN歳以上のユーザーを取得するサービス .right.> Iユーザーリポジトリ
男でN歳以上のユーザーを取得するサービス --|> I男でN歳以上のユーザーを取得するサービス
}
@enduml
)
検索の条件なんて業務知識ではなく各ユースケースの都合と考えて、アプリケーションサービスに置いてしまいます。
2.特定のユースケースに特化した情報を抽出したい
[](
@startuml
package ドメイン {
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
}
package アプリケーション {
interface Iあのユースケースで使う情報DTO {
+ID:ユーザーID
+名前:名前
+性別種別:性別種別
}
}
@enduml
)
クエリ(CQRS的な)で実装
[](
@startuml
package ドメイン {
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
}
package アプリケーション {
interface Iあのユースケースで使う情報DTO {
+ID:ユーザーID
+名前:名前
+性別種別:性別種別
}
interface Iあのユースケースで使う情報を取得するクエリ {
+取得():IList
+男だけ取得():IList
+女だけ取得():IList
}
Iあのユースケースで使う情報DTO -[hidden]down- Iあのユースケースで使う情報を取得するクエリ
}
@enduml
)
ユースケース専用のDTOとクエリを用意してそこに条件を混ぜ込めます。
3.集約をまたいだ検索条件でマッチした集約を抽出したい
[](
@startuml
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
class 伝票ID {
-id:GUID
}
伝票ID --|> 値オブジェクト
class 金額 {
-_金額: int
}
金額 --|> 値オブジェクト
class 伝票 <<集約兼エンティティ>> {
-_id:伝票ID
-_金額:金額
-_ユーザーid:ユーザーID
}
伝票 .down.> 伝票ID
伝票 .down.> 金額
伝票 .down.> ユーザーID
@enduml
)
これが一番悩みます。集約またぎなのでリポジトリに実装するのは抵抗があります。
ドメインサービスで実装
[](
@startuml
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
class 伝票ID {
-id:GUID
}
伝票ID --|> 値オブジェクト
class 金額 {
-_金額: int
}
金額 --|> 値オブジェクト
class 伝票 <<集約兼エンティティ>> {
-_id:伝票ID
-_金額:金額
-_ユーザーid:ユーザーID
}
伝票 .down.> 伝票ID
伝票 .down.> 金額
伝票 .down.> ユーザーID
interface Iユーザーリポジトリ {
+一覧: IList<ユーザー>
}
interface I伝票リポジトリ {
+一覧: IList<伝票>
}
interface I金額がN以上でN歳以上のユーザーを取得するサービス {
+取得(金額 指定金額, int 指定歳):IList<ユーザー>
}
class 金額がN以上でN歳以上のユーザーを取得するサービス {
-ユーザーリポジトリ:Iユーザーリポジトリ
-伝票リポジトリ:I伝票リポジトリ
+金額がN以上でN歳以上のユーザーを取得するサービス(Iユーザーリポジトリ, I伝票リポジトリ)
+取得(金額 指定金額, int 指定歳):IList<ユーザー>
}
値オブジェクト -[hidden]down- 金額がN以上でN歳以上のユーザーを取得するサービス
金額がN以上でN歳以上のユーザーを取得するサービス -down-|> I金額がN以上でN歳以上のユーザーを取得するサービス
金額がN以上でN歳以上のユーザーを取得するサービス .left.> Iユーザーリポジトリ
金額がN以上でN歳以上のユーザーを取得するサービス .left.> I伝票リポジトリ
@enduml
)
実装は、リポジトリを介さず直接保存領域を操作するのでも良いかも。
アプリケーションサービスで実装
[](
@startuml
package ドメイン {
abstract 値オブジェクト {
}
class 名前 {
-_名前:string
}
名前 --|> 値オブジェクト
enum 性別 {
男
女
}
class 性別種別 {
-_性別:性別
}
性別種別 .left.> 性別
性別種別 --|> 値オブジェクト
class 生年月日 {
-_生年月日:Date
+年齢(Date 基準日):int
+現在時点の年齢:int
}
生年月日 --|> 値オブジェクト
class ユーザーID {
-_id:GUID
}
ユーザーID --|> 値オブジェクト
class ユーザー <<集約兼エンティティ>> {
-_id:ユーザーID
-_性別種別:性別種別
-_名前:名前
-_生年月日:生年月日
}
ユーザー .down.> ユーザーID
ユーザー .down.> 生年月日
ユーザー .down.> 名前
ユーザー .down.> 性別種別
class 伝票ID {
-id:GUID
}
伝票ID --|> 値オブジェクト
class 金額 {
-_金額: int
}
金額 --|> 値オブジェクト
class 伝票 <<集約兼エンティティ>> {
-_id:伝票ID
-_金額:金額
-_ユーザーid:ユーザーID
}
伝票 .down.> 伝票ID
伝票 .down.> 金額
伝票 .down.> ユーザーID
interface Iユーザーリポジトリ {
+一覧: IList<ユーザー>
}
interface I伝票リポジトリ {
+一覧: IList<伝票>
}
}
package アプリケーション {
interface I金額がN以上でN歳以上のユーザーを取得するサービス {
+取得(金額 指定金額, int 指定歳):IList<ユーザー>
}
class 金額がN以上でN歳以上のユーザーを取得するサービス {
-ユーザーリポジトリ:Iユーザーリポジトリ
-伝票リポジトリ:I伝票リポジトリ
+金額がN以上でN歳以上のユーザーを取得するサービス(Iユーザーリポジトリ, I伝票リポジトリ)
+取得(金額 指定金額, int 指定歳):IList<ユーザー>
}
伝票 -[hidden]right- 金額がN以上でN歳以上のユーザーを取得するサービス
金額がN以上でN歳以上のユーザーを取得するサービス -down-|> I金額がN以上でN歳以上のユーザーを取得するサービス
金額がN以上でN歳以上のユーザーを取得するサービス .left.> Iユーザーリポジトリ
金額がN以上でN歳以上のユーザーを取得するサービス .left.> I伝票リポジトリ
}
@enduml
)
検索の条件なんて業務知識ではなく各ユースケースの都合と考えて、アプリケーションサービスに置いてしまいます。
集約を返す機能なのでクエリとするには抵抗があります。(CQRS的な)
まとめ
コレクション操作を記述する場所には
- リポジトリ
- ファーストクラスコレクション
- ドメインサービス
- アプリケーションサービス
- クエリ(CQRS的な)
がある気がします。
これは元々持っているコレクションをごにょごにょする話ではなくDB等保存領域から引っ張ってくる時の話です。
既に持っているコレクションに対してごにょごにょする場合は
- ファーストクラスコレクション
- ドメインサービス
- アプリケーションサービス
- その場でベタベタに記述
の選択肢がありそう。
public class ユーザー一覧 {
private List<ユーザー> 一覧 => new List<ユーザー>();
public ユーザー一覧(IList<ユーザー> _一覧) => 一覧.AddRange(_一覧);
public IList<ユーザー> ごにょごにょする() => ごにょごにょ;
}
public interface ごにょごにょするサービス {
public IList<ユーザー> ごにょごにょする(IList<ユーザー> _一覧);
}
コレクション操作を記述する場所はどこかに統一したい気持ちになりますが、そうするとドメインサービスかアプリケーションサービスしかないわけで。
そうなると大量にサービスが発生してしまい、把握できなくなって似たような検索条件のサービスが出来上がりそう。
結局ケースバイケースとか状況に応じてとしか言えないのでしょうが、少なくともコレクション操作の置き場所の候補の整理には役に立つかも。
ずれた話をしていたらすみません。
他にも良い置き場所があればご教示願います。