<!-- このファイルは KrileWorks が公開している Claude Code 用スキルです。
     出典: https://krileworks.com/agent-skills
     手元の .claude/skills/ に置くと、エージェントが Apex Stem の API を
     記憶ではなくこの記述に従って書くようになります。 -->

---
name: apex-architecture
description: >
  Use this skill when deciding where a piece of Apex logic belongs, or when creating any new
  Apex class in a project that follows the Handler + Usecase architecture. Covers the two-layer
  split (Handler = entry point, Usecase = business logic), the fixed Trigger.cls shape, the
  Layered Constructor Pattern (public production constructor + @TestVisible private DI
  constructor), directory layout (TriggerHandlers / BatchHandlers / FlowHandlers / Usecases /
  Constants / Blueprints / Utilities), naming rules including the 40-character Apex identifier
  limit, the Constants class convention, and how to judge whether two pieces of code should be
  merged. Fires on: Handler / Usecase / TriggerHandler / Trigger.cls / invoke() / @TestVisible /
  null-coalescing で依存を受ける / どこにクラスを置くか / ディレクトリ構成 / 命名規則 /
  クラス名が 40 文字 / Constants クラス / RecordTypeUtil / 共通化すべきか / 過剰分割 /
  新しい Usecase を作る / 新しいトリガーを作る。
  Salesforce Apex を Handler (入口) と Usecase (業務ロジック) の 2 層で書くための規約集。
  クラスの置き場所・命名・依存の受け方・共通化の判断基準まで含む。
---

# Apex アーキテクチャ (Handler + Usecase)

すべての Apex 処理を **Handler (エントリポイント) + Usecase (業務ロジック)** の 2 段構えで書く。
Laravel の Controller + Service に近い責務分離。

**このスキルが答える問い**: このロジックはどこに書くか / クラスをどこに置くか / 何という名前にするか /
依存をどう受けるか / この 2 つはまとめるべきか。

> 各 OSS の API そのもの (Scribe の書き方、MockEloquent の使い方など) はこのスキルの対象外。
> `apex-eloquent` / `apex-blueprint` / `apex-trace` / `apex-tools` を Skill ツールでロードする。

---

## 1. 2 層の責務

| 層 | 責務 | 例えるなら |
|---|---|---|
| **Handler** | エントリポイントから受け取った引数を解釈し、適切な Usecase を呼ぶ | Controller |
| **Usecase** | 単一の業務ロジックを実装する。コンストラクタで入力を受け `invoke()` のみ public | Service / Action |

🚨 **Handler にビジネスロジックを書かない。** Handler がやるのは「条件判定」と「Usecase 呼び出し」だけ。
Handler が 20 行を超えたら、たいていロジックが漏れ出している。

### Handler の種類

| 種類 | エントリ元 | 配置 |
|---|---|---|
| トリガーハンドラ | DML トリガー | `TriggerHandlers/{オブジェクト名}/` |
| バッチハンドラ | `Schedulable` / `Database.executeBatch` | `BatchHandlers/{オブジェクト名}/` |
| フローハンドラ | `@InvocableMethod` | `FlowHandlers/{オブジェクト名}/` |
| Queueable ハンドラ | `System.enqueueJob` | `QueueableHandlers/{オブジェクト名}/` |
| コントローラ | `@AuraEnabled` (LWC / Flow) | `Controllers/{オブジェクト名}/` |

LWC 経由の Usecase は戻り値を `Result` DTO に包む規約がある → Skill `apex-lwc-result`。

---

## 2. Trigger.cls の固定パターン

書き方を強制する (バラつき禁止)。ファイルは `force-app/main/default/triggers/` に置く。

```apex
trigger Opportunity on Opportunity(
  before insert,
  before update,
  before delete,
  after insert,
  after update,
  after delete,
  after undelete
) {
  (new TriggerOppHandler()).execute();
}
```

- **7 イベントすべて宣言する** (実際に処理しないものも含む)
- Handler を `(new XxxHandler()).execute()` の **1 行のみ** で呼ぶ
- trigger ファイルにロジックを書かない

### ⚠️ トリガー名にカスタムオブジェクトの `__c` をそのまま付けない

Apex 識別子は **`__` の連続を含めない** (Salesforce 予約)。デプロイが `Invalid character in identifier` で落ちる。

```apex
// ❌ デプロイ失敗
trigger SalesActivity__c on SalesActivity__c(...)

// ✅ オブジェクト名 + Trigger サフィックス
trigger SalesActivityTrigger on SalesActivity__c(...)
```

ファイル名も合わせる。標準オブジェクトは `trigger Opportunity on Opportunity(...)` のままでよい。

### Handler は必ず `extends TriggerHandler` する

`Trigger.isAfter && Trigger.isInsert` を手書きしていたり、new/old 比較を自前実装していたら
**ApexTools 未導入のサイン**。基底クラスの hook と `getUpdateRecordIdsWithChangedFields` を使えば車輪の再発明は不要。
フック一覧と変更検知ヘルパーの詳細は Skill `apex-tools`。

```apex
public with sharing class TriggerOppHandler extends TriggerHandler {
  protected override void afterInsert(Map<Id, SObject> newRecordsMap) {
    (new RecalcAccountRevenue(newRecordsMap.keySet())).invoke();
  }

  protected override void afterUpdate(Map<Id, SObject> newMap, Map<Id, SObject> oldMap) {
    // 特定フィールドが変わったレコードだけに絞る
    Set<Id> needIds = this.getUpdateRecordIdsWithChangedFields(new List<SObjectField>{
      Opportunity.Amount,
      Opportunity.StageName
    });
    (new RecalcAccountRevenue(needIds)).invoke();
  }
}
```

---

## 3. Usecase の標準パターン (Layered Constructor Pattern)

4 つの規約に厳格に従う。

1. ✅ **コンストラクタは 2 つ** — 本番用 (`public`) と テスト用 (`@TestVisible private`、依存 DI 対応)
2. ✅ **public は `invoke()` のみ** — 他はすべて `private`
3. ✅ **依存は null-coalescing で本番デフォルト** — `eloquent ?? new Eloquent()`
4. ✅ **`Trace` でライフサイクルを残す** — `start` / `skip` / `abort` / `finish` (詳細は Skill `apex-trace`)

```apex
public with sharing class CopyAccountIndustryToOpportunity {
  @TestVisible static final String LBL_FETCH = 'oppFetch';
  @TestVisible static final String LBL_UPDATE = 'oppUpdate';

  private final Set<Id> opportunityIds;
  private final IEloquent eloquent;
  private Trace t = Trace.of('商談に親取引先の業種をコピー');

  // 🚪 本番用。業務入力だけを受ける
  public CopyAccountIndustryToOpportunity(Set<Id> opportunityIds) {
    this(opportunityIds, null);
  }

  // 🧪 テスト用。依存を差し替えられる唯一の継ぎ目
  @TestVisible
  private CopyAccountIndustryToOpportunity(Set<Id> opportunityIds, IEloquent eloquent) {
    this.opportunityIds = opportunityIds;
    this.eloquent = eloquent ?? new Eloquent();   // null なら本番デフォルト
  }

  // 🎬 唯一の public メソッド
  public void invoke() {
    this.t.start();
    if (this.opportunityIds == null || this.opportunityIds.isEmpty()) {
      this.t.skip('対象の商談がないため終了');
      return;
    }
    // ... 業務ロジック ...
    this.t.finish('完了');
  }

  // ⬇️ 以下すべて private helper
}
```

### なぜこの形か

- **継ぎ目を 1 か所だけ開ける**。public コンストラクタに依存を並べると、呼び出し側 (Handler) が
  本番の組み立てまで知ることになる
- **生焼けオブジェクトを作らない**。「引数なしコンストラクタ + setter で後から注入」は、
  注入前のインスタンスが存在してしまう。null-coalescing なら常に完成した状態で生まれる
- Apex に DI コンテナは無いので、この形が実質的な代替になる

### 依存が複数あるとき

`IEloquent` だけならラベル多重化で 1 本にまとめる (`apex-eloquent` の `label()`)。
種類の違う依存 (Reader / Validator / Launcher など) はフィールドごとに分けて DI する。
各部品も `new AccountReader(new Eloquent())` のように生焼けを作らず、完成させて渡す。

---

## 4. 🔌 配線の禁欲 — 「全事実を再計算する」Usecase をどこに繋ぐか

フェーズ判定・ロールアップのように **配下の全事実を集計して 1 値を焼く** Usecase を、
関係する全トリガー入口に配線すると、**カスケード再入のたびに同じ集計 SOQL が乗算**される。

**判断順を守る。推測で減らさない。**

1. **まず正しさ** — 再計算を漏らさない。**安全側 (必要な入口すべてに配線) がデフォルト**
2. **次に計測** — ガバナ IT の実 SOQL 数で問題を可視化する (Skill `apex-trace`)
3. **数字が出た箇所だけ集約** — カスケードが自然に合流する 1 点に乗せる

⚠️ 合流点だけに相乗りさせる場合、**「上流の変更が必ず合流点を通る」という不変条件に依存する**。
なぜこの入口には配線しないのかを **Usecase の doc コメントに明示する**。
書かないと、後で「合流点を通らず関係事実だけ変わる」新要件が来たときに **沈黙して再計算を漏らす**。

---

## 5. ディレクトリ構造

```
force-app/main/default/classes/
├── TriggerHandlers/{オブジェクト名}/     ← 入口 (トリガー)
├── BatchHandlers/{オブジェクト名}/       ← 入口 (バッチ)
├── FlowHandlers/{オブジェクト名}/        ← 入口 (Flow)
├── QueueableHandlers/{オブジェクト名}/   ← 入口 (Queueable)
├── Controllers/{オブジェクト名}/         ← 入口 (LWC / @AuraEnabled)
├── Usecases/{オブジェクト名}/            ← 業務ロジック (全入口から共有)
├── Constants/{オブジェクト名}/           ← picklist 値・レコードタイプ名
├── Blueprints/Blueprints.cls             ← テストデータの基本構成を 1 クラスに集約
├── Utilities/                            ← SObject に紐づかない汎用処理
└── ApexEloquent/ ApexBlueprint/ ApexTrace/ ApexTools/   ← OSS (submodule)
```

- **Handler 種別ごとにトップレベル dir**、その配下に **対象 SObject 名 (lowerCamelCase)**
- **Usecase は Handler と置き場所を分ける**。1 つの Usecase を複数の入口から呼ぶため
- **テストは同階層の `tests/` サブディレクトリ**に `{元クラス名}_T.cls`

### SObject 横断 Usecase の置き場所

複数 SObject を更新する Usecase は **「主役の SObject」** (処理結果として最終的に値が焼き付く方) の dir に置く。
商談を集計して取引先に書き込むなら `Usecases/account/`。

---

## 6. 命名規則

| 種類 | 規則 | 例 |
|---|---|---|
| ディレクトリ (SObject) | lowerCamelCase (`__c` は省略可) | `opportunity/` |
| トリガーハンドラ | `Trigger{Object}Handler.cls` | `TriggerOppHandler.cls` |
| バッチハンドラ | `{機能名}Handler.cls` | `ResetWeeklyReportHandler.cls` |
| Usecase | **業務内容そのまま** (`Usecase` 接尾辞は任意) | `RecalcAccountRevenue.cls` |
| Blueprint | 単一 `Blueprints.cls` + `{obj}Basic()` | `Blueprints.oppBasic()` |
| テスト | `{元クラス名}_T.cls` | `RecalcAccountRevenue_T.cls` |

### `Usecase` 接尾辞を強制しない

「Usecase であること」は**ディレクトリ (`Usecases/`) が示す**ので、クラス名に付けなくてよい。
業務名がそのままクラス名になり読みやすく、後述の 40 字制限を 7 文字節約できる。

ただし **接尾辞やより具体的な名前を付けるべき場合**もある:
- 業務名だけだと曖昧 (`Cleanup.cls` → 何の cleanup か分からない)
- 同名の DTO や Constants と衝突する

判断軸: **「ディレクトリ + ファイル名」を見て何をするか即分かるか**。

### 🚨 Apex 識別子は 40 文字以内

テストクラスは `_T` で 2 文字使うので、**本体は 38 文字までを目安**にする。

| ❌ 40 字超 | ✅ 短縮 |
|---|---|
| `RecalculateAccountLatestActivityUsecase_T` (41) | `RecalcAccountLatestActivity_T` (29) |

**まず `Usecase` 接尾辞を外す** (7 文字)。足りなければ動詞を省略する
(`Recalculate`→`Recalc` / `Generate`→`Gen` / `Aggregate`→`Aggr` / `Initialize`→`Init`)。

---

## 7. Constants クラス

picklist 値・レコードタイプ DevName・固定文字列を集約する。配置は `Constants/{オブジェクト名}/`。

```apex
public with sharing class OppStageName {
  /**
   * 02.商談50％
   */
  public static final String NEGOTIATION_50 = '02.商談50％';

  /**
   * 99.失注
   */
  public static final String LOST = '99.失注';

  /** 商談中のステージかどうか */
  public static Boolean isInProgress(String stage) {
    return new List<String>{ NEGOTIATION_50, NEGOTIATION_80 }.contains(stage);
  }
}
```

🚨 **各定数の直上に `/** ... */` の doc コメントを必ず書く。**
LSP がこれを拾うので、利用箇所で `OppStageName.LOST` にカーソルを当てるだけで実値が hover 表示される。
**定義ジャンプ不要で実値が分かる**のがこの規約の目的。

**判定ヘルパー (`isXxx`) は「グループ概念」を置く場所。** 「商談中ステージ」のようなビジネス概念を
ここに集約すると、利用側がステージのリストをハードコードしないで済む。

### レコードタイプ ID はハードコードしない

```apex
Id storeRtId = RecordTypeUtil.getRecordTypeIdByDevName(Opportunity.class, OppRecordType.STORE);
```

`Utilities/RecordTypeUtil.cls` に置く。内部でキャッシュするのでトリガー / バッチでも使い回せる。

---

## 8. まとめる / 分ける の判断

### 共通化の基準は「同じ知識か」— 「同じコードか」ではない

判定は **「片方を直すとき、必ずもう片方も直すことになるか」**。

- ✅ 同じ理由で一緒に変わる → まとめる
- ❌ **たまたま今のコードが似ている** → まとめない (別々の理由で変わるので、いずれ分岐が生える)

🚨 **闇雲な共通化は、共通化を置いたクラス自体を不完全体にする。** 呼び出し元ごとの事情がフラグや
分岐としてそのクラスに溜まり、「何のクラスなのか」を誰も説明できなくなる。
`XxxUtil` / `Common` / `Helper` が肥大していたら、ほぼこれが起きている。**重複は分岐より安い。**

### ✂️ 分割しすぎの是正 — レバーを 2 本混ぜない

「似たデータを 2 回クエリしている」を **クラス統合で直そうとしない**。症状ごとに効くレバーが違う。

| 症状 | 効くレバー | やること |
|---|---|---|
| 同じオブジェクトを別々の場所で取得している | **クエリ発行点の集約** | 呼び出し元で 1 回取り `List<IEntry>` を下へ渡す |
| データの持ち回りが多く、常に一緒に動く | **粒度** | クラスをまとめる |

前者は **クラス構成を変えずに直る**。ここで統合すると責務の違うものが同居し、後で分岐が増える。

### 🔍 実装後の見直しプロンプト

まとまった実装が終わったらこの形で自己レビューする。「まとめられるものはあるか」とだけ問うと
**必ず何か見つけてきてしまう** (問いの形が答えを歪める) ので、分類と否定の余地を先に与える。

```
実装したコードを見直す。目的は「過剰分割の是正」であって、闇雲な共通化ではない。
以下を分けて報告する。混ぜない。

【A】クエリ発行点の重複
  同じオブジェクトを別々の場所で取得している箇所。
  → 直し方は「呼び出し元で1回取り IEntry を渡す」。クラス統合ではない。

【B】統合を検討すべきクラス
  次を両方満たすものだけ挙げる。片方でも欠けたら挙げない。
  1. 変更理由が同じ (片方を直すとき必ずもう片方も直す)
  2. 統合してもテストしたい分岐が増えない
  各候補に「統合して失うもの」を必ず併記する
  (テストの独立性 / モックの肥大 / 将来の分岐余地)

【C】このままでよいもの
  分かれているのが正しいと判断した箇所と、その理由。

制約:
- 「まとめられるものは無かった」は正当な結論。無理に候補を出さない
- 「共通化できる」は理由にならない。一緒に変わるかで判断する
- 変更を提案する前に、ガバナITの実SOQL数を根拠として出す
```

---

## 9. テストの棲み分け

| 種別 | 対象 | DB | 使うもの | 置き場所 |
|---|---|---|---|---|
| **単体** | Usecase 単体 | ❌ | `MockEloquent` + `MockEntry` | `Usecases/{obj}/tests/` |
| **結合** | Handler 経由 | ✅ 実 DML | `ApexBlueprint` | `{Handler}/tests/` |

- **ロジック分岐の網羅は単体で**。高速・隔離で書ける
- **結合は代表 1〜3 本のみ**。「実 DML を流すと Trigger を経由して Usecase が呼ばれるか」を見る
- その代表 1 本は **ガバナ IT** にする (バルクで SOQL/DML が乗算されないか)。詳細は Skill `apex-trace`

### 🚦 テストが赤くなった時の一次診断 (コードを触る前に必ずやる)

単体テストは DML を流さない = **Flow / トリガー / 入力規則が発火しない**。
この構造上の性質から、赤の出方が故障箇所を示す。

| 単体 | 結合 | 診断 | 最初の一手 |
|---|---|---|---|
| 🟢 | 🔴 | **コードは無傷。org 側の宣言的変更** (追加された Flow / 入力規則 / 必須項目) | ❌ コードを直さない。✅ 対象オブジェクトの Flow / ValidationRule / 項目を retrieve して差分確認 |
| 🔴 | — | ロジック自体の破壊 | 通常のデバッグ |
| 🔴 | 🔴 | スキーマ級の破壊的変更 (項目削除・型変更) | メタデータ差分から確認 |

🚨 **「単体緑 + 結合赤」でコードをいじって赤を消そうとするのは悪手。**
正常なロジックを org の一時的な状態に合わせて歪めることになる。
org 側の必須項目追加でテストデータが通らなくなった場合、直すのは **`Blueprints.xxxBasic()` の該当メソッド 1 つ**。
各テストにインラインでデータを継ぎ足さない。

### テストの命名と説明

- クラス: `{クラス名}_T.cls` / メソッド: `test{メソッド名}_When{条件}_Then{結果}`
- 説明は **`Trace.of(...)` の引数に集約**する。メソッド上に `// 正常系: ...` の独立コメントを書かない (重複)
- `Trace.of` のメッセージは **`正常系:` / `異常系:` / `エッジケース:`** のいずれかで始める
- 🚫 テストコードに **ブランチ名 / チケット番号を書かない**。変更理由はコミットメッセージ側に書く

---

## 深掘り (元ドキュメント)

- https://krileworks.com/document/ja/handler-usecase-architecture.md — 2 層に分ける理由、fflib との立て分け、Salesforce 公式パターンとの対応
- https://krileworks.com/document/ja/layered-constructor-pattern.md — 2 コンストラクタの構造と、他の DI 手法を採らない理由
- https://krileworks.com/document/ja/test-strategy.md — 単体 / 結合の責任分離とガバナ IT
