Skip to main content

Apply Service Layer Principles in Apex

Learning Objectives

After completing this unit, you’ll be able to:

  • Create a Service Apex class and make effective use of it in your application.
  • Expose a Service Apex class as an API.

Follow Along with Trail Together

Want to follow along with an expert as you work through this step? Take a look at this video, part of the Trail Together series.

(This clip starts at the 38:48 minute mark, in case you want to rewind and watch the beginning of the step again.)

Creating Services

Let's jump into some code! If you’re using a full encapsulation of the database and state management, one implementation approach is to use an appropriately named class with static methods representing the operations of the service.

The methods in your class represent the service operations, which access the information they need through the environment and parameters passed. The logic in the method updates the database or returns information in the method’s return type using custom Apex exceptions to indicate failure. The following example shows a service to apply a given discount to a set of Opportunities (and lines items, if present).

public with sharing class OpportunitiesService {
   public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) {
        // Validate parameters
        if(opportunityIds==null || opportunityIds.size()==0)
            throw new OpportunityServiceException('Opportunities not specified.');
        if(discountPercentage<0 || discountPercentage>100)
            throw new OpportunityServiceException('Invalid discount to apply.');
        // Query Opportunities and Lines (SOQL inlined for this example, see Selector pattern in later module)
        List<Opportunity> opportunities =
            [SELECT Amount, (SELECT UnitPrice FROM OpportunityLineItems)
             FROM Opportunity WHERE Id IN :opportunityIds];
        // Update Opportunities and Lines (if present)
        List<Opportunity> oppsToUpdate = new List<Opportunity>();
        List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>();
        Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100);
        for(Opportunity opportunity : opportunities) {
            // Apply to Opportunity Amount
            if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) {
                for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) {
                    oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor;
                    oppLinesToUpdate.add(oppLineItem);
                }
            } else {
                opportunity.Amount = opportunity.Amount * factor;
                oppsToUpdate.add(opportunity);
            }
        }
        // Update the database
        SavePoint sp = Database.setSavePoint();
        try {
            update oppLinesToUpdate;
            update oppsToUpdate;
        } catch (Exception e) {
            // Rollback
            Database.rollback(sp);
            // Throw exception on to caller
            throw e;
        }
    }
    public class OpportunityServiceException extends Exception {}
}

If we apply the configuration design consideration described earlier, we can add an overloaded version of the above service with an Options parameter, which allows the caller to instruct the service to skip committing the work. Returning the discounted values allows the client to implement a preview of the discounts that would be applied.

public static List<Decimal> applyDiscounts(
     List<Id> opportunityIds, Decimal discountPercentage, Options config)

The method signature in the full example above takes a list of IDs, which is also per the design considerations. However, only a single parameter for the discount was used. The assumption is that the same discount is applied to all opportunities. However, if allowing different discounts per opportunity is required, you can use a parameter class, as shown here.

public class OpportunityService {
    public class ApplyDiscountInfo {
        public Id OpportunityId;
        public Decimal DiscountPercentage;
    }
    public static void applyDiscounts(List<ApplyDiscountInfo> discInfos) {
      // Implementation...
    }
}

Exposing Services as APIs

Everyone loves APIs! Exposing your service logic to external parties through an API into your application is a must to develop a strong ecosystem of innovation and partner integrations around your products.

If you consider your service layer as fully tested, robust, and open to any client—and why wouldn’t it be because you’re using it as well, right?—then the simplest way to expose it to Apex developers is to modify the class and method modifiers from public to global. Boom!

global class OpportunityService {
    global class ApplyDiscountInfo {
        global Id OpportunityId;
        global Decimal DiscountPercentage;
    }
    global static void applyDiscounts(List<ApplyDiscountInfo> discInfos) {
       // Implementation...
    }
}

However, nothing in life is ever as easy as it sounds. If you’re creating an AppExchange package, using global has implications when changing method signatures between releases. Make sure you understand these implications.

It’s also worth considering exposing your API for off-platform callers, such as mobile or IoT. One way to do this is via the REST protocol. Here is a custom Apex REST API implementation.

@RestResource(urlMapping='/opportunity/*/applydiscount')
global with sharing class OpportunityApplyDiscountResource {
    @HttpPost
    global static void applyDiscount(Decimal discountPercentage) {
        // Parse context
        RestRequest req = RestContext.request;
        String[] uriParts = req.requestURI.split('/');
        Id opportunityId = uriParts[2];
        // Call the service
        OpportunitiesService.applyDiscounts(
            new Set<Id> { opportunityId }, discountPercentage);
    }
}

The ID of the opportunity is taken from the URI, and the discount percentage is taken from the posted information. As with the JavaScript Remoting example in the prior unit, exception handling is left to the caller. The platform marshalls exceptions into the appropriate JSON or XML response.

Pro Tip: Consider exposing some invocable methods so that users of the Flow Builder can access your Service layer functionality without writing code. Aren't platforms awesome!

Resources

在 Salesforce 帮助中分享 Trailhead 反馈

我们很想听听您使用 Trailhead 的经验——您现在可以随时从 Salesforce 帮助网站访问新的反馈表单。

了解更多 继续分享反馈