⚙️

Salesforce Opensouce LibraryのTriggerHandlerを試してみた

2022/03/19に公開

Salesforce Opensouce Libraryとは?

MITライセンスで開発されたライブラリ

Piotr Kożuchowskiさんによって開発されたApex周りを整えるユーティリティやTriggerHandlerのフレームワークを含んだライブラリ。
MITライセンスで開発されているので自由度高く利用することができるのがなんともありがたい。https://salesforce-opensource-library.com/
 今回はその中でTriggerHandlerに着目してみます。
 対象はあくまでコアの部分だけ。カスタムメタデータを使ったところは触れませんのでご了承ください。

Template Method パターンによる実装のメリット

そもそもなぜ今回TriggerHandlerのフレームワークを探していたのかということなのですが、こんなケースはみなさんにもあるあるではないかと思います。

  • 社内で開発をしていて、かつ、人がガンガン変わっていって技術継承がうまくできていない。結局、技術レベルが人に依存する
  • 導入時の開発ベンダーが開発をしていたり、それ以後に入った開発ベンダーが開発していたりと、様々な会社が入ってくる

さて、これらによって何が起こるかーー。オブジェクトによって(というか開発者・時期によって)トリガーに直接ロジックまで書かれていたり、トリガーコンテキストまでがトリガーに書かれてロジックはクラスに書かれていたり、トリガーコンテキストからTriggerHandlerに任せていたりと、三者三様の様相を呈してもうカオス。これをメンテする側としてはもう涙目という状況が生まれます。
今回はこの状況をTemplate Method パターンの実装によって、以後の開発だけでもせめて統一できないものかしら、と抵抗を試みようというわけです。
このフレームワークでは、TriggerHandlerや各トリガーコンテキストが抽象クラスとして定義されているので、これを具象化してあげることにより、TriggerHandlerやLogic部分を実装します。
それにより、どの開発者でも同じようなコードになるようコントロールを効かせられるようになります。

とりあえず実装してみよう

今回の構成

  1. AccountTrigger:トリガー。TriggerDispatcherのrunを呼ぶだけ
  2. AccountTriggerHandler:トリガーに呼び出されてbedore insertでAccountDescriptionIsHelloを呼び出す
  3. AccountDescriptionIsHello:説明にHelloと入れるだけのロジック
AccountTrigger.trigger
trigger AccountTrigger on Account (before insert, after insert, before update, after update, before delete, after delete, after undelete ) {
    TriggerDispatcher.run(new AccountTriggerHandler());
}
AccountTriggerHandler.cls
public inherited sharing class AccountTriggerHandler extends TriggerHandler {

    public override void onBeforeInsert(List<SObject> triggerNew, TriggerContext tc) {
        this.execute(triggerNew, tc, new List<Logic>{
            new AccountDescriptionIsHello()
        });
    }
}
AccountDescriptionIsHello.cls
public inherited sharing class AccountDescriptionIsHello implements TriggerHandler.Logic {

    public void execute(List<SObject> records, TriggerContext ctx) {
        for (Account acc : (Account[]) records){
                acc.Description = 'Hello';
        }
    }
 }

構成を一般化すると

  1. トリガーでTriggerDispatcherのrun(またはrunMetadataDefinedTriggers())を呼び出して、各オブジェクトのTriggerHandlerを指定(するかカスタムメタデータを利用)する
  2. 各オブジェクトのTriggerHandlerはTriggerHandler抽象クラスを具象化する形で作成。動作が必要なトリガーコンテキスト(onBeforeUpdateとか)のみ実装して、その中でLogicを指定する
  3. LogicはTriggerHandler内に書かれているインターフェースを実装する(もしくは従来のようにクラスで作成する)。

導入で何が変わる?

  • 書き方さえ決めれば、トリガー、クラスの役割が明確になり、どこに何を書くかのバラツキがなくなる
  • TriggerHandlerの定番whenなんちゃらトリガーコンテキストがカプセル化されているので、全部を書く必要がない(これはむしろ嫌な人もいるかも…?)
  • トリガーでごちゃごちゃ書かずに、クラス側(+カスタムメタデータ)に委任できる

フレームワークについての簡単に

  • TriggerDispatcher
    • 2つのメソッドが含まれていて、Trigger内はこのどちらか一方の1行だけ記載します
      • run(TriggerHandler triggerHandler):対象オブジェクトのTriggerHandlerを書いてそこに飛ばします
      • runMetadataDefinedTriggers():カスタムメタデータでトリガーを定義したい場合。今回はこっちは試していません。複数のチームが同時に1つのオブジェクトに対してアプローチする場合特に便利そうです。
  • TriggerHandler
    • これ自体が抽象クラスとして定義されているので、各オブジェクトではこれを具象化させます
    • onBeforeInsertやonAfterUpdateのように、各コンテキストがメソッドとして定義されているので、必要なものだけoverrideして利用します
    • LogicとParameterizableの2つのインターフェースが用意されています。
      • Logic:具体的な処理を記載するインターフェースです。これを実装しなくてもTriggerHandlerで通常通りクラスを指定することによっても動きますが、Logicを使うことがベストプラクティスに指定されています。
      • Parameterizable:似たような動作を組みたいけれど、オブジェクトによって異なる項目を指定したいんだよなあ…といった場合のようなときに、パラメータをカスタムメタデータに作成することによって、様々なオブジェクトで1つのクラスを共用できるようにします。今回は触れません。

Discussion