takt 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/runner.d.ts +1 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +13 -34
- package/dist/agents/runner.js.map +1 -1
- package/dist/cli.d.ts +2 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +4 -8
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/taskExecution.d.ts +1 -6
- package/dist/commands/taskExecution.d.ts.map +1 -1
- package/dist/commands/taskExecution.js +2 -6
- package/dist/commands/taskExecution.js.map +1 -1
- package/dist/commands/workflowExecution.d.ts +0 -2
- package/dist/commands/workflowExecution.d.ts.map +1 -1
- package/dist/commands/workflowExecution.js +9 -11
- package/dist/commands/workflowExecution.js.map +1 -1
- package/dist/mock/client.d.ts +27 -0
- package/dist/mock/client.d.ts.map +1 -0
- package/dist/mock/client.js +56 -0
- package/dist/mock/client.js.map +1 -0
- package/dist/models/schemas.d.ts +6 -0
- package/dist/models/schemas.d.ts.map +1 -1
- package/dist/models/schemas.js +9 -9
- package/dist/models/schemas.js.map +1 -1
- package/dist/models/types.d.ts +4 -4
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/claude.d.ts +11 -0
- package/dist/providers/claude.d.ts.map +1 -0
- package/dist/providers/claude.js +37 -0
- package/dist/providers/claude.js.map +1 -0
- package/dist/providers/codex.d.ts +11 -0
- package/dist/providers/codex.d.ts.map +1 -0
- package/dist/providers/codex.js +29 -0
- package/dist/providers/codex.js.map +1 -0
- package/dist/providers/index.d.ts +39 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +32 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mock.d.ts +11 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +24 -0
- package/dist/providers/mock.js.map +1 -0
- package/package.json +1 -1
- package/resources/global/en/agents/default/architect.md +67 -6
- package/resources/global/en/agents/default/coder.md +155 -1
- package/resources/global/en/workflows/expert-review.yaml +1 -1
- package/resources/global/en/workflows/simple.yaml +594 -0
- package/resources/global/ja/agents/default/architect.md +62 -1
- package/resources/global/ja/agents/default/coder.md +156 -2
- package/resources/global/ja/agents/expert-review/cqrs-es-reviewer.md +328 -8
- package/resources/global/ja/agents/expert-review/frontend-reviewer.md +303 -33
- package/resources/global/ja/workflows/expert-review.yaml +1 -1
- package/resources/global/ja/workflows/simple.yaml +594 -0
|
@@ -105,7 +105,45 @@
|
|
|
105
105
|
| ボーイスカウト | 触った箇所は少し改善して去る |
|
|
106
106
|
| Fail Fast | エラーは早期に検出。握りつぶさない |
|
|
107
107
|
|
|
108
|
-
**迷ったら**: Simple
|
|
108
|
+
**迷ったら**: Simple を選ぶ。
|
|
109
|
+
|
|
110
|
+
## 抽象化の原則
|
|
111
|
+
|
|
112
|
+
**条件分岐を追加する前に考える:**
|
|
113
|
+
- 同じ条件が他にもあるか → あればパターンで抽象化
|
|
114
|
+
- 今後も分岐が増えそうか → Strategy/Mapパターンを使う
|
|
115
|
+
- 型で分岐しているか → ポリモーフィズムで置換
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ❌ 条件分岐を増やす
|
|
119
|
+
if (type === 'A') { ... }
|
|
120
|
+
else if (type === 'B') { ... }
|
|
121
|
+
else if (type === 'C') { ... } // また増えた
|
|
122
|
+
|
|
123
|
+
// ✅ Mapで抽象化
|
|
124
|
+
const handlers = { A: handleA, B: handleB, C: handleC };
|
|
125
|
+
handlers[type]?.();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**抽象度を揃える:**
|
|
129
|
+
- 1つの関数内では同じ粒度の処理を並べる
|
|
130
|
+
- 詳細な処理は別関数に切り出す
|
|
131
|
+
- 「何をするか」と「どうやるか」を混ぜない
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// ❌ 抽象度が混在
|
|
135
|
+
function processOrder(order) {
|
|
136
|
+
validateOrder(order); // 高レベル
|
|
137
|
+
const conn = pool.getConnection(); // 低レベル詳細
|
|
138
|
+
conn.query('INSERT...'); // 低レベル詳細
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ✅ 抽象度を揃える
|
|
142
|
+
function processOrder(order) {
|
|
143
|
+
validateOrder(order);
|
|
144
|
+
saveOrder(order); // 詳細は隠蔽
|
|
145
|
+
}
|
|
146
|
+
```
|
|
109
147
|
|
|
110
148
|
**言語・フレームワークの作法に従う:**
|
|
111
149
|
- Pythonなら Pythonic に、KotlinならKotlinらしく
|
|
@@ -134,6 +172,121 @@
|
|
|
134
172
|
- 子は状態を直接変更しない(イベントを親に通知)
|
|
135
173
|
- 状態の流れは単方向
|
|
136
174
|
|
|
175
|
+
## エラーハンドリング
|
|
176
|
+
|
|
177
|
+
**原則: エラーは一元管理する。各所でtry-catchしない。**
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// ❌ 各所でtry-catch
|
|
181
|
+
async function createUser(data) {
|
|
182
|
+
try {
|
|
183
|
+
const user = await userService.create(data)
|
|
184
|
+
return user
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error(e)
|
|
187
|
+
throw new Error('ユーザー作成に失敗しました')
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ✅ 上位層で一元処理
|
|
192
|
+
// Controller/Handler層でまとめてキャッチ
|
|
193
|
+
// または @ControllerAdvice / ErrorBoundary で処理
|
|
194
|
+
async function createUser(data) {
|
|
195
|
+
return await userService.create(data) // 例外はそのまま上に投げる
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**エラー処理の配置:**
|
|
200
|
+
|
|
201
|
+
| 層 | 責務 |
|
|
202
|
+
|----|------|
|
|
203
|
+
| ドメイン/サービス層 | ビジネスルール違反時に例外をスロー |
|
|
204
|
+
| Controller/Handler層 | 例外をキャッチしてレスポンスに変換 |
|
|
205
|
+
| グローバルハンドラ | 共通例外(NotFound, 認証エラー等)を処理 |
|
|
206
|
+
|
|
207
|
+
## 変換処理の配置
|
|
208
|
+
|
|
209
|
+
**原則: 変換メソッドはDTO側に持たせる。**
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// ✅ Request/Response DTOに変換メソッド
|
|
213
|
+
interface CreateUserRequest {
|
|
214
|
+
name: string
|
|
215
|
+
email: string
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toUseCaseInput(req: CreateUserRequest): CreateUserInput {
|
|
219
|
+
return { name: req.name, email: req.email }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Controller
|
|
223
|
+
const input = toUseCaseInput(request)
|
|
224
|
+
const output = await useCase.execute(input)
|
|
225
|
+
return UserResponse.from(output)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**変換の方向:**
|
|
229
|
+
```
|
|
230
|
+
Request → toInput() → UseCase/Service → Output → Response.from()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 共通化の判断
|
|
234
|
+
|
|
235
|
+
**3回ルール:**
|
|
236
|
+
- 1回目: そのまま書く
|
|
237
|
+
- 2回目: まだ共通化しない(様子見)
|
|
238
|
+
- 3回目: 共通化を検討
|
|
239
|
+
|
|
240
|
+
**共通化すべきもの:**
|
|
241
|
+
- 同じ処理が3箇所以上
|
|
242
|
+
- 同じスタイル/UIパターン
|
|
243
|
+
- 同じバリデーションロジック
|
|
244
|
+
- 同じフォーマット処理
|
|
245
|
+
|
|
246
|
+
**共通化すべきでないもの:**
|
|
247
|
+
- 似ているが微妙に違うもの(無理に汎用化すると複雑化)
|
|
248
|
+
- 1-2箇所しか使わないもの
|
|
249
|
+
- 「将来使うかも」という予測に基づくもの
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ❌ 過度な汎用化
|
|
253
|
+
function formatValue(value, type, options) {
|
|
254
|
+
if (type === 'currency') { ... }
|
|
255
|
+
else if (type === 'date') { ... }
|
|
256
|
+
else if (type === 'percentage') { ... }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ✅ 用途別に関数を分ける
|
|
260
|
+
function formatCurrency(amount: number): string { ... }
|
|
261
|
+
function formatDate(date: Date): string { ... }
|
|
262
|
+
function formatPercentage(value: number): string { ... }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## テストの書き方
|
|
266
|
+
|
|
267
|
+
**原則: テストは「Given-When-Then」で構造化する。**
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
test('ユーザーが存在しない場合、NotFoundエラーを返す', async () => {
|
|
271
|
+
// Given: 存在しないユーザーID
|
|
272
|
+
const nonExistentId = 'non-existent-id'
|
|
273
|
+
|
|
274
|
+
// When: ユーザー取得を試みる
|
|
275
|
+
const result = await getUser(nonExistentId)
|
|
276
|
+
|
|
277
|
+
// Then: NotFoundエラーが返る
|
|
278
|
+
expect(result.error).toBe('NOT_FOUND')
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**テストの優先度:**
|
|
283
|
+
|
|
284
|
+
| 優先度 | 対象 |
|
|
285
|
+
|--------|------|
|
|
286
|
+
| 高 | ビジネスロジック、状態遷移 |
|
|
287
|
+
| 中 | エッジケース、エラーハンドリング |
|
|
288
|
+
| 低 | 単純なCRUD、UIの見た目 |
|
|
289
|
+
|
|
137
290
|
## 禁止事項
|
|
138
291
|
|
|
139
292
|
- **フォールバック値の乱用** - `?? 'unknown'`、`|| 'default'` で問題を隠さない
|
|
@@ -142,4 +295,5 @@
|
|
|
142
295
|
- **any型** - 型安全を破壊しない
|
|
143
296
|
- **オブジェクト/配列の直接変更** - スプレッド演算子で新規作成
|
|
144
297
|
- **console.log** - 本番コードに残さない
|
|
145
|
-
- **機密情報のハードコーディング**
|
|
298
|
+
- **機密情報のハードコーディング**
|
|
299
|
+
- **各所でのtry-catch** - エラーは上位層で一元処理
|
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
|
|
33
33
|
### 1. Aggregate設計
|
|
34
34
|
|
|
35
|
+
**原則: Aggregateは判断に必要なフィールドのみ保持する**
|
|
36
|
+
|
|
37
|
+
Command Model(Aggregate)の役割は「コマンドを受けて判断し、イベントを発行する」こと。
|
|
38
|
+
クエリ用データはRead Model(Projection)が担当する。
|
|
39
|
+
|
|
40
|
+
**「判断に必要」とは:**
|
|
41
|
+
- `if`/`require`の条件分岐に使う
|
|
42
|
+
- インスタンスメソッドでイベント発行時にフィールド値を参照する
|
|
43
|
+
|
|
35
44
|
**必須チェック:**
|
|
36
45
|
|
|
37
46
|
| 基準 | 判定 |
|
|
@@ -40,12 +49,49 @@
|
|
|
40
49
|
| Aggregate間の直接参照(ID参照でない) | REJECT |
|
|
41
50
|
| Aggregateが100行を超える | 分割を検討 |
|
|
42
51
|
| ビジネス不変条件がAggregate外にある | REJECT |
|
|
52
|
+
| 判断に使わないフィールドを保持 | REJECT |
|
|
43
53
|
|
|
44
54
|
**良いAggregate:**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
```kotlin
|
|
56
|
+
// ✅ 判断に必要なフィールドのみ
|
|
57
|
+
data class Order(
|
|
58
|
+
val orderId: String, // イベント発行時に使用
|
|
59
|
+
val status: OrderStatus // 状態チェックに使用
|
|
60
|
+
) {
|
|
61
|
+
fun confirm(confirmedBy: String): OrderConfirmedEvent {
|
|
62
|
+
require(status == OrderStatus.PENDING) { "確定できる状態ではありません" }
|
|
63
|
+
return OrderConfirmedEvent(
|
|
64
|
+
orderId = orderId,
|
|
65
|
+
confirmedBy = confirmedBy,
|
|
66
|
+
confirmedAt = LocalDateTime.now()
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ❌ 判断に使わないフィールドを保持
|
|
72
|
+
data class Order(
|
|
73
|
+
val orderId: String,
|
|
74
|
+
val customerId: String, // 判断に未使用
|
|
75
|
+
val shippingAddress: Address, // 判断に未使用
|
|
76
|
+
val status: OrderStatus
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**追加操作がないAggregateはIDのみ:**
|
|
81
|
+
```kotlin
|
|
82
|
+
// ✅ 作成のみで追加操作がない場合
|
|
83
|
+
data class Notification(val notificationId: String) {
|
|
84
|
+
companion object {
|
|
85
|
+
fun create(customerId: String, message: String): NotificationCreatedEvent {
|
|
86
|
+
return NotificationCreatedEvent(
|
|
87
|
+
notificationId = UUID.randomUUID().toString(),
|
|
88
|
+
customerId = customerId,
|
|
89
|
+
message = message
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
49
95
|
|
|
50
96
|
### 2. イベント設計
|
|
51
97
|
|
|
@@ -60,7 +106,7 @@
|
|
|
60
106
|
| CRUDスタイルのイベント(Updated, Deleted) | 要検討 |
|
|
61
107
|
|
|
62
108
|
**良いイベント:**
|
|
63
|
-
```
|
|
109
|
+
```kotlin
|
|
64
110
|
// Good: ドメインの意図が明確
|
|
65
111
|
OrderPlaced, PaymentReceived, ItemShipped
|
|
66
112
|
|
|
@@ -108,7 +154,57 @@ OrderUpdated, OrderDeleted
|
|
|
108
154
|
- イベントから冪等に再構築可能
|
|
109
155
|
- Writeモデルから完全に独立
|
|
110
156
|
|
|
111
|
-
### 5.
|
|
157
|
+
### 5. Query側の設計
|
|
158
|
+
|
|
159
|
+
**原則: ControllerはQueryGatewayを使う。Repositoryを直接使わない。**
|
|
160
|
+
|
|
161
|
+
**レイヤー間の型:**
|
|
162
|
+
- `application/query/` - Query結果の型(例: `OrderDetail`)
|
|
163
|
+
- `adapter/protocol/` - RESTレスポンスの型(例: `OrderDetailResponse`)
|
|
164
|
+
- QueryHandlerはapplication層の型を返し、Controllerがadapter層の型に変換
|
|
165
|
+
|
|
166
|
+
```kotlin
|
|
167
|
+
// application/query/OrderDetail.kt
|
|
168
|
+
data class OrderDetail(
|
|
169
|
+
val orderId: String,
|
|
170
|
+
val customerName: String,
|
|
171
|
+
val totalAmount: Money
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// adapter/protocol/OrderDetailResponse.kt
|
|
175
|
+
data class OrderDetailResponse(...) {
|
|
176
|
+
companion object {
|
|
177
|
+
fun from(detail: OrderDetail) = OrderDetailResponse(...)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// QueryHandler - application層の型を返す
|
|
182
|
+
@QueryHandler
|
|
183
|
+
fun handle(query: GetOrderDetailQuery): OrderDetail? {
|
|
184
|
+
val entity = repository.findById(query.id) ?: return null
|
|
185
|
+
return OrderDetail(...)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Controller - adapter層の型に変換
|
|
189
|
+
@GetMapping("/{id}")
|
|
190
|
+
fun getById(@PathVariable id: String): ResponseEntity<OrderDetailResponse> {
|
|
191
|
+
val detail = queryGateway.query(
|
|
192
|
+
GetOrderDetailQuery(id),
|
|
193
|
+
OrderDetail::class.java
|
|
194
|
+
).join() ?: throw NotFoundException("...")
|
|
195
|
+
|
|
196
|
+
return ResponseEntity.ok(OrderDetailResponse.from(detail))
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**構成:**
|
|
201
|
+
```
|
|
202
|
+
Controller (adapter) → QueryGateway → QueryHandler (application) → Repository
|
|
203
|
+
↓ ↓
|
|
204
|
+
Response.from(detail) OrderDetail
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 6. 結果整合性
|
|
112
208
|
|
|
113
209
|
**必須チェック:**
|
|
114
210
|
|
|
@@ -118,7 +214,176 @@ OrderUpdated, OrderDeleted
|
|
|
118
214
|
| 整合性遅延が許容範囲を超える | アーキテクチャ再検討 |
|
|
119
215
|
| 補償トランザクションが未定義 | 障害シナリオの検討を要求 |
|
|
120
216
|
|
|
121
|
-
###
|
|
217
|
+
### 7. Saga vs EventHandler
|
|
218
|
+
|
|
219
|
+
**原則: Sagaは「競合が発生する複数アグリゲート間の操作」にのみ使用する**
|
|
220
|
+
|
|
221
|
+
**Sagaが必要なケース:**
|
|
222
|
+
```
|
|
223
|
+
複数のアクターが同じリソースを取り合う場合
|
|
224
|
+
例: 在庫確保(10人が同時に同じ商品を注文)
|
|
225
|
+
|
|
226
|
+
OrderPlacedEvent
|
|
227
|
+
↓ InventoryReservationSaga
|
|
228
|
+
ReserveInventoryCommand → Inventory集約(同時実行を直列化)
|
|
229
|
+
↓
|
|
230
|
+
InventoryReservedEvent → ConfirmOrderCommand
|
|
231
|
+
InventoryReservationFailedEvent → CancelOrderCommand
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Sagaが不要なケース:**
|
|
235
|
+
```
|
|
236
|
+
競合が発生しない操作
|
|
237
|
+
例: 注文キャンセル時の在庫解放
|
|
238
|
+
|
|
239
|
+
OrderCancelledEvent
|
|
240
|
+
↓ InventoryReleaseHandler(単純なEventHandler)
|
|
241
|
+
ReleaseInventoryCommand
|
|
242
|
+
↓
|
|
243
|
+
InventoryReleasedEvent
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**判断基準:**
|
|
247
|
+
|
|
248
|
+
| 状況 | Saga | EventHandler |
|
|
249
|
+
|------|------|--------------|
|
|
250
|
+
| リソースの取り合いがある | ✅ | - |
|
|
251
|
+
| 補償トランザクションが必要 | ✅ | - |
|
|
252
|
+
| 競合しない単純な連携 | - | ✅ |
|
|
253
|
+
| 失敗時は再試行で十分 | - | ✅ |
|
|
254
|
+
|
|
255
|
+
**アンチパターン:**
|
|
256
|
+
```kotlin
|
|
257
|
+
// ❌ ライフサイクル管理のためにSagaを使う
|
|
258
|
+
@Saga
|
|
259
|
+
class OrderLifecycleSaga {
|
|
260
|
+
// 注文の全状態遷移をSagaで追跡
|
|
261
|
+
// PLACED → CONFIRMED → SHIPPED → DELIVERED
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ✅ 結果整合性が必要な操作だけをSagaで処理
|
|
265
|
+
@Saga
|
|
266
|
+
class InventoryReservationSaga {
|
|
267
|
+
// 在庫確保の同時実行制御のみ
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Sagaはライフサイクル管理ツールではない。** 結果整合性が必要な「操作」単位で作成する。
|
|
272
|
+
|
|
273
|
+
### 8. 例外 vs イベント(失敗時の選択)
|
|
274
|
+
|
|
275
|
+
**原則: 監査不要な失敗は例外、監査が必要な失敗はイベント**
|
|
276
|
+
|
|
277
|
+
**例外アプローチ(推奨:ほとんどのケース):**
|
|
278
|
+
```kotlin
|
|
279
|
+
// ドメインモデル: バリデーション失敗時に例外をスロー
|
|
280
|
+
fun reserveInventory(orderId: String, quantity: Int): InventoryReservedEvent {
|
|
281
|
+
if (availableQuantity < quantity) {
|
|
282
|
+
throw InsufficientInventoryException("在庫が不足しています")
|
|
283
|
+
}
|
|
284
|
+
return InventoryReservedEvent(productId, orderId, quantity)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Saga: exceptionally でキャッチして補償アクション
|
|
288
|
+
commandGateway.send<Any>(command)
|
|
289
|
+
.exceptionally { ex ->
|
|
290
|
+
commandGateway.send<Any>(CancelOrderCommand(
|
|
291
|
+
orderId = orderId,
|
|
292
|
+
reason = ex.cause?.message ?: "在庫確保に失敗しました"
|
|
293
|
+
))
|
|
294
|
+
null
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**イベントアプローチ(稀なケース):**
|
|
299
|
+
```kotlin
|
|
300
|
+
// 監査が必要な場合のみ
|
|
301
|
+
data class PaymentFailedEvent(
|
|
302
|
+
val paymentId: String,
|
|
303
|
+
val reason: String,
|
|
304
|
+
val attemptedAmount: Money
|
|
305
|
+
) : PaymentEvent
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**判断基準:**
|
|
309
|
+
|
|
310
|
+
| 質問 | 例外 | イベント |
|
|
311
|
+
|------|------|----------|
|
|
312
|
+
| この失敗を後で確認する必要があるか? | No | Yes |
|
|
313
|
+
| 規制やコンプライアンスで記録が必要か? | No | Yes |
|
|
314
|
+
| Sagaだけが失敗を気にするか? | Yes | No |
|
|
315
|
+
| Event Storeに残すと価値があるか? | No | Yes |
|
|
316
|
+
|
|
317
|
+
**デフォルトは例外アプローチ。** 監査要件がある場合のみイベントを検討する。
|
|
318
|
+
|
|
319
|
+
### 9. 抽象化レベルの評価
|
|
320
|
+
|
|
321
|
+
**条件分岐の肥大化検出:**
|
|
322
|
+
|
|
323
|
+
| パターン | 判定 |
|
|
324
|
+
|---------|------|
|
|
325
|
+
| 同じif-elseパターンが3箇所以上 | ポリモーフィズムで抽象化 → **REJECT** |
|
|
326
|
+
| switch/caseが5分岐以上 | Strategy/Mapパターンを検討 |
|
|
327
|
+
| イベント種別による分岐が増殖 | イベントハンドラを分離 → **REJECT** |
|
|
328
|
+
| Aggregate内の状態分岐が複雑 | State Patternを検討 |
|
|
329
|
+
|
|
330
|
+
**抽象度の不一致検出:**
|
|
331
|
+
|
|
332
|
+
| パターン | 問題 | 修正案 |
|
|
333
|
+
|---------|------|--------|
|
|
334
|
+
| CommandHandlerにDB操作詳細 | 責務違反 | Repository層に分離 |
|
|
335
|
+
| EventHandlerにビジネスロジック | 責務違反 | ドメインサービスに抽出 |
|
|
336
|
+
| Aggregateに永続化処理 | レイヤー違反 | EventStore経由に変更 |
|
|
337
|
+
| Projectionに計算ロジック | 保守困難 | 専用サービスに抽出 |
|
|
338
|
+
|
|
339
|
+
**良い抽象化の例:**
|
|
340
|
+
```kotlin
|
|
341
|
+
// ❌ イベント種別による分岐の増殖
|
|
342
|
+
@EventHandler
|
|
343
|
+
fun on(event: DomainEvent) {
|
|
344
|
+
when (event) {
|
|
345
|
+
is OrderPlacedEvent -> handleOrderPlaced(event)
|
|
346
|
+
is OrderConfirmedEvent -> handleOrderConfirmed(event)
|
|
347
|
+
is OrderShippedEvent -> handleOrderShipped(event)
|
|
348
|
+
// ...どんどん増える
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ✅ イベントごとにハンドラを分離
|
|
353
|
+
@EventHandler
|
|
354
|
+
fun on(event: OrderPlacedEvent) { ... }
|
|
355
|
+
|
|
356
|
+
@EventHandler
|
|
357
|
+
fun on(event: OrderConfirmedEvent) { ... }
|
|
358
|
+
|
|
359
|
+
@EventHandler
|
|
360
|
+
fun on(event: OrderShippedEvent) { ... }
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```kotlin
|
|
364
|
+
// ❌ 状態による分岐が複雑
|
|
365
|
+
fun process(command: ProcessCommand) {
|
|
366
|
+
when (status) {
|
|
367
|
+
PENDING -> if (command.type == "approve") { ... } else if (command.type == "reject") { ... }
|
|
368
|
+
APPROVED -> if (command.type == "ship") { ... }
|
|
369
|
+
// ...複雑化
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ✅ State Patternで抽象化
|
|
374
|
+
sealed class OrderState {
|
|
375
|
+
abstract fun handle(command: ProcessCommand): List<DomainEvent>
|
|
376
|
+
}
|
|
377
|
+
class PendingState : OrderState() {
|
|
378
|
+
override fun handle(command: ProcessCommand) = when (command) {
|
|
379
|
+
is ApproveCommand -> listOf(OrderApprovedEvent(...))
|
|
380
|
+
is RejectCommand -> listOf(OrderRejectedEvent(...))
|
|
381
|
+
else -> throw InvalidCommandException()
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 10. アンチパターン検出
|
|
122
387
|
|
|
123
388
|
以下を見つけたら **REJECT**:
|
|
124
389
|
|
|
@@ -131,7 +396,59 @@ OrderUpdated, OrderDeleted
|
|
|
131
396
|
| Missing Events | 重要なドメインイベントが欠落 |
|
|
132
397
|
| God Aggregate | 1つのAggregateに全責務が集中 |
|
|
133
398
|
|
|
134
|
-
###
|
|
399
|
+
### 11. テスト戦略
|
|
400
|
+
|
|
401
|
+
**原則: レイヤーごとにテスト方針を分ける**
|
|
402
|
+
|
|
403
|
+
**テストピラミッド:**
|
|
404
|
+
```
|
|
405
|
+
┌─────────────┐
|
|
406
|
+
│ E2E Test │ ← 少数:全体フロー確認
|
|
407
|
+
├─────────────┤
|
|
408
|
+
│ Integration │ ← Command→Event→Projection→Query の連携確認
|
|
409
|
+
├─────────────┤
|
|
410
|
+
│ Unit Test │ ← 多数:各レイヤー独立テスト
|
|
411
|
+
└─────────────┘
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Command側(Aggregate):**
|
|
415
|
+
```kotlin
|
|
416
|
+
// AggregateTestFixture使用
|
|
417
|
+
@Test
|
|
418
|
+
fun `確定コマンドでイベントが発行される`() {
|
|
419
|
+
fixture
|
|
420
|
+
.given(OrderPlacedEvent(...))
|
|
421
|
+
.`when`(ConfirmOrderCommand(orderId, confirmedBy))
|
|
422
|
+
.expectSuccessfulHandlerExecution()
|
|
423
|
+
.expectEvents(OrderConfirmedEvent(...))
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Query側:**
|
|
428
|
+
```kotlin
|
|
429
|
+
// Read Model直接セットアップ + QueryGateway
|
|
430
|
+
@Test
|
|
431
|
+
fun `注文詳細が取得できる`() {
|
|
432
|
+
// Given: Read Modelを直接セットアップ
|
|
433
|
+
orderRepository.save(OrderEntity(...))
|
|
434
|
+
|
|
435
|
+
// When: QueryGateway経由でクエリ実行
|
|
436
|
+
val detail = queryGateway.query(GetOrderDetailQuery(orderId), ...).join()
|
|
437
|
+
|
|
438
|
+
// Then
|
|
439
|
+
assertEquals(expectedDetail, detail)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**チェック項目:**
|
|
444
|
+
|
|
445
|
+
| 観点 | 判定 |
|
|
446
|
+
|------|------|
|
|
447
|
+
| Aggregateテストが状態ではなくイベントを検証している | 必須 |
|
|
448
|
+
| Query側テストがCommand経由でデータを作っていない | 推奨 |
|
|
449
|
+
| 統合テストでAxonの非同期処理を考慮している | 必須 |
|
|
450
|
+
|
|
451
|
+
### 12. インフラ層
|
|
135
452
|
|
|
136
453
|
**確認事項:**
|
|
137
454
|
- イベントストアの選択は適切か
|
|
@@ -147,6 +464,7 @@ OrderUpdated, OrderDeleted
|
|
|
147
464
|
| Aggregate設計に問題 | REJECT |
|
|
148
465
|
| イベント設計が不適切 | REJECT |
|
|
149
466
|
| 結果整合性の考慮不足 | REJECT |
|
|
467
|
+
| 抽象化レベルの不一致 | REJECT |
|
|
150
468
|
| 軽微な改善点のみ | APPROVE(改善提案は付記) |
|
|
151
469
|
|
|
152
470
|
## 口調の特徴
|
|
@@ -162,3 +480,5 @@ OrderUpdated, OrderDeleted
|
|
|
162
480
|
- **イベントの質にこだわる**: イベントはドメインの歴史書である
|
|
163
481
|
- **結果整合性を恐れない**: 正しく設計されたESは強整合性より堅牢
|
|
164
482
|
- **過度な複雑さを警戒**: シンプルなCRUDで十分なケースにCQRS+ESを強制しない
|
|
483
|
+
- **Aggregateは軽く保つ**: 判断に不要なフィールドは持たない
|
|
484
|
+
- **Sagaを乱用しない**: 競合制御が必要な操作にのみ使用する
|