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

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

学習の目的

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

  • Martin Fowlerの エンタープライズアプリケーションアーキテクチャパターンで紹介されているサービスパターンの根幹について説明する。
  • サービスレイヤに属する Apex コードを判断する。
  • アプリケーションのアーキテクチャおよびプラットフォームにサービスレイヤがどのように適合するか議論する。
  • プラットフォームのベストプラクティスに沿って機能するサービスレイヤを設計する。

はじめに

前の単元では、ソフトウェアアーキテクトをアプリケーションロジックのレイヤリングの検討に集中させる手段として、SOC を紹介しました。この単元では、アプリケーションの他のレイヤやコンシューマ (API など) への重要なエントリポイントとしてのサービスレイヤの定義および使用について説明します。

サービスレイヤは、「利用可能な操作セットの設定と各操作のアプリケーションレスポンスの調整を行うサービス層によって、アプリケーションの境界を定義する」Martin Fowler / Randy Stafford、EAA パターン

サービスレイヤは、ビジネスタスク、計算、プロセスを実装するコードを明確かつ厳密にカプセル化することに役立ちます。サービスレイヤが、異なるコンテキスト (モバイルアプリケーション、UI フォーム、リッチ Web UI、各種 API など) で確実に使用できるようにすることが重要です。また、今後の時代や要求の変化に対応するために、純粋かつ抽象的であり続ける必要があります。次のセクションでは、Force.com のベストプラクティスとガバナ制限を踏まえつつ、Apex でのサービスレイヤ実装の作成ガイドラインを定義します。

サービスレイヤの利用者とは?

この質問に「イカした人」と答える人もいるかもしれませんが、実際のところ、サービスレイヤのコンシューマは「クライアント」と呼ばれます。クライアントは、サービスレイヤコードを呼び出します。サービスレイヤとやりとりするのは、人ではなく、UI コントローラ、Apex 一括処理など、ユーザやシステムとやりとりするコードです。

クライアントの一例として、Visualforce または Lightning コントローラクラスで記述されているコードがあります。ただし、考慮すべきサービスレイヤコードのクライアント (コンシューマ) は、ほかにもたくさんあります。候補の一覧を作成するには、Force.com プラットフォームの Apex ロジックを呼び出せるすべての手段を検討してください。

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

Note

メモ

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

ご想像どおり、サービスレイヤロジックは、他のレイヤおよび目的向けに作成された Apex コードに非常に簡単に漏れてしまいます。そうなると、一貫性が低下し、その影響はエンドユーザエクスペリエンスにまで及ぶため、サービスレイヤ実装の価値が損なわれます。たとえば、Force.com のいくつかのテクノロジを通じて公開されている特定の機能を使用してユーザがアプリケーションとやりとりする場合や、作成した Lightning コンポーネント経由、および Apex REST サービス経由で特定の計算を公開する場合に考えられます。どちらの場合においても、動作は一貫している必要があります。次のセクションでは、サービスレイヤの設計と責任、およびサービスレイヤを使用するコードの期待事項について説明します。

プラットフォームのイノベーションと適応性

上記のテクノロジは、長年をかけて、新しい機能としてプラットフォームに徐々に導入されてきました。特定の機能に結びついた方法でコードが記述されていて、たびたびリファクタリングしなければならない場合を想像してみてください。もし、それまでの領域のコードのリファクタリングについて心配する必要がないとしたら、これらの機能や新しい機能に対してアプリケーションを採用したり適応させたりすることは非常に簡単です。下手すると、リファクタリングによって既存の機能に問題が発生する心配があるため、コードを重複させることもあります。これは問題です。

設計の考慮事項

  • 命名規則 - サービスレイヤは、さまざまなクライアントで意味が通じるように抽象的である必要があります。多くの場合、クラス、メソッド、およびパラメータ名で使用する動詞や名詞が採用されます。特定のクライアントコール元に関連したものではなく、アプリケーションやタスクの一般的な用語で表してください。たとえば、ビジネス操作に基づいたメソッドの名前の例は InvoiceService.calculateTax(...)、特定のクライアントの使用操作に基づいたメソッドの名前の例は InvoiceService.handleTaxCodeForACME(...) です。後者は少し不便です。

  • プラットフォーム / コール元の共鳴 - プラットフォームのベストプラクティス、特に一括処理化をサポートするメソッド署名を設計します。Force.com のコードの大きな関心の 1 つは一括処理化です。リストでコールするサービスと、1 つのパラメータセットでコールするサービスの場合を考えてみてください。たとえば、InvoiceService.calculateTax(List<TaxCalulation> taxCalulations) の場合、メソッドのパラメータは一括処理化に対応しますが、 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 パラメータを使用するメソッドオーバーロード)。

Note

メモ

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

Apex でのサービスの使用

次にいくつかコードを見てみましょう。商談レイアウトにカスタムボタンがあり、ボタンを押すと Visualforce ページが表示されて、商談金額に対して、または存在する場合には関連する商談品目に対して適用する割引率がユーザに表示されるとします。

さまざまな場所から OpportunitiesService.applyDiscounts メソッドをどのように使用できるか確認しましょう。Visualforce、Apex 一括処理、JavaScript Remoting が以下に示してあります。次の例は、StandardController 経由で選択された 1 つの商談を処理します。Visualforce はエラーを独自の方法で表面化するため、コントローラのエラー処理は、サービスではなくコントローラにより完了されています。

public PageReference applyDiscount() {
    try {
        // Apply discount entered to the current Opportunity
        OpportunitiesService.applyDiscounts(
            new Set<ID> { standardController.getId() }, DiscountPercentage);
    } catch (Exception e) {
        ApexPages.addMessages(e);
    }          
    return ApexPages.hasMessages() ? null : standardController.view();
}

次の例は、StandardSetController 経由で複数の商談を処理します。

public PageReference applyDiscounts() {

    try {
        // Apply discount entered to the selected Opportunities
        OpportunitiesService.applyDiscounts(
           // Tip: Creating a Map from an SObject list gives easy access to the Ids (keys)
           new Map<Id,SObject>(standardSetController.getSelected()).keyValues(),
           DiscountPercentage
        );
    } catch (Exception e) {
        ApexPages.addMessages(e);
    }          
    return ApexPages.hasMessages() ? null : standardController.view();               
}

次の例は、Apex 一括処理実行メソッド経由で、まとまったレコードの処理を扱います。例外処理が、上記の Visualforce コントローラの例と異なることがわかります。

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) { }    

}

この例は、JavaScript Remoting 経由でサービスメソッドをラップして、公開します。JavaScript Remoting には例外が投げられたときの組み込みのマーシャリング機能があるため、ここでは例外はキャッチされません。JavaScript クライアントコードに例外を渡し、組み込みの機能でキャッチさせて、これを活用します。

public class OpportunityController {
    @RemoteAction
    public static void applyDiscount(Id opportunityId, Decimal discountPercent) {
        // Call service
        OpportunitiesService.applyDiscounts(new Set<ID> { opportunityId }, discountPercent);
    }
}

後の単元で、REST API 経由のサービスメソッドの公開について学習します。

Apex サービスレイヤのその他の利点と考慮事項

この単元で扱わないトピックは、モックテストとパラレル開発のサービスの実装です。サービスは、メソッドで直接コードを記述するのとは対照的に、ファクトリパターンと Apex インターフェースを使用して、実装を動的に解決できます。このアプローチは、サービス関連のテスト範囲のエンジニアリングでの柔軟性を高めるために役立ちます。ただし、ファクトリには、いくつかの調整に加え、インターフェースを作成するフレームワーク、クラスを登録する手段、コードのその他の要素が必要です。これらの利用により、モックテストや、設定に基づくランタイムの柔軟性の面で価値が高まるでしょう。

また、サービスレイヤ設計を事前に定義することで、開発者や開発者チームの共同作業または平行作業が促進します。サービスをコールする必要がある場合は、ダミーの実装を使用して、静的データを返せます。一方、サービスを実装する場合は、コール元に影響することなくコードに取り組めます。この開発スタイルは、契約による設計 (Dbc) と呼ばれることがあり、優れた方法です。

まとめ

アプリケーションのサービスレイヤに取り組むと、再利用が進み、適応性が高まることで、エンジニアリングにおいて利点がもたらされます。また、今日のクラウド統合型の世界に必要不可欠なアプリケーションの API の実装を、クリーンかつ費用対効果の高い方法で実現できます。上記のカプセル化と設計の考慮事項をよく確認して、今後も変化し続ける革新の時代に、持続し、しっかりとした投資として残るアプリケーションのコア作りを始めましょう。

リソース