Skip to main content

サービスレイヤーの原則について

学習の目的

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

  • 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 ロジックを呼び出せるすべての手段を検討してください。

Salesforce プラットフォームの Apex ロジックを呼び出す手段: Apex UI コントローラー、Apex Web サービス、Apex REST サービス、呼び出し可能なメソッド、受信メールハンドラー、Apex 一括処理、スケジュール済みの Apex、および Queueable

メモ

メモ

Apex トリガーは、ロジックがアプリケーションのドメインレイヤーに属しているため、含まれていません。ドメインレイヤーは、オブジェクトや、アプリケーションのレコードの操作と密接に結びついています。ドメインロジックは、サービスレイヤー内、およびプラットフォーム UI や API 経由で直接的または間接的にコールされます。

ご想像どおり、サービスレイヤーロジックは、他のレイヤーおよび目的向けに作成された 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 では、要求がエラーなしで完了し、未処理の例外のイベントでロールバックされた場合、データベーストランザクションが自動的にコミットされます。ただし、これらの例外を処理するプラットフォームは、エンドユーザーがすべてにアクセスできるわけではなかったり (Apex 一括処理ジョブ)、見やすいデザインでなかったり (白いページ、黒いテキスト) することが多く、エラーが投げられた状態で要求が完了可能であることは、ユーザーエクスペリエンスとして望ましくありません。このことから、開発者は、通常、例外をキャッチして、適宜転送します。この方法の副次的影響としては、プラットフォームがこれを要求の有効な完了と見なし、発生したエラーまでの挿入または更新対象のレコードをコミットすることが考えられます。ステートレスおよびトランザクション管理に関する上記のサービスレイヤー設計の原則に従うことで、この問題を回避できます。

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 の実装を、クリーンかつ費用対効果の高い方法で実現できます。上記のカプセル化と設計の考慮事項をよく確認して、今後も変化し続ける革新の時代に、持続し、しっかりとした投資として残るアプリケーションのコア作りを始めましょう。

リソース

Salesforce ヘルプで Trailhead のフィードバックを共有してください。

Trailhead についての感想をお聞かせください。[Salesforce ヘルプ] サイトから新しいフィードバックフォームにいつでもアクセスできるようになりました。

詳細はこちら フィードバックの共有に進む