Apex でのドメインレイヤーの原則の適用
学習の目的
この単元を完了すると、次のことができるようになります。
- ドメイン Apex クラスを作成する。
- ドメインクラスにデフォルト設定および検証コードを反映する。
- ドメインクラスのメソッドを Apex トリガーイベントに対応付ける。
- 実行時にセキュリティ適用のアプリケーションを制御する。
一緒にトレイルを進みましょう
エキスパートの説明を見ながらこのステップを実行したい場合は、次の動画をご覧ください。これは「Trail Together」(一緒にトレイル) シリーズの一部です。
(この動画は 17:30 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)
参照コード
このモジュールでは、FFLIB Apex Common Samplecode プロジェクトの次の Apex クラスを参照します。始める前に開いておくことをお勧めします。
ドメインクラスの作成
前の単元のトリガーコードで使用されている fflib_SObjectDomain クラスは、トリガーハンドラーをサポートする基本クラスを拡張し、オブジェクトセキュリティなどの有益な機能を提供します。
この基本クラスは、テンプレートメソッドパターンを使用して、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 トリガーサンプルで使用されていた基本クラスメソッド fflib_SObjectDomain.triggerHandler
が、ドメインクラスのトリガーハンドラーロジックの新しいインスタンスを作成して sObject リスト (Trigger.new
など) で渡せるようになっています 。
項目のデフォルト設定ロジックの実装
項目のデフォルト設定ロジックの場所を提供するために、fflib_SObjectDomain
基本クラスは onApplyDefaults()
メソッドを公開します。このメソッドは、トリガーの呼び出し時に fflib_SObjectDomain
基本クラスの handleBeforeInsert()
メソッドからコールされます。
ここにロジックを配置することで、レコードが追加されたときにアプリケーション全体で一貫したデフォルトが使用されます。必要に応じて、サービスから明示的にコールすることもでき、Visualforce ページまたは Lightning コンポーネント経由でカスタム UI にアクセスしているユーザーにデフォルトレコード値を提示する場合などに役立ちます。
この基本クラスは、コンストラクターコールで提供された sObject リストを records
プロパティを介してすべてのメソッドに公開します。これは厳密にはトリガーシナリオではありませんが、前の単元のドメイン設計の目標に従って、一括処理化を考慮することを強くお勧めします。
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;
}
}
}
検証ロジックの実装
上記のトリガーメソッドを上書きして検証ロジックを実装できますが、ベストプラクティスとして、これは Apex トリガー呼び出しの after フェーズでのみ行います。fflib_SObjectDomain
基本クラスの 2 つの onValidate()
メソッドのいずれかを上書きすることで、明確に定義された場所にこのロジックを実装できます。
public override void onValidate() {
// Validate Opportunities
for(Opportunity opp :(List<Opportunity>) this.records) {
if(opp.Type.startsWith('Existing') && opp.AccountId == null) {
opp.AccountId.addError('You must provide an Account when ' +
'creating Opportunities for existing Customers.');
}
}
}
上記の onValidate()
メソッドは、レコードがオブジェクトに挿入されると基本クラスからコールされます。レコード更新時のデータ変更に反応する検証ロジックが必要な場合、次のバリアントを上書きできます。
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
ドメインクラスに行レベルの割引の適用を委ねます。説明上、商品ラインを利用する商談ではロジックが異なると想定してください。
OpportunityLineItems ドメインクラスのコードはこちらで確認できます。
fflib_ISObjectUnitOfWork
インスタンスは引数として取られます。そうすることでコール元 (この場合は 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) にとってメリットがあります。テストでドメインクラスを構築し、メソッドを直接呼び出すことが容易になるためです。サービスレイヤーのテストが不要になる訳ではありませんが、より漸進的なテスト-開発アプローチを適用できます。
ハンズオン Challenge の準備
この Challenge を実行するには、「Apex エンタープライズパターン: サービスレイヤー」モジュールで使用した Trailhead Playground を起動します。すでにインストールしたオープンソースライブラリが必要になります。別の Trailhead Playground を使用している場合は、起動してから、下の [Deploy to Salesforce (Salesforce にリリース)] ボタンを使用して、まず ApexMocks ライブラリ、次に Apex Commons ライブラリ をインストールしてください。これらのライブラリとそれぞれのオープンソース使用許諾契約についての詳細は、各リポジトリを参照してください。
ApexMocks オープンソースライブラリのリリース。
Apex Common オープンソースライブラリのリリース。
リソース
- GitHub: Apex Enterprise Patterns (Apex エンタープライズパターン)
- Wikipedia:関心の分離
- Blog 投稿:Where to Place Validation Code in an Apex Trigger (Apex トリガーのどこに検証コードを配置するか)
- Blog 投稿: Unit Testing, Apex Enterprise Patterns and Apex Mocks – Part 1 (単体テスト、Apex エンタープライズパターンと Apex Mocks – パート 1)
- Blog 投稿: Unit Testing, Apex Enterprise Patterns and Apex Mocks – Part 2(単体テスト、Apex エンタープライズパターンと Apex Mocks – パート 2)
- Blog 投稿: Martin Fowler: Catalog of Patterns of Enterprise Application Architecture (エンタープライズアプリケーションアーキテクチャのパターンのカタログ)