進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

Apex でのドメインレイヤの原則の適用

学習の目的

この単元を完了すると、次のことができるようになります。

  • ドメイン Apex クラスを作成する。
  • ドメインクラスにデフォル設定および検証コードを反映する。
  • ドメインクラスのメソッドを Apex トリガイベントに対応付ける。
  • 実行時にセキュリティ適用のアプリケーションを制御する。

ドメインクラスの作成

前のトリガコードで使用されている fflib_SObjectDomain クラスは、実際にはすべてのドメインクラスの基本クラスであり、オブジェクトセキュリティなど有益な機能を提供します。

fflib_SObjectDomain class

注意: ほとんどのメソッドは仮想メソッド (横に v が付いているメソッド) として提供されます。これらの上書きは任意です。この基本クラスの handle*XXXX* メソッドは、適切なタイミングで on*XXXX* メソッドがコールされるようにします。より高度なケースで直接の処理が必要な場合は、ハンドラメソッドの上書きを選択できます。ただし、ハンドラメソッドのスーパー基本クラスバージョンをコールしない限り、on*XXXX* メソッドはコールされません。

この基本クラスは、テンプレートメソッドパターンを使用して、onValidate によるレコード検証と onApplyDefaults による項目値のデフォルト設定を行う共通のドメインロジックを実装するための標準フックを提供します。

さらに、特定の Apex トリガイベントに関連するロジックを配置するためのメソッドがあります。最後に、コンストラクタ (このために fflib_SObjectDomain クラスを拡張するすべてのクラスも公開する必要があります) が、前の単元で説明した一括処理化の設計目標に沿って sObject のリストを取得します。

以下は、Opportunities ドメインクラスの基本的な実装です。

public class Opportunities extends fflib_SObjectDomain {

    public Opportunities(List<Opportunity> sObjectList) {
        super(sObjectList);
    }

    public class Constructor implements fflib_SObjectDomain.IConstructable {
        public fflib_SObjectDomain construct(List<SObject> sObjectList) {
            return new Opportunities(sObjectList);
        }
    }
}

注意: Constructor 内部クラスによって、前の単元の Apex トリガサンプルで使用されていた基本クラスメソッド SObjectDomain.triggerHandler が、ドメインクラスの新しいインスタンスを作成して sObject リストで渡せるようになります (Trigger.new など)。これは、現在 Apex で完全な反映ができないことに対する回避策です。

項目のデフォルト設定ロジックの実装

項目のデフォルト設定ロジックの場所を提供するために、基本クラスは onApplyDefaults メソッドを公開します。このメソッドは、トリガの呼び出し時に handleBeforeInsert 基本クラスメソッドからコールされます。

ここにロジックを配置することで、レコードが追加されたときにアプリケーション全体で一貫したデフォルトが使用されます。必要に応じて、サービスから明示的にコールすることもでき、Visualforce ページまたは Lightning コンポーネント経由でカスタム UI にアクセスしているユーザにデフォルトレコード値を提示する場合などに役立ちます。

この基本クラスは、コンストラクタが Records プロパティを使用してすべてのメソッドをコールするときに提供される sObject リストを公開します。これは厳密にはトリガシナリオではありませんが、前の単元のドメイン設計の目標に従って、一括処理化を考慮することを強くお勧めします。

public override void onApplyDefaults() {

    // Apply defaults to Opportunities
    for(Opportunity opportunity : (List<Opportunity>) Records) {
        if(opportunity.DiscountType__c == null) {
            opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
        }               
    }
}

注意: 上記の例は、DiscountType__c 項目定義の一環として表された数式を使用して実現することも可能です。当然、デフォルト設定ロジックは複数項目または他のレコードにまたがる可能性があります。その場合は、Apex コーディングを使用して対処する必要があります。

検証ロジックの実装

上記のトリガメソッドを上書きして検証ロジックを実装できますが、プラットフォームのベストプラクティスとして、これは Apex トリガ呼び出しの after フェーズでのみ行います。2 つの onValidate メソッドのいずれかを上書きすることで、このロジックを明確に定義された場所に実装できます。

public override void onValidate() {

    // Validate Opportunities
    for(Opportunity opp : (List<Opportunity>) Records) {
        if(opp.Type.startsWith('Existing') && opp.AccountId == null) {
            opp.AccountId.addError('You must provide an Account when ' +
                'creating Opportunities for existing Customers.');
        }
    }
}

上記の検証メソッドは、レコードがオブジェクトに挿入されると基本クラスからコールされます。レコード更新時のデータ変更に反応する検証ロジックが必要な場合、次のバリアントを上書きできます。

public override void onValidate(Map<Id,SObject> existingRecords) {

    // Validate changes to Opportunities
    for(Opportunity opp : (List<Opportunity>) Records) {
        Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
        if(opp.Type != existingOpp.Type) {
            opp.Type.addError('You cannot change the Opportunity type once it has been created');
        }
    }
}

注意: 基本クラスメソッド handleAfterInsert および handleAfterUpdate のコードでは、Apex トリガの after 部分でのみ (このオブジェクトに対するすべての Apex トリガが完了した後) このメソッドをコールすることで、セキュリティベストプラクティスが適用されます。この動作は、AppExchange パッケージ開発者にとってきわめて重要です (「リソース」セクションを参照)。

Apex トリガロジックの実装

実装するドメインロジックが必ず上記のメソッドに当てはまるとは限りません。実際、関心の分離ガイドラインでは、これらのメソッドにすべてのデフォルト設定または検証ロジックを実装することは厳格な要件ではありません。これは単なる考慮事項です。必要であれば、下記のメソッドのすべてに加えることができます。

他のドメインオブジェクトを介して動作を呼び出す Apex トリガ関連のコードを実装するために、次の少し不自然な例では、onAfterInsert メソッドを上書きして、新しい Opportunities が挿入されたら常に関連する Account の Description 項目を更新します。

public override void onAfterInsert() {
    // Related Accounts
    List<Id> accountIds = new List<Id>();
    for(Opportunity opp : (List<Opportunity>) Records) {
        if(opp.AccountId!=null) {
            accountIds.add(opp.AccountId);
        }
    }

    // Update last Opportunity activity on related Accounts via the Accounts domain class
    fflib_SObjectUnitOfWork uow =
      new fflib_SObjectUnitOfWork(
        new Schema.SObjectType[] { Account.SObjectType });
    Accounts accounts = new Accounts([select Id from Account
      where id in :accountIds]);
    accounts.updateOpportunityActivity(uow);
    uow.commitWork();              

}

上記の例では次の点に留意してください。

  • Accounts ドメインクラスのインスタンスがインライン SOQL クエリを使用して初期化されます。このモジュールの次の単元では、クエリロジックをカプセル化して、ドメインクラスロジックにとって重要な出力データの再利用性と一貫性を高めるのに役立つパターンを紹介します。

  • 「SOC」モジュールによると、fflib_SObjectUnitOfWork メソッドは、サービスコンテキストよりも Apex トリガコンテキストで使用されます。このケースでは、そのスコープはトリガイベントまたはメソッドです。これらのメソッドは、サービスレイヤではなくパターンから直接コールされます。そのため、作業単位が作成されて Accounts メソッドに提供され、Accounts メソッドが更新を Account レコードに登録できるようになります。ここには含まれていませんが、通常は、1 か所で作業単位を初期化して重複を回避することをお勧めします。

  • Accounts に基づく更新アクティビティは、Opportunity というより Account オブジェクトの動作であるため、ここで Accounts ドメインクラスに委ねることは妥当です。ドメインクラス間でのこの種の SSC については、次のセクションでも説明します。

参考までに、以下が Accounts.updateOpportunityActivity メソッドです。

public class Accounts extends fflib_SObjectDomain {

    public Accounts(List<Account> sObjectList) {
        super(sObjectList);
    }

    public void updateOpportunityActivity(fflib_SObjectUnitOfWork uow) {

        for(Account account : (List<Account>) Records) {
            account.Description = 'Last Opportunity Raised ' + System.today();
            uow.registerDirty(account);
        }
    }
}

カスタムロジックの実装

基本クラスから上書き可能なメソッドの実装のみに制限されているわけではありません。前の単元の修正されたサービスレイヤを思い出してください。

public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) {
    // Unit of Work
    // ...

    // Validate parameters
    // ...

    // Construct Opportunities domain class
    Opportunities opportunities = new Opportunities( ... );

    // Apply discount via domain class behavior
    opportunities.applyDiscount(discountPercentage, uow);

    // Commit updates to opportunities     
    uow.commitWork();                      
}

このコードは、割引を Opportunity に適用する機能を持つドメインクラスメソッドを使用し、基本的に、このロジックをドメインオブジェクトに関連付けられた場所にさらにカプセル化します。

必要に応じて、このコードは OpportunityLineItems ドメインクラスに行レベルの割引の適用を委ねます。説明上、商品ラインを利用する商談ではロジックが異なると想定してください。

public void applyDiscount(Decimal discountPercentage, fflib_SObjectUnitOfWork uow) {

    // Calculate discount factor
    Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100);

    // Opportunity lines to apply discount to
    List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();

    // Apply discount
    for(Opportunity opportunity : (List<Opportunity>) Records) {

        // Apply to the Opportunity Amount?
        if(opportunity.OpportunityLineItems.size()==0) {
            // Adjust the Amount on the Opportunity if no lines
            opportunity.Amount = opportunity.Amount * factor;
            uow.registerDirty(opportunity);
        } else {
            // Collect lines to apply discount to
            linesToApplyDiscount.addAll(opportunity.OpportunityLineItems);
        }
    }      

    // Apply discount to lines
    OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);
    lineItems.applyDiscount(this, discountPercentage, uow);
}

注意: OpportunityLineItems ドメインクラスのコードはこちらで確認できます。

fflib_SObjectUnitOfWork メソッドは引数として取られます。そうすることでコール元 (この場合は OpportunitiesService.applyDiscount メソッド) が渡せるようになり、ドメインコードがそれに対する作業を登録できるようになります。また、OpportunityLineItems ドメインクラスの applyDiscount メソッドにも渡されます。

ドメインクラスとサービスクラスのビジネスロジックの比較

どこにコードを配置すべきかわからなくなることがあります。SOC 原則に立ち返って、サービスレイヤとドメインレイヤの関心が何にあるかを考えてみましょう。

アプリケーションの関心の種類 サービスまたはドメイン
レコードデータが操作されたら、一貫して項目が検証され、デフォルト値が設定されるようにする。 ドメイン 商品が追加されたらデフォルトの割引ポリシーを適用する。
複数の情報の一括取得または複数のオブジェクトの更新が含まれるシユーザまたはシステムアクションに反応する。ほとんどの場合は、レコードのコレクションに対して発生可能なアクションを提供し、そのアクションを完了するのに必要なすべてを調整します (他の支援サービスと連携する可能性があります)。 サービス 作業指示から請求書を作成して計算する。価格表情報を取得する場合があります。
他の関連レコードの変更の一部として、またはユーザまたはシステムアクションの実行によって、アプリケーションで発生するレコードへの変更を処理する。たとえば、必要に応じてデフォルト値を設定します。ある項目の変更によって影響を受ける別の項目も更新されます。 ドメイン Opportunity が作成されたときに Account オブジェクトがどう反応するか、または Opportunity 割引プロセスの実行時に Discount がどう適用されるか。 注意: この種のロジックはサービスレイヤで開始することがありますが、サービスメソッドサイズのサイズと複雑さを管理したり、再利用を改善したりする場合は、ドメインレイヤでより適切に機能します。
複数の異なるオブジェクトに対して適用される共通の動作を処理する。 ドメイン 商談商品または作業指示商品ラインの価格を計算する。 注意: これを共有ドメイン基本クラスに配置して、fflib_SObjectDomain メソッドを上書きし、Apex トリガイベントにフックできます。その際、具体的なドメインクラスがその動作でこのクラスを拡張できます。

セキュリティ適用の制御

デフォルトでは、fflib_SObjectDomain 基本クラスは Salesforce オブジェクトに CRUD セキュリティを適用します。ただし、コントローラまたはサービスのどちらを使用する場合でも、これはオブジェクトへのあらゆる種別のアクセスに対して呼び出されます。サービスロジックでは、オブジェクトへの権限を要求せずにユーザに代わってオブジェクトにアクセスする場合があるかもしれません。

このデフォルトの動作を無効にして独自のサービスコードでセキュリティを適用する場合、基本クラスの設定機能を使用できます。次の例は、各コンストラクタでこれを行う方法を示しています。他にも、このコードを含む独自の基本クラスを作成し、すべてのドメインクラスに対してそのクラスを拡張するという方法があります。

public Opportunities(List<Opportunity> sObjectList) {

    super(sObjectList);        

    // Disable default Object Security checking    
    Configuration.disableTriggerCRUDSecurity();
}  

注意: 基本クラスでは、更新に項目レベルセキュリティを適用するための汎用的なサポートを提供しません。この適用は引き続きユーザの責任で行います。

ドメインクラスのテスト

より小さく、より多くのカプセル化されたチャンクにロジックを分離することは、テスト主導型開発 (TDD) にとってメリットがあります。テストでドメインクラスを構築し、メソッドを直接呼び出すことが容易になるためです。サービスレイヤのテストが不要になる訳ではありませんが、より漸進的なテストおよび開発アプローチを適用できます。

課題に向けた準備

これらの課題を完了するには、いくつかのオープンソースライブラリをリリースする必要があります。ただし、「Apex エンタープライズパターン: サービスレイヤ」モジュールの一環としてすでにリリースしている場合は不要です。まず ApexMocks ライブラリ、次に Apex Commons ライブラリをインストールする必要があります。これらのライブラリとそれぞれのオープンソース使用許諾契約についての詳細は、各リポジトリを参照してください。

これらのライブラリを組織にインストールするには、次の [Deploy (リリース)] ボタンを使用します。

ApexMocks オープンソースライブラリのリリース。

Salesforce へのリリース

Apex Common オープンソースライブラリのリリース。

Salesforce へのリリース

リソース