サービスレイヤーの原則について
学習の目的
この単元を完了すると、次のことができるようになります。
- Martin Fowlerの エンタープライズアプリケーションアーキテクチャパターンで紹介されているサービスパターンの根幹について説明する。
- サービスレイヤーに属する Apex コードを判断する。
- アプリケーションのアーキテクチャおよびプラットフォームにサービスレイヤーがどのように適合するか議論する。
- プラットフォームのベストプラクティスに沿って機能するサービスレイヤーを設計する。
一緒にトレイルを進みましょう
エキスパートの説明を見ながらこのステップを実行したい場合は、次の動画をご覧ください。これは「Trail Together」(一緒にトレイル) シリーズの一部です。
(この動画は 17:45 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)
はじめに
前の単元では、ソフトウェアアーキテクトをアプリケーションロジックのレイヤリングの検討に集中させる手段として、SOC を紹介しました。この単元では、アプリケーションの他のレイヤーやコンシューマー (API など) への重要なエントリポイントとしてのサービスレイヤーの定義および使用について説明します。
サービスレイヤーは、「利用可能な操作セットの設定と各操作のアプリケーションレスポンスの調整を行うサービス層によって、アプリケーションの境界を定義する。」Martin Fowler / Randy Stafford、EAA パターン
サービスレイヤーは、ビジネスタスク、計算、プロセスを実装するコードを明確かつ厳密にカプセル化することに役立ちます。サービスレイヤーが、異なるコンテキスト (モバイルアプリケーション、UI フォーム、リッチ Web UI、各種 API など) で確実に使用できるようにすることが重要です。また、今後の時代や要求の変化に対応するために、純粋かつ抽象的であり続ける必要があります。次のセクションでは、Salesforce のベストプラクティスとガバナ制限を踏まえつつ、Apex でのサービスレイヤー実装の作成ガイドラインを定義します。
サービスレイヤーの利用者とは?
この質問に「イカした人」と答える人もいるかもしれませんが、実際のところ、サービスレイヤーのコンシューマーは「クライアント」と呼ばれます。クライアントは、サービスレイヤーコードを呼び出します。サービスレイヤーとやり取りするのは、人ではなく、UI コントローラー、Apex 一括処理など、ユーザーやシステムとやり取りするコードです。
クライアントの一例として、Visualforce コントローラーまたは @AuraEnabled
メソッド内で記述されているコードがあります。ただし、考慮すべきサービスレイヤーコードのクライアント (コンシューマー) は、ほかにもたくさんあります。候補の一覧を作成するには、Salesforce プラットフォームの Apex ロジックを呼び出せるすべての手段を検討してください。
ご想像どおり、サービスレイヤーロジックは、他のレイヤーおよび目的向けに作成された Apex コードに非常に簡単に漏れてしまいます。そうなると、一貫性が低下し、その影響はエンドユーザーエクスペリエンスにまで及ぶため、サービスレイヤー実装の価値が損なわれます。たとえば、Salesforce のいくつかのテクノロジーを通じて公開されている特定の機能を使用してユーザーがアプリケーションとやり取りする場合や、作成した Lightning コンポーネント経由、および Apex REST サービス経由で特定の計算を公開する場合に考えられます。どちらの場合においても、動作は一貫している必要があります。次のセクションでは、サービスレイヤーの設計と責任、およびサービスレイヤーを使用するコードの期待事項について説明します。
プラットフォームのイノベーションと適応性
上記のテクノロジーは、長年をかけて、新しい機能としてプラットフォームに徐々に導入されてきました。特定の機能に結びついた方法でコードが記述されていて、たびたびリファクタリングしなければならない場合を想像してみてください。もし、それまでの領域のコードのリファクタリングについて心配する必要がないとしたら、これらの機能や新しい機能に対してアプリケーションを採用したり適応させたりすることは非常に簡単です。下手すると、リファクタリングによって既存の機能に問題が発生する心配があるため、コードを重複させることもあります。これは問題です。
デザインの考慮事項
-
命名規則 - サービスレイヤーは、さまざまなクライアントで意味が通じるように抽象的である必要があります。多くの場合、クラス、メソッド、およびパラメーター名で使用する動詞や名詞が採用されます。特定のクライアントコール元に関連したものではなく、アプリケーションやタスクの一般的な用語で表してください。たとえば、ビジネス操作に基づいたメソッドの名前の例は
InvoiceService.calculateTax(...)
、特定のクライアントの使用操作に基づいたメソッドの名前の例はInvoiceService.handleTaxCodeForACME(...)
です。後者は少し不便です。
-
プラットフォーム / コール元の共鳴 - プラットフォームのベストプラクティス、特に一括処理化をサポートするメソッド署名を設計します。Salesforce のコードの大きな関心の 1 つは一括処理化です。リストでコールするサービスと、1 つのパラメーターセットでコールするサービスの場合を考えてみてください。たとえば、
InvoiceService.calculateTax(List<TaxCalculation> taxCalculations)
の場合、このメソッドのパラメーターは一括処理化に対応しますがInvoiceService.calculateTax(Invoice invoice, TaxInfo taxCodeInfo)
の場合、コール元は何度もメソッドをコールする必要があります。ここでも、後者は少し不便です。
-
SOC の考慮事項 - サービスレイヤーコードは、通常、アプリケーションの複数のオブジェクトを使用して、タスクやプロセスロジックをカプセル化します。これをオーケストレーターだと考えてください。一方、検証、項目値、または計算は、レコードの挿入、更新、削除時に発生しますが、これらに特化したコードは、関連オブジェクトの関心対象です。このようなコードは通常、Apex トリガーに記述されており、そこに残せます。このタイプのコードのドメインパターンについては、後で紹介します。
-
セキュリティ - サービスレイヤーコード、およびそれによりコールされるコードは、デフォルトでユーザーセキュリティを適用した状態で実行してください。そのためには、
with sharing
修飾子を Apex サービスクラスで使用します (global 修飾子経由でこのようなコードを公開する場合には特に重要です)。Apex ロジックがユーザーの表示対象範囲外のレコードにアクセスする必要がある場合、コードは、できる限り短時間で、実行コンテキストを明示的に昇格する必要があります。適切なアプローチは、without sharing
修飾子を適用して非公開 Apex 内部クラスを使用することです。
-
マーシャリング - サービスレイヤーとのやり取りのアスペクトの処理方法を命令しないでください。なぜなら、エラー処理およびメッセージのようなセマンティックなど、特定のアスペクトはサービスのコール元に委ねたほうがよいためです。多くの場合、コール元はこれらを独自の手段で解釈および処理します。たとえば、Visualforce は
<apex:pagemessages>
を使用し、スケジュールジョブはたいてい、メール、Chatter 投稿、またはログを使用してエラーに対応します。つまり、このような場合、通常は、例外を投げて、Apex のデフォルトエラー処理セマンティックを活用する方法が最適です。または、サービスからコール元に部分的なデータベース更新フィードバックを提供します。この場合、適切な Apex クラスを検討して、そのタイプのリストを返します。システムDatabase.insert
メソッドは、このメソッド署名タイプのよい例です。
-
複合サービス - クライアントは複数のサービスコールを 1 つずつ実行できるものの、これにより、効率が低下したり、データベーストランザクションの問題が発生したりする可能性があります。複数のサービスコールを 1 つのサービスコールに内部的にまとめる複合サービスの作成をお勧めします。サービスレイヤーを、SOQL および DML の使用に関して、可能な限り最適化することも重要です。これは、詳細なサービスを公開できないということではありません。必要に応じて、より細かい 1 つのサービスを使用するオプションをコール元に提供する必要があるというだけです。
-
トランザクション管理とステートレス - 処理対象のプロセスの長さと管理対象の情報に関する要求は、通常、サービスレイヤーのクライアントにより異なります。たとえば、サーバーに対する 1 つの要求と、別々の範囲に分かれる複数の要求 (Apex 一括処理などの管理状態、または複数の要求で独自のページ状態を維持する複雑な UI) があります。状態管理でこれらの違いが存在する場合、サービスレイヤーへのメソッドコール内でデータベース操作およびサービス状態のカプセル化を行うことが最適です。言い換えると、サービスをステートレスにして、コールのコンテキストが独自の状態管理ソリューションを自由に使用できるようにします。また、データベースとのトランザクションの範囲は、各サービスメソッドに含まれていなければなりません。これにより、コール元は、たとえば独自の
SavePoints
でこれを考慮する必要がなくなります。
-
設定 - 変更のコミットやメールの送信を実行しないようにクライアントからサービスレイヤーに指示できるようにする管理機能を提供するなど、共通の設定や動作の上書きがサービスレイヤーに存在することがあります。このシナリオは、クライアントがプレビューまたは「もし ~ だったら」というタイプの機能を実装している場合に便利です。一貫性を保って実装する方法を検討してください (たとえば、Apex の DML メソッドのように、共有 Options パラメーターを使用するメソッドオーバーロード)。
Apex でのサービスの使用
さまざまな場所から OpportunitiesService.applyDiscounts
メソッドをどのように使用できるか確認しましょう。Lightning コンポーネントと Apex の一括処理をすべて、以下に示します。
次の例は、Lightning コンポーネント 経由で選択された 1 つの商談を処理します。選択した商談金額に適用する割引率を入力するようユーザーに求める Lightning コンポーネントがあるとします。エラー処理はサービス内ではなく、このフェーズで行われています。これは、Lightning コンポーネントには独自のエラー表示方法があるためです。
@AuraEnabled public void applyDiscount(Id opportunityId, Decimal discountPercentage) { try { // Apply discount entered to the current Opportunity OpportunitiesService.applyDiscounts( new Set<ID> { opportunityId }, discountPercentage); } catch(Exception e) { throw new AuraHandledException('Something went wrong: ' + e.getMessage()); } }
次の例は、Apex 一括処理実行メソッド経由で、まとまったレコードの処理を扱います。例外処理が、上記の Lightning コンポーネントの例と異なることがわかります。
public with sharing class OpportunityApplyDiscountJob implements Database.Batchable<SObject> { public Decimal DiscountPercentage {get;private set;} public OpportunityApplyDiscountJob(Decimal discountPercentage) { // Discount to apply in this job this.DiscountPercentage = discountPercentage; } public Database.QueryLocator start(Database.BatchableContext ctx) { // Opportunities to discount return Database.getQueryLocator( 'SELECT Id FROM Opportunity WHERE StageName = \'Negotiation/Review\''); } public void execute(Database.BatchableContext BC, List<sObject> scope) { try { // Call the service OpportunitiesService.applyDiscounts( new Map<Id,SObject>(scope).keySet(),DiscountPercentage); } catch (Exception e) { // Email error, log error, chatter error etc.. } } public void finish(Database.BatchableContext ctx) { } }
後の単元で、REST API 経由のサービスメソッドの公開について学習します。
Apex サービスレイヤーのその他の利点と考慮事項
この単元で扱わないトピックは、モックテストとパラレル開発のサービスの実装です。サービスは、メソッドで直接コードを記述するのとは対照的に、ファクトリパターンと Apex インターフェースを使用して、実装を動的に解決できます。このアプローチは、サービス関連のテスト範囲のエンジニアリングでの柔軟性を高めるために役立ちます。ただし、ファクトリには、いくつかの調整に加え、インターフェースを作成するフレームワーク、クラスを登録する手段、コードのその他の要素が必要です。これらの利用により、モックテストや、設定に基づくランタイムの柔軟性の面で価値が高まるでしょう。
また、サービスレイヤー設計を事前に定義することで、開発者や開発者チームの共同作業や並行作業がはかどります。サービスをコールする必要がある場合は、ダミーの実装を使用して、静的データを返せます。一方、サービスを実装する場合は、コール元に影響することなくコードに取り組めます。この開発スタイルは、契約による設計 (Dbc) と呼ばれることがあり、優れた方法です。
まとめ
アプリケーションのサービスレイヤーに取り組むと、再利用が進み、適応性が高まることで、エンジニアリングにおいて利点がもたらされます。また、今日のクラウド統合型の世界に必要不可欠なアプリケーションの API の実装を、クリーンかつ費用対効果の高い方法で実現できます。上記のカプセル化と設計の考慮事項をよく確認して、今後も変化し続ける革新の時代に、持続し、しっかりとした投資として残るアプリケーションのコア作りを始めましょう。
リソース
- 関心の分離 (Wikipedia)
- Martin Fowler’s Service Layer Pattern (Martin Fowler のサービスレイヤーパターン)
- Martin Fowler’s Enterprise Architecture Patterns (Martin Fowler のエンタープライズアーキテクチャパターン)