Apex Stem · Foundation
v1.0.0·リリースノート

ApexTools

どのプロジェクトでも書き直していた土台を、1 回だけ書く。トリガーの分岐と、コールアウトのモック。

Trigger.isAfter の手書き分岐も、HttpCalloutMock の実装クラスも、 プロジェクトごとに毎回書くものではありません。ApexTools はそれを引き取る、基盤層のユーティリティ集です。

TriggerOppHandler.cls
public with sharing class TriggerOppHandler    extends TriggerHandler {   protected override void afterUpdate(      Map<Id, SObject> newMap, Map<Id, SObject> oldMap) {     // 変更されたレコードだけを 1 行で絞る    Set<Id> needIds = this.getUpdateRecordIdsWithChangedFields(      new List<SObjectField>{ Opportunity.StageName });     (new RegenerateCollection(needIds)).invoke();  }}// Trigger.isAfter も new/old 比較も書かない。

7 つのフックのうち必要なものだけを override します。extends TriggerHandler と書いた時点で、 コンテキスト判定はもう自分の仕事ではありません。

What is in the box

基盤層のユーティリティ集です。いま入っているのはどのプロジェクトでも書き直していた 2 つ。 同じ性質のものが見つかれば、これからも増えます。

Pillar 1

TriggerHandler 基底クラス

7 つのフック (beforeInsertafterUndeleteandFinally) をprotected virtual で持ちます。 必要なものだけ override すればよく、`Trigger.isAfter && Trigger.isInsert` の手書き分岐が消えます。 「特定項目が変わったレコードだけ」を返すヘルパーも 4 種そろっています。

7 つのフックと変更検知ヘルパーを読む →
Pillar 2

IHttpRequestHandler

コールアウトを DI で差し替え可能にします。本番はHttpRequestHandler、テストはMockHttpRequestHandler。 応答は MockResponse の宣言だけで組み立てるので、順序つきの応答でも、`HttpCalloutMock` の実装クラスを書かずに済みます

2 つのモードと Spy を読む →
The scaffolding you rewrite every time

Two features with nothing in common.Except where they came from.

トリガーの分岐と HTTP のモックは、機能としては何の関係もありません。 共通しているのは出自です。どちらも 「プラットフォームが用意してくれなかったので、プロジェクトのたびに誰かが書き直していた」もの。 しかも書き直すたびに少しずつ違う出来になり、レビューのたびに同じ議論をやり直すことになります。

ApexTools はそれを、1 回だけ書いて固定した置き場です。 収録の基準はこの出自 (プラットフォームが用意せず、毎回書き直していた) ひとつなので、 当てはまるものが出てくればこれからも増えます。 いまはその 2 つ目までが入っています。

How it reads

2 つの柱それぞれについて、書き直していたコードと、 ApexTools で置き換えたあとを並べます。

Pillar 1 · 「この項目が変わったレコードだけ」を絞る
TriggerOppHandler.clsHand-written
public void execute() {  if (Trigger.isAfter && Trigger.isUpdate) {    Set<Id> needIds = new Set<Id>();    for (Opportunity o : (List<Opportunity>) Trigger.new) {      Opportunity old =        (Opportunity) Trigger.oldMap.get(o.Id);      if (o.StageName != old.StageName) {        needIds.add(o.Id);      }    }    (new RegenerateCollection(needIds)).invoke();  } else if (Trigger.isAfter && Trigger.isInsert) {    // ...  }}
TriggerOppHandler.clsApexTools
protected override void afterUpdate(    Map<Id, SObject> newMap, Map<Id, SObject> oldMap) {   Set<Id> needIds =    this.getUpdateRecordIdsWithChangedFields(      new List<SObjectField>{ Opportunity.StageName });   (new RegenerateCollection(needIds)).invoke();}
Pillar 2 · 「存在確認 → 作成 → 再取得」の 3 手をモックする
MyCalloutMock.clsHand-written
public class MyCalloutMock    implements HttpCalloutMock {  private Integer callCount = 0;   public HttpResponse respond(HttpRequest req) {    HttpResponse res = new HttpResponse();    callCount++;    if (callCount == 1) {      res.setStatusCode(404);      res.setBody('{"message":"not found"}');    } else if (callCount == 2) {      res.setStatusCode(201);      res.setBody('{"id":"abc"}');    } else {      res.setStatusCode(200);      res.setBody('{"id":"abc","name":"Acme"}');    }    return res;  }}
KintoneUpsert_T.clsApexTools
MockHttpRequestHandler mock =  new MockHttpRequestHandler(new List<MockResponse>{    MockResponse.of('GET').respond(notFound, 404),    MockResponse.of('POST').respond(created, 201),    MockResponse.of('GET').respond(found, 200)  }); new KintoneUpsertUsecase(input, mock).invoke(); Assert.areEqual(1, mock.countByMethod('POST'));

どちらも消えているのは「状態を自分で管理するコード」です。 左は片方が Trigger のコンテキスト判定、 もう片方が callCount のカウンタ。 右ではどちらも宣言に置き換わっています。 フックの名前が起動タイミングを、リストの並び順が応答の順序を表します。

Position in Apex Stem

ApexTools は Apex Stem を構成する 4 つの OSS のうち Foundation を担います。Handler-Usecase Architecture の Handler 層そのものを支える土台で、他の 3 つが Usecase の中で働くのに対し、ApexTools はその外側にいます。

ApexEloquent
Data Access: SOQL / DML + モック
ApexBlueprint
Test Data Factory: 実 DML の結合テストデータ
ApexTrace
Lifecycle Logging: Usecase の経路追跡とテスト検証
ApexTools
Foundation: TriggerHandler 基底 + HTTP DI ラッパー
Related Documents

Start with the Developer Guide

TriggerHandler の 7 つのフックと、IHttpRequestHandler の 2 つのモードを、実コードで辿ります。