ApexTools
どのプロジェクトでも書き直していた土台を、1 回だけ書く。トリガーの分岐と、コールアウトのモック。
Trigger.isAfter の手書き分岐も、HttpCalloutMock の実装クラスも、 プロジェクトごとに毎回書くものではありません。ApexTools はそれを引き取る、基盤層のユーティリティ集です。
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 つ。 同じ性質のものが見つかれば、これからも増えます。
TriggerHandler 基底クラス
7 つのフック (beforeInsert 〜afterUndelete とandFinally) をprotected virtual で持ちます。 必要なものだけ override すればよく、`Trigger.isAfter && Trigger.isInsert` の手書き分岐が消えます。 「特定項目が変わったレコードだけ」を返すヘルパーも 4 種そろっています。
IHttpRequestHandler
コールアウトを DI で差し替え可能にします。本番はHttpRequestHandler、テストはMockHttpRequestHandler。 応答は MockResponse の宣言だけで組み立てるので、順序つきの応答でも、`HttpCalloutMock` の実装クラスを書かずに済みます。
Two features with nothing in common.Except where they came from.
トリガーの分岐と HTTP のモックは、機能としては何の関係もありません。 共通しているのは出自です。どちらも 「プラットフォームが用意してくれなかったので、プロジェクトのたびに誰かが書き直していた」もの。 しかも書き直すたびに少しずつ違う出来になり、レビューのたびに同じ議論をやり直すことになります。
ApexTools はそれを、1 回だけ書いて固定した置き場です。 収録の基準はこの出自 (プラットフォームが用意せず、毎回書き直していた) ひとつなので、 当てはまるものが出てくればこれからも増えます。 いまはその 2 つ目までが入っています。
How it reads
2 つの柱それぞれについて、書き直していたコードと、 ApexTools で置き換えたあとを並べます。
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) { // ... }}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();}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; }}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 はその外側にいます。
- TriggerHandler: 7 つのフックと、項目変更を検知する 4 つのヘルパー
- IHttpRequestHandler: 台本モード / label モードと、Content-Type ガードの定石
- Handler-Usecase Architecture: ApexTools が支える Handler 層の設計
- Layered Constructor Pattern:
IHttpRequestHandlerの DI 設計を支える基本パターン
Start with the Developer Guide
TriggerHandler の 7 つのフックと、IHttpRequestHandler の 2 つのモードを、実コードで辿ります。