Layered Constructor Pattern

Apex Stem ドキュメント
Apex StemHandler-Usecase ArchitectureDesign PatternTestingSalesforceApex
DI コンテナを使わずに、本番用のシンプルな API とテスト用の柔軟な依存注入を 1 つの Usecase クラスで両立する設計パターンを解説します。

このドキュメントは、Apex Stem の Usecase 層で繰り返し現れる設計パターン Layered Constructor Pattern を、独立したテーマとして掘り下げます。Handler-Usecase Architecture を読んで「2 つのコンストラクタって結局なに?」となった人向けです。

Layered Constructor Pattern は、Usecase 1 クラスの中に 本番用のシンプルな APIテスト用の柔軟な依存注入 を共存させる設計パターンです。DI コンテナを持たない Apex でも、生焼けオブジェクトを作らずにテスト容易性を確保するための定石として機能します。

Salesforce 公式ブログのサンプルコードも、これとまったく同じ形のコンストラクタを示しています (該当箇所)。独自の書き方ではありません。

Layered Constructor Pattern とは

ひとつの Usecase クラスに、2 つのコンストラクタを並べます。

  • public コンストラクタ: 本番用。業務に必要な入力 (ID、SObject、リクエスト DTO など) だけを受け取ります。
  • @TestVisible private コンストラクタ: テスト用。業務入力に加えて、データアクセスやヘルパー部品といった依存をすべて引数で受け取ります。

public コンストラクタは private コンストラクタに this(...) で委譲し、依存はすべて null を渡します。private コンストラクタ側で ?? new Eloquent() のような null-coalescing で本番デフォルトに差し替えれば、本番では依存が完全に隠れ、テストではモックを注入できます。

Apex Stem の Usecase 層では、この形が標準です。

なぜこの形が必要か

継ぎ目を、1 か所だけ開ける

このパターンの目的は「コンストラクタを 2 つ書くこと」ではありません。差し替えられる場所 (継ぎ目) を、オブジェクトが完成する 1 か所に集めることです。

private コンストラクタは、このクラスで唯一の初期化ロジックです。フィールドが埋まるのはここだけ、依存が決まるのもここだけ。だから、テストが介入する場所もここ 1 か所で済みます。

継ぎ目が 1 か所だと、次のことが同時に成り立ちます。

  • 本番の呼び出し側からは継ぎ目が見えないnew Xxx(ids) の 1 行で、いつも完成した状態が返る
  • テストは何も足さずに介入できる。setter を生やす、可視性を緩める、テスト用のフラグを足す — どれも要りません
  • どの経路で作っても不変条件が同じ。public から来ても private から来ても、通るコードは 1 本です

逆に、継ぎ目が散らばると (setter が 3 つ、初期化が 2 系統) 「どれを呼べば完成なのか」がクラスの外に漏れ出します。それを防ぐのがこの形です。

継ぎ目は、テストを本番より甘くしないためにある

差し替え可能にする目的は「テストを速くすること」だけではありません。テストを本番より甘くしないことも同じくらい重要です。

IEloquent をこの継ぎ目から MockEloquent に差し替えると、モックは本番のクエリ (Scribe) が何を SELECT しているかを知った状態でレコードを返します。SELECT していない項目に触れば、単体テストの段階で例外になります。自分で組み立てた SObject を直接渡していたら、null が返って静かに通り、本番で初めて落ちていた挙動です。

継ぎ目をここに置くから、モックが本番の契約を引き継げます。これは「テストのために設計を歪める」話ではなく、設計を正すとテストが本番に近づくという話です。

Apex には DI コンテナがない

Java の Spring や PHP の Laravel が提供するような、依存を自動配線してくれる DI コンテナは Apex の標準ライブラリにはありません。Inject 用のアノテーションも、コンストラクタの自動解決もありません。

ということは、依存をどう注入するかは 手動で設計するしかない わけです。Layered Constructor Pattern は、その手動 DI のひとつの解です。


「全部コンストラクタで受ける」は呼び出し側がつらい

愚直にやろうとすると、public コンストラクタひとつだけを置いて、業務入力も依存もすべてそこで受け取る形になります。

APEX
// アンチパターン: 本番でも依存をすべて組み立てる必要がある
new CreateOpportunityFromAccountUsecase(
  accountId,
  new AccountReader(new Eloquent()),
  new OpportunityEligibilityValidator(),
  new OpportunityMapper(),
  new Eloquent()
).invoke();

Trigger ハンドラから呼び出すたびにこれを書くのは現実的ではありません。本番コードのノイズが増えるだけでなく、本番デフォルトの定義が呼び出し側に散ります。デフォルトをひとつ変えたくなった時に、全呼び出し箇所を直すことになります。


「引数なしのコンストラクタ + setter で後から注入」は生焼けオブジェクトを作る

別のアプローチとして、引数のないコンストラクタでインスタンスを作ってから setter で依存を流し込む形が考えられます。

APEX
// アンチパターン: setter 注入は「生焼けオブジェクト」を作ってしまう
CopyAccountIndustryToOpportunityUsecase usecase
  = new CopyAccountIndustryToOpportunityUsecase();
usecase.setOpportunityIds(opportunityIds);
usecase.setFetchEloquent(new Eloquent());
usecase.invoke();

これは Handler-Usecase Architecture の核となる原則「生焼けオブジェクトを作らない」に反します。setter の呼び忘れによる NullPointerException が顕在化しにくく、「コンストラクタを呼んだ時点で完成している」というオブジェクトの整合性が保てません。


Layered Constructor Pattern が両方を解決する

Layered Constructor Pattern では、

  • 本番側は public コンストラクタで 業務入力だけ を渡せばよい
  • テスト側は private コンストラクタで 依存も含めて完全な状態 で組み上げる
  • どちらのコンストラクタも「呼び出した瞬間に完成している」状態を保つ

DI コンテナの代わりに、コンパイラと @TestVisible の組み合わせがその役割を担います。

構造

public コンストラクタと private コンストラクタの役割

コンストラクタ可視性受け取る引数役割
public コンストラクタpublic業務入力のみ本番からの呼び出し口。private コンストラクタに委譲する
private コンストラクタ@TestVisible private業務入力 + 全依存唯一のオブジェクト初期化ロジック。依存が null なら本番デフォルトに差し替え

「public コンストラクタは最小、private コンストラクタは完全」が原則です。本番の呼び出し側からは依存が見えず、テストからは依存をすべて差し替えられる、という両立がここで成立します。

委譲と null-coalescing

public コンストラクタは private コンストラクタに this(...) で委譲し、依存はすべて null を渡します。private コンストラクタ側で ?? new Eloquent() のような null-coalescing 演算子で本番デフォルトに差し替えます。

APEX
public CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds) {
  this(opportunityIds, null);  // 依存は null、private コンストラクタに委譲
}
 
@TestVisible
private CopyAccountIndustryToOpportunityUsecase(
  Set<Id> opportunityIds,
  IEloquent eloquent
) {
  this.opportunityIds = opportunityIds;
  this.eloquent = eloquent ?? new Eloquent();   // null なら本番デフォルト
}

呼び出し側から見ると、本番では new CopyAccountIndustryToOpportunityUsecase(ids) の 1 行で済み、テストでは new CopyAccountIndustryToOpportunityUsecase(ids, mock) で依存を差し替えられます。

例 1: 末端 Usecase (IEloquent を DI)

Apex Stem 導入ガイド のステップ 3 で扱った CopyAccountIndustryToOpportunityUsecase を、Layered Constructor Pattern の視点で見直します。

APEX
public with sharing class CopyAccountIndustryToOpportunityUsecase {
  @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 CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds) {
    this(opportunityIds, null);
  }
 
  @TestVisible
  private CopyAccountIndustryToOpportunityUsecase(
    Set<Id> opportunityIds,
    IEloquent eloquent
  ) {
    this.opportunityIds = opportunityIds;
    this.eloquent = eloquent ?? new Eloquent();
  }
 
  public void invoke() {
    // (導入ガイドのステップ 3 に全文)
  }
}

注目するポイントは 2 つです。

  • IEloquent 1 本をラベル多重化で用途別に分けている。取得用 (LBL_FETCH) と更新用 (LBL_UPDATE) を同じ IEloquent に対するラベルとして分けることで、テスト時に「取得は成功するが、更新だけ例外を投げる」のような独立シナリオを 1 本の MockEloquent で組めます。v2.1 で label() が登場する前は、2 本の IEloquent フィールドに分けて DI していました (それでも動きますが、コンストラクタが太ります)。
  • ?? new Eloquent() で本番デフォルト。本番の呼び出し側からは依存が完全に隠れ、new CopyAccountIndustryToOpportunityUsecase(ids) だけで動きます。

このように、データアクセスを抽象 (IEloquent) で受けて Layered Constructor Pattern で DI する形が、末端 Usecase の基本形です。IEloquent 単体の Usecase なら、v2.1 以降は ラベル多重化で 1 本にまとめる のが推奨です。異なる種類の依存 (IEloquent + Reader + Validator + Mapper 等) を分ける場面は、次の例 2 で扱います。

例 2: オーケストレーター Usecase (部品クラスを DI)

複数ステップを束ねるオーケストレーター Usecase では、依存が IEloquent だけでなく Reader / Validator / Mapper などの 部品クラス に広がります。それでも Layered Constructor Pattern の形は変わりません。

例として、「取引先 ID から商談を 1 件作成する」Usecase を考えます。手順は次の通りです。

  1. 取引先を取得する (AccountReader)
  2. 商談を作って良いかを検証する (OpportunityEligibilityValidator)
  3. 取引先の情報を元に商談を組み立てる (OpportunityMapper)
  4. 商談を insert する (IEloquent)
APEX
public with sharing class CreateOpportunityFromAccountUsecase {
  private final Id accountId;
  private final AccountReader accountReader;
  private final OpportunityEligibilityValidator validator;
  private final OpportunityMapper mapper;
  private final IEloquent insertEloquent;
  private Trace t = Trace.of('取引先から商談を作成');
 
  public CreateOpportunityFromAccountUsecase(Id accountId) {
    this(accountId, null, null, null, null);
  }
 
  @TestVisible
  private CreateOpportunityFromAccountUsecase(
    Id accountId,
    AccountReader accountReader,
    OpportunityEligibilityValidator validator,
    OpportunityMapper mapper,
    IEloquent insertEloquent
  ) {
    this.accountId = accountId;
    this.accountReader = accountReader ?? new AccountReader(new Eloquent());
    this.validator = validator ?? new OpportunityEligibilityValidator();
    this.mapper = mapper ?? new OpportunityMapper();
    this.insertEloquent = insertEloquent ?? new Eloquent();
  }
 
  public void invoke() {
    this.t.start();
 
    IEntry accountEntry = this.accountReader.fetch(this.accountId);
    this.validator.assertEligible(accountEntry);
    Opportunity opp = this.mapper.toOpportunity(accountEntry);
    this.insertEloquent.doInsert(opp);
 
    this.t.finish('商談を 1 件作成。');
  }
}

形は例 1 とまったく同じです。違うのは 依存の種類が増えた ことだけで、public コンストラクタは業務入力 (accountId) しか受け取らず、private コンストラクタで全依存を受けて null なら本番デフォルトに差し替えています。

AccountReader 自身も「コンストラクタで IEloquent を受け取る」形 (生焼けオブジェクトを作らない原則) になっており、new AccountReader(new Eloquent()) の 1 行で完成します。OpportunityEligibilityValidatorOpportunityMapper のように外部依存を持たない部品は、引数なしの new で組み上げます。

部品クラス (Reader / Validator / Mapper) の責務分けや切り出し方の指針は、Handler-Usecase Architecture の「部品クラスの扱い」 を参照してください。本ドキュメントの主題は、それらをどう Usecase に 注入するか です。

この 4 分割は、目標ではありません

例 2 は「依存の種類が増えても形は変わらない」ことを示すための例であって、4 つに分けること自体を推奨しているわけではありません

分ける動機は、そこに差し替えたい継ぎ目があるかどうかです。AccountReader は DB に触るので継ぎ目に値します。一方、差し替える必要のない純粋なロジックを「DI したいから」という理由だけでクラスに切り出すと、割に合わないことが起きます。

  • 引数のバケツリレーが増える (部品間でデータを持ち回るためだけの引数)
  • 同じデータを扱う処理が別クラスに分かれ、同じレコードを 2 回クエリする
  • 切り出した先のクラスが、他から使われない中途半端な存在になる

判断の目安は、同じ知識 (コンテキスト) を扱うものはまとめ、知識が変わるところで切る、です。闇雲にまとめるのも逆方向の失敗で、共通化の受け皿にされたクラスは、どの文脈にも属さない不完全なものになっていきます。

実装したあとに一度、こう問い直すと粒度が整います。

まとめられるもの・効率化できるものはあるか。ただし過剰な共通化は避け、同じコンテキストの知識だけをまとめること。

粒度そのものへの留保は Handler-Usecase Architecture の「ただし、粒度については留保があります」 にも書いています。ここに示した形は、2026 年時点で妥当だと考えている粒度です。

テストで何が変わるか

Layered Constructor Pattern を採用した Usecase は、テスト時に次の自由度を手に入れます。

  • 用途別に依存を独立差し替えできるIEloquent 単体ならラベル多重化 (例: LBL_FETCH / LBL_UPDATE)、オーケストレーターでは部品クラスをフィールドごとに分割、と粒度を選べます。どちらの形でも「取得は成功するが更新だけ例外を投げる」「取得は空、更新は呼ばれないことを検証する」といった独立シナリオが自然に書けます。1 つの IEloquentラベルなしで使い回す設計では難しい検証が、無理なく組めるようになります。
  • 部品クラスをモック・fake・本物の任意粒度で差し替えできる。オーケストレーター Usecase では、AccountReader をモックに、OpportunityMapper だけは本物を使う、といった粒度の選択がテストごとに可能です。「ロジックの中心は本物で動かしつつ、外部 I/O だけ閉じる」テスト設計が無理なく書けます。
  • 本番コードを 1 行も書き換えなくてよい。テストを書くために、本番のコンストラクタを増やしたり setter を生やしたりする必要はありません。@TestVisible を付けた private コンストラクタが、テスト専用の入り口としてすでにそこにあるからです。

テストコードの具体例は、Apex Stem 導入ガイドのステップ 4 に載せています。MockEloquent を 1 本注入し、取得を attach(LBL_FETCH, ...) で仕込んで、更新結果を upsertedRecordsAt(LBL_UPDATE) で検証する形が、Layered Constructor Pattern の素直な使い方になっています。

次に読む