技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

Salesforce で DI できないか考えてみた

DI って?

Dependency Injection 外部依存注入の略。
DI の説明が要旨でもないので、下記 URL 参照。

qiita.com

Salesforce で DI ?

Salesforce のアプリケーションをパッケージで公開するとき、やってみるとわかるのですが、「global」修飾したオブジェクトしか利用者はカスタムできません。
そこは別に良いのですが、「色々カスタマイズできるようにしたい」とあれこれ global 指定してしまうと、以下のジレンマに悩まされます。

global インターフェースは絶対に変更できないので、公開すればしただけ変更ができない

ということになります。
特に、継承を許してしまうと致命的ですね。
内部挙動に依存したコードを書かれて、何か変更しようものなら「バグだゴルァ!」とクレームが飛ぶこと請け合いです。

そこで、機能を分離して、設定で上書きしたり継承したりできるようにできないか?
といったときに

インターフェースだけ公開して好きに実装しろ

と、内部挙動を勝手に実装しろ、ただしインターフェースの仕様は守れよと。
そんな感じでカスタムできる様にする手段として DI を考察しました。

基本原理

クラス名さえわかれば、引数なしコンストラクタ経由でインスタンスを生成できることがわかっているので、

  • カスタム設定に「インターフェース名=実装クラス名」を実装させる
  • コンテナがこれをみてインスタンスを生成する(未設定ならデフォルトの設定クラスをインスタンス化する)
  • インターフェースに依存する形でアプリケーションを作成する

としておけば、顧客はカスタム設定を変更するだけで、実際の挙動を変更したりカスタムしたりできるようになるはずだ。

ということで検証した。

いざ実装

まずは検証対象クラスを作成

実際にはルートクラスと、インターフェースは global にでもするのだろうがここでは割愛。

public with sharing class SimpleExample {
    public interface ExampleInterface {
        String getMessage();
    }

    public class DefaultImpl implements ExampleInterface {
        public String getMessage() {
            return 'Default implements';
        }
    }
}

DI 置き換え対象のクラスも作成

public with sharing class CustomImpl implements SimpleExample.ExampleInterface {
    public String getMessage() {
        return 'Custom implements';
    }
}

ここまでは大した難しい話ではないかも?

カスタム設定を追加する

f:id:white-azalea:20200412170152p:plain
という事で Salesforce 上に設定した図

設定の内容はあくまで検証用サンプルなので

  • API 参照名
    DISample__c
  • 項目
    ExampleInterface__c : String(32)

DI コンテナマネージャの作成

DI を実際に行っていくクラスを作成する。
ここも実運用だと global だったり、ネームスペース指定とかカスタム設定を受け取るとか色々ありそうだが、今回はあくまで検証なので割愛して実装した。

public with sharing class DIManager {

    public class DISetting {
        private String interfaceName { get; private set; }
        private String settingName { get; private set; }
        private Type defaultObjectType { get; private set; }

        public DISetting(String interfaceName, String settingName, Type defaultObjectType) {
            this.interfaceName = interfaceName;
            this.settingName = settingName;
            this.defaultObjectType = defaultObjectType;
        }
    }

    private Map<String, DISetting> diSettings;

    public DIManager(List<DISetting> settings) {
        this.diSettings = new Map<String, DISetting>();
        for (DISetting st : settings) {
            this.diSettings.put(st.interfaceName, st);
        }
    }

    public Object getObject(String interfaceName) {
        DISetting setting = this.diSettings.get(interfaceName);
        if (setting == null) { return null; }  // not found

        DISample__c diSetting = DISample__c.getOrgDefaults();
        String targetObjectName = diSetting != null ? (String) diSetting.get(setting.settingName) : null;

        if (String.isEmpty(targetObjectName)) {
            return this.getDefaultObject(interfaceName);
        } else {
            Type t = Type.forName(targetObjectName);
            return t.newInstance();
        }
    }

    private Object getDefaultObject(String interfaceName) {
        DISetting setting = this.diSettings.get(interfaceName);
        if (setting == null) { return null; }  // not found
        return setting.defaultObjectType.newInstance();
    }
}

検証

色々面倒なので匿名 Apex 実行をしてみる。
ソースは下記

DIManager.DISetting setting = new DIManager.DISetting('ExampleInterface', 'ExampleInterface__c', SimpleExample.DefaultImpl.class);
DIManager manager = new DIManager(new List<DIManager.DISetting> { setting });

SimpleExample.ExampleInterface example = (SimpleExample.ExampleInterface) manager.getObject('ExampleInterface');

System.debug('Message : ' + example.getMessage());

まずは、カスタム設定を利用せずに実行する。

(前略)
Execute Anonymous: System.debug('Message : ' + example.getMessage());
17:05:25.25 (25031288)|USER_INFO|[EXTERNAL]|0052w000003PbHm|arstromeria@white-azalea.net|(GMT+09:00) 日本標準時 (Asia/Tokyo)|GMT+09:00
17:05:25.25 (25094245)|EXECUTION_STARTED
17:05:25.25 (25103087)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
17:05:25.25 (30135496)|USER_DEBUG|[6]|DEBUG|Message : Default implements  ← 特に設定が存在しないのでデフォルト実装が動作してる
17:05:25.30 (30230158)|CUMULATIVE_LIMIT_USAGE
17:05:25.30 (30230158)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
(以下略)

ではここで、カスタム設定を行ってみる。

f:id:white-azalea:20200412170848p:plain

この状態で実行すると

17:09:13.33 (33341882)|EXECUTION_STARTED
17:09:13.33 (33360008)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
17:09:13.33 (42787189)|USER_DEBUG|[6]|DEBUG|Message : Custom implements  ← 設定によって利用されるインスタンスが書き換わった!
17:09:13.42 (42932602)|CUMULATIVE_LIMIT_USAGE
17:09:13.42 (42932602)|LIMIT_USAGE_FOR_NS|(default)|

ということができた。