Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 36 additions & 38 deletions BDR_PATTERN-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ BDRパターンは重要なことを達成します:**SQL基盤による真の
- **明確な意図** - クエリ(データ読み取り)とコマンド(データ変更)の分離

```php
// ドメインオブジェクトでのDIの力を活用
// 読み取り側の問いにDIの力を活用
final readonly class UserDomainObject
{
public function __construct(
Expand All @@ -125,7 +125,7 @@ final readonly class UserDomainObject
private PermissionService $permissionService,
) {}

// 注入されたサービスによる動的なビジネスルール
// 注入されたサービスによる読み取り側の問い
public function canEdit(Document $document): bool
{
// ORMエンティティでは不可能 - 外部サービスに依存
Expand All @@ -139,7 +139,7 @@ final readonly class UserDomainObject
}
```

BDRパターンでは、オブジェクトは単なるデータコンテナではなく、ビジネスロジックを含むドメインオブジェクトです。これらはビジネスドメインに関する質問に答えますが、データベース自体を変更することはありません
BDRパターンでは、オブジェクトは単なるデータコンテナではなく、振る舞いを持つ読み取り側のドメインオブジェクトです。現在の projection やユーザー体験に関する問いに答えますが、状態変更前の最終判断は Command モデルが行います

## 実装ガイド

Expand Down Expand Up @@ -250,13 +250,13 @@ final readonly class OrderDomainObject
public float $tax, // 地域別計算済み税額
public float $shipping, // 計算済み送料
public float $total, // 総合計
public bool $canFulfill, // 適用済みビジネスルール
public bool $canFulfill, // 読み取り側ルールの結果
public array $insufficientStockItems, // 在庫不足商品リスト
// 注入されたビジネスルールエンジン - ORMでは不可能
// 注入された読み取り側ルールエンジン - ORMでは困難
private BusinessRuleEngine $ruleEngine,
) {}

// ドメインオブジェクトの振る舞い
// 読み取り側ドメインオブジェクトの振る舞い
public function getDisplayTotal(): string
{
return '$' . number_format($this->total, 2);
Expand All @@ -277,12 +277,12 @@ final readonly class OrderDomainObject
return $this->status === 'pending';
}

public function canProcess(): bool
public function canShowProcessAction(): bool
{
return $this->canFulfill && $this->isPending();
}

// 注入されたサービスによる動的なビジネスルール
// 注入されたサービスによる読み取り側の優先度分類
public function getBusinessPriority(): string
{
// ORMエンティティでは不可能 - 外部サービスに依存
Expand Down Expand Up @@ -315,7 +315,7 @@ BDRパターンの重要な利点の一つは、**テストがシンプルで信

1. **SQLクエリ**:入力に対して正しいデータを返すか?
2. **ファクトリー**:データを正しくドメインオブジェクトに変換するか?
3. **ドメインオブジェクト**:ビジネスルールを正しく実装しているか
3. **ドメインオブジェクト**:読み取り側の振る舞いを正しく実装しているか

これらが個別に正しければ、組み合わせは必然的に正しくなります。**論理的構造です。**

Expand Down Expand Up @@ -400,7 +400,7 @@ class OrderDomainObjectTest extends TestCase
// 振る舞いをテスト
$this->assertEquals('$2,660.00', $order->getDisplayTotal());
$this->assertEquals(8.0, $order->getTaxRate());
$this->assertTrue($order->canProcess());
$this->assertTrue($order->canShowProcessAction());
$this->assertFalse($order->hasInsufficientStock());
}
}
Expand Down Expand Up @@ -612,50 +612,48 @@ BDRパターンでは、それぞれが自身の領域で優秀さを発揮し

### Q: 変更されたオブジェクトをどうやってDBに書き戻す(保存する)のですか?

**A: 書き戻しません。** BDRパターンのオブジェクトは読み取り専用であり、データのクエリのために存在します。データを変更する必要がある場合:
**A: BDR の読み取りオブジェクトを保存するのではなく、明示的な書き込み経路を使います。** BDRパターンのオブジェクトは、画面、帳票、API レスポンス、ユースケースに合わせた読み取り側の projection です。オブジェクト自身は保存しません。

1. **アプリケーション層でビジネス判断を行う**
2. **コマンドを発行する** - 明確で明示的な書き込み操作
3. **シンプルな書き込みクエリを実行** - UPDATE、INSERT、DELETE文

これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の原則に従います:
1. 現在の状態や表示可能な操作を示すために、必要なら BDR の Query モデルを読む
2. 状態変更には Command またはアプリケーションの書き込みユースケースを呼ぶ
3. その書き込み経路で書き込み側の不変条件を検証し、UPDATE、INSERT、DELETE、または別の書き込み手段で結果を永続化する

```php
// クエリ側(BDRパターン)
// Query側(BDRパターン): 今の用途に必要なprojection
$order = $this->orderRepo->getOrder($id);
if ($order->canProcess()) {
// コマンド側(シンプルな書き込み)
$this->orderCommandRepo->markAsProcessed($id, new DateTime());
if ($order->canShowProcessAction()) {
// アプリケーションは操作を提示できる。ただし最終判断はCommand側が行う。
$this->processOrder->execute($id, new DateTimeImmutable());
}

// orderCommandRepoはシンプルなSQLを使用
// processOrder は必要に応じて明示的な書き込みSQLを使う
// UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id
```

この分離は意図的です:
- **クエリ**は複雑で、JOINや集約を含むことができる
- **コマンド**はシンプルで、状態変更に集中すべき
- **ドメインロジック**はクエリオブジェクトに存在し、データベース書き込みには存在しない
`canShowProcessAction()` は表示のための読み取り側の派生判断です。`ProcessOrder` は書き込み側の不変条件をあらためて検証しなければなりません。BDR は Command モデルを定義しません。Ray.MediaQuery は DML を実行できますが、それを書き込み手段として選ぶかどうかはアプリケーション側の設計です。

これは**CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)**の区別に従います:
- **Query モデル**は画面や帳票に合わせてデータを形作り、派生・表示の振る舞いを持つことができる
- **Command モデル**はドメインの整合性を守り、業務上の行為を実行してよいかを判断する
- **SQL** は projection に向いている。JOIN、集約、計算、非正規化された結果の形を自然に表せる

### Q: これはCQRSパターンですか?

**A: はい、特にクエリ(読み取り)側です。** BDRパターンはCQRSのクエリ側の強力な実装です。
**A: BDRパターンは CQRS の Query 側に適用できます。ただし、より正確には rich read model のパターンです。** Read Repository と Write Repository を別の場所に置く、という話が中心ではありません。重要なのは、関心とモデルを分けることです。

出発点は単純です。読み取りと書き込みでは、最適なモデルが違います。

書き込み側には、ドメインの整合性を守るモデルが必要です。そこでは意図、振る舞い、不変条件、失敗理由を扱います。「この業務上の行為は実行してよいか?」に答えるモデルです。

読み取り側では、画面、帳票、API レスポンスのために、非正規化されたフラットなデータが欲しいことがよくあります。「今表示するにはどんな形が役に立つか?」に答えるモデルです。同じ Repository や Entity モデルで両方を満たそうとするから無理が生じます。

CQRSは読み取りと書き込みの責務を分離します:
- **クエリ側(BDRパターン)**:ビジネスロジックを含む豊富なドメインオブジェクトによる複雑な読み取り
- **コマンド側**:状態を変更するシンプルで焦点を絞った書き込み
CQRS はしばしば、データベースを分ける、インフラを分ける、Repository の置き場所を分ける、といった物理的なアーキテクチャとして誤解されます。それらは有用な実装上の選択肢になることはありますが、本質ではありません。本質は、Command は業務の意思決定であり、Query は表示のための構造である、という分離です。

BDRパターンは複雑な部分(クエリ)を以下を組み合わせて処理します:
- データ取得のためのSQLの力
- 変換と充実化のためのファクトリー
- ビジネスロジックのためのドメインオブジェクト
BDR は薄い DTO に限定されません。読み取りモデルは、派生・表示のロジックである限り、振る舞いを持てます。合計、ラベル、表示可否、読み取り側の優先度分類など、現在の projection に関する問いに答える振る舞いです。状態変更の不変条件は Command 側に置きます。

一方、コマンド側はシンプルに保たれます:
- 直接的なUPDATE/INSERT/DELETE文
- イベントソーシング(必要な場合)
- 書き込み前のシンプルな検証
SQL はそもそもこの Query 側の性質を持っています。`SELECT` は JOIN、集約、計算を使って、必要な形へ結果を projection できます。その構造を永続的な正規ドメインモデルであるかのように扱う必要はありません。BDR では、SQL ファイルがその projection を定義し、ファクトリやドメインオブジェクトが PHP の型として表面を与えます。

この分離により、両側がよりシンプルで保守しやすくなります
Query モデルは使い捨てであってもかまいません。画面、帳票、API レスポンスが変われば、別の `SELECT` と小さな読み取りモデルを作ればよい。それは DRY 違反ではなく、CQRS の要点です。関心が違うものには、違うモデルを与えます

### Q: ファクトリーで外部APIを呼ぶと、リスト取得時に遅くなりませんか?

Expand Down
72 changes: 35 additions & 37 deletions BDR_PATTERN.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ final readonly class UserDomainObject
private PermissionService $permissionService,
) {}

// Dynamic business rules through injected service
// Read-side business questions through injected service
public function canEdit(Document $document): bool
{
// Impossible with ORM entities - depends on external service
// Difficult with ORM entities - depends on external service
// Test env: FakePermissionService (everyone can edit)
// Production: RealPermissionService (complex permission checks)
return $this->permissionService->canEdit($this, $document);
Expand All @@ -141,7 +141,7 @@ final readonly class UserDomainObject
}
```

In the BDR Pattern, objects are not mere data containers but domain objects containing business logic. They answer questions about the business domain but don't change the database themselves.
In the BDR Pattern, objects are not mere data containers but read-side domain objects with behavior. They answer questions about the current projection and user experience, but Command models still make the final decision before state changes.

## Implementation Guide

Expand Down Expand Up @@ -252,13 +252,13 @@ final readonly class OrderDomainObject
public float $tax, // Calculated by region
public float $shipping, // Calculated shipping
public float $total, // Complete total
public bool $canFulfill, // Business rule applied
public bool $canFulfill, // Read-side rule result
public array $insufficientStockItems, // List of insufficient stock items
// Injected business rule engine - impossible with ORM
private BusinessRuleEngine $ruleEngine,
) {}

// Domain object behavior
// Read-side domain object behavior
public function getDisplayTotal(): string
{
return '$' . number_format($this->total, 2);
Expand All @@ -279,15 +279,15 @@ final readonly class OrderDomainObject
return $this->status === 'pending';
}

public function canProcess(): bool
public function canShowProcessAction(): bool
{
return $this->canFulfill && $this->isPending();
}

// Dynamic business rules through injected service
// Read-side priority through injected service
public function getBusinessPriority(): string
{
// Impossible with ORM entities - depends on external service
// Difficult with ORM entities - depends on external service
// Test environment: Relaxed thresholds (e.g., high priority at $100+)
// Production: Strict thresholds (e.g., high priority at $10,000+)
// Peak season: Different thresholds
Expand Down Expand Up @@ -317,7 +317,7 @@ Because each layer is **independent**, if each is tested individually, the combi

1. **SQL Query**: Does it return correct data for the input?
2. **Factory**: Does it correctly transform data into domain objects?
3. **Domain Object**: Does it correctly implement business rules?
3. **Domain Object**: Does it correctly implement read-side behavior?

If these are individually correct, the combination is necessarily correct. **It's a logical structure.**

Expand Down Expand Up @@ -564,50 +564,48 @@ In the BDR Pattern, each excels in its own domain while building something great

### Q: How do I save modified objects back to the database?

**A: You don't.** Objects in the BDR Pattern are read-only and exist for querying data. When you need to modify data:
**A: Use an explicit write path, not the BDR read object.** A BDR object is a read-side projection shaped for a screen, report, API response, or use case. It does not save itself.

1. **Make business decisions** in your application layer
2. **Issue a Command** - a clear, explicit write operation
3. **Execute simple write queries** - UPDATE, INSERT, DELETE statements

This follows the **CQRS (Command Query Responsibility Segregation)** principle:
1. Read a BDR Query model when it helps present the current state or available actions.
2. Call a Command or application write use case for the state change.
3. In that write path, validate write-side invariants and persist the result with UPDATE, INSERT, DELETE, or another write mechanism.

```php
// Query side (BDR Pattern)
// Query side (BDR Pattern): projection for the current use
$order = $this->orderRepo->getOrder($id);
if ($order->canProcess()) {
// Command side (simple write)
$this->orderCommandRepo->markAsProcessed($id, new DateTime());
if ($order->canShowProcessAction()) {
// The application may offer the action, but the Command owns the final decision.
$this->processOrder->execute($id, new DateTimeImmutable());
}

// orderCommandRepo might use simple SQL:
// processOrder may use explicit write SQL:
// UPDATE orders SET status = 'processed', processed_at = :timestamp WHERE id = :id
```

The separation is intentional:
- **Queries** can be complex, with JOINs and aggregations
- **Commands** should be simple and focused on changing state
- **Domain logic** lives in the query objects, not in the database writes
`canShowProcessAction()` is read-side derivation for presentation. `ProcessOrder` must still enforce the write-side invariant. BDR does not define the Command model; Ray.MediaQuery can execute DML if that is the write mechanism you choose.

This follows the **CQRS (Command Query Responsibility Segregation)** distinction:
- **Query models** shape data for display or reporting and may contain derivation/presentation behavior
- **Command models** protect domain consistency and decide whether a business action may happen
- **SQL** is naturally good at projection: JOINs, aggregations, calculations, and denormalized result shapes

### Q: Is this the CQRS pattern?

**A: Yes, specifically the Query (read) side.** The BDR Pattern is a powerful implementation of CQRS's query side.
**A: BDR fits the Query side of CQRS, but more precisely it is a rich read-model pattern.** It is not mainly about placing read repositories and write repositories in different locations. It is about separating concerns and models.

The starting point is simple: reads and writes want different models.

The write side needs a domain model that protects consistency. It carries intent, behavior, invariants, and failure reasons. It answers, "May this business action happen?"

The read side often wants denormalized, flattened data for a screen, report, or API response. It answers, "What shape is useful to display now?" Trying to satisfy both with one Repository or Entity model creates friction.

CQRS separates read and write responsibilities:
- **Query side (BDR Pattern)**: Complex reads with rich domain objects containing business logic
- **Command side**: Simple, focused writes that change state
CQRS is often mistaken for a physical architecture: separate databases, separate infrastructure, separate repository locations. Those may be useful implementation choices, but they are not the essence. The essence is that Command is business decision, and Query is display structure.

The BDR Pattern handles the complex part (queries) by combining:
- SQL's power for data retrieval
- Factories for transformation and enrichment
- Domain objects for business logic
BDR is not limited to a thin DTO. Its read model can expose behavior, as long as that behavior is derivation or presentation logic: totals, labels, visibility, read-side priority, or other answers about the current projection. State-changing invariants stay on the Command side.

Meanwhile, the command side remains straightforward:
- Direct UPDATE/INSERT/DELETE statements
- Event sourcing (if needed)
- Simple validation before writes
SQL already has this Query-side character. A `SELECT` can join, aggregate, calculate, and project a result into the exact structure needed without pretending that structure is the canonical domain model. In BDR, the SQL file defines that projection, and the factory/domain object gives it a typed PHP surface.

This separation makes both sides simpler and more maintainable.
The Query model may be disposable. If a screen, report, or API response changes, write another `SELECT` and another small read model. That is not a DRY violation; it is the point of CQRS: different concerns get different models.

### Q: Won't calling external APIs in factories slow down list retrievals?

Expand Down
Loading
Loading