Implementation of design guidelines

Having studied the Separation of Concerns in the previous chapter and reflected on the previous illustration, the following design guidelines help ensure that the Service layer is agnostic towards the caller, easy to locate, and encourages some Force.com best practices, such as bulkification. Note that bulkification is not just a concept for Apex Triggers; all the logic in your application must make efficient use of governed resources.

Naming conventions

A colleague of mine used to reference the following when talking about naming:

"There are only two hard things in Computer Science: cache invalidation and naming things."

- Phil Karlton

In my career so far, I have come to realize that there is some truth in this statement. Naming conventions have never been so important on Force.com as it is currently without a means to group or structure code files, using a directory structure for example. Instead, all classes are effectively in one root folder called /classes.

Thus, it comes down to the use of naming conventions to help clarify purpose (for developers, both new and old, working on the codebase) and to which layer in your architecture a class belongs. Naming does not, however, stop at the class name level; the enums, methods, and even parameter names all matter.

Tip

It is a good idea to work with those designing and documenting your application in order to establish and agree on an application vocabulary of terms. This is not only a useful reference for your end users to get to know your application, but also helps in maintaining consistency throughout your user interface and can also be used in the implementation of it, right down to the method and class names used. This comes in handy, especially if you are planning on exposing your Service layer as a public API (I'm a big fan of clear self-documenting APIs).

The following points break down some specific guidelines for the Service layer, though some are also applicable throughout your application code:

  • Avoid acronyms: In general, try to avoid using acronyms (as far as possible). While these might be meaningful to those experienced in working on the application, newcomers will find it harder to navigate the code as they learn the application functionality and codebase. This applies more so if you're using your Service layer as an API. Though it can be tempting from a typing perspective, a good editor with autocompletion should resolve this concern pretty well. Widely used and understood acronyms like ID are reasonable.
  • Class names: Ending your class name with the Service suffix allows developers to filter easily in order to find these critical classes in your application codebase. The actual name of the service can be pretty much anything you like, typically a major module or a significant object in your application. Make sure, however, that it is something from your application's vocabulary. If you've structured your application design well, your service class names should roughly fall into the groupings of your application's modules; that is, the naming of your Service layer should be a by-product of your application's architecture, and not a thing you think up when creating the class.
    • Some bad examples are UtilService, RaceHelper, BatchApexService, and CalcRacePointsService. These examples either use acronyms that are platform feature bias or potentially too contextualized. Classes with the name Helper in them are often an indication that there is a lack of true understanding of where the code should be located; watch out for such classes.
    • Some good examples are CommonService, RaceService, and SeasonService. These examples clearly outline some major aspects of the application and are general enough to permit future encapsulation of related operations as the application grows.
  • Method names: The public or global method names are essentially the business operations exposed by the service. These should also ideally relate or use terms expressed in your application's end user vocabulary, giving the same thought to these as you would to a label or button for the end user, for example. Avoid naming them in a way that ties them to their caller; remember that the Service layer doesn't know or care about the caller type.
    • Some bad examples are RaceService.recalcPointsOnSave, SeasonService.handleScheduler, and SeasonService.issueWarnings. These examples are biased either towards the initial caller use case or towards the calling context. Nor does the handleScheduler method name really express enough about what the method is actually going to perform.
    • Some good examples are RaceService.awardChampionshipPoints, SeasonService.issueNewsLetter, and DriverService.issueWarnings. These examples are named according to what they do, correctly located, and unbiased to the caller.
  • Parameter names and types: As with method and class names, keep them focused on the data they represent, and ideally use consistent terms. Keep in mind that the type of your parameter can also form a part of the meaning of the parameter, and may help in some cases with the implementation of the service method. For example, if the parameter is intended to receive a list of IDs, consider using Set instead of List, as typically, duplicate IDs in the list are unwanted. For the Map parameters, I always try to use the somethingABySomethingB convention in my naming so that it's at least clear what the map is keyed by. In general, I actually try to apply these conventions to all variables, regardless of them being method parameters.
    • Some bad examples are List<String>driverList and Map<String, DriverPerformance>mapOfPerformance. These examples are either don't use the correct data type and/or are using unclear data types as to the list or map contents; there is also some naming redundancy.
    • Some good examples are Set<Id>driverIds and Map<String,DrivePerformance>drivePerformanceByName. These examples use the correct types and help in documenting how to use the Map parameter correctly; the reader now knows that the String key is a name. Another naming approach would be somethingsToName, for example. Also, the Map parameter name no longer refers to the fact that it is a map because the type of parameter communicates this well enough.
  • Inner class names: While there is no formal structure to Apex classes in the form of a typical directory facility (or package structure, in Java terms), Apex does support the concept of inner classes to one level. These can sometimes be used to define classes that represent parameter data that is only relevant to the service methods. Because inner classes are always qualified by the name of the parent class, you do not need to repeat it.
    • Some bad examples are SeasonService.SeasonSummary and DriverService.DriverRaceData. These examples repeat the parent class name.
    • Some good examples are SeasonService.Summary and DriverService.RaceDetails. These examples are shorter as they are qualified by the outer class.

These guidelines should not only help you to ensure that your Service layer remains more neutral to its callers and thus more consumable now and in the future, but also to follow some best practices around the platform. Finally, as we will discuss in Chapter 9, Providing Integration and Extensibility, following these guidelines leaves things in good shape to expose as an actual API, if desired.

Tip

If you're having trouble agreeing on the naming, you can try a couple of things. Firstly, try presenting the name and/or method signatures to someone not as close to the application or functionality to see what they interpret from it. This approach could be considered as parallel to a popular user experience acceptance testing approach, fly-by reviews. Obviously, they may not tell you precisely what the actual intent is, but how close or not they get can be quite useful when deciding on the naming.

Secondly, try writing out pseudo code for how calling code might look; this can be useful to spot redundancy in your naming. For example, SeasonService.issueSeasonNewsLetter(Set<Id>seasonIdList) could be reduced to SeasonService.issueNewsLetter(Set<Id>seasonIds), since the scope of the method within the SeasonService method need not include Season again, and the parameter name can also be shortened since its type infers a list.

Bulkification

It's well known that the best practice of Apex is to implement bulkification within Apex Triggers, mainly because they can receive and process many records of the same type. This is also true for the use of StandardSetController classes or Batch Apex, for example.

As we identified in the previous chapter, handling bulk sets of records is a common requirement. In fact, it's one that exists throughout your code paths, since DML or SOQL in a loop at any level in your code will risk hitting governor limits.

For this reason, when designing methods on the Service layer, it is appropriate that you consider list parameters by default. This encourages development of bulkified code within the method and avoids the caller having to call the Service method in a loop.

The following is an example of non-bulkified code:

RaceService.awardChampionShipPoints(Id raceId)

The following is another example of non-bulkified code:

RaceService.awardChampionShipPoints(Set<Id>raceIds)

A non-bulkified method with several parameters might look like this, for example:

RaceService.retireFromRace(Id raceId, String reason)

A bulkified version of the preceding method signature can utilize an Apex inner class as described earlier and shown in the following example in the Defining and passing data sub-section.

Note

Sometimes, implementing bulkified versions of service methods are more costly and complex than what the callers will realistically require, so this should not be seen as a fixed guideline, but should at least always be considered.

Sharing rules enforcement

As discussed in the previous chapter, by default Apex code runs in system mode, meaning no sharing rules are enforced. However, business logic behavior should, in general, honor sharing rules. To avoid sharing information to which the user does not have access, sharing rule enforcement must be a concern of the Service layer.

Salesforce security review requires Apex controller class entry points to honor this, although your Service layer will be called by these classes and thus could inherit this context. Keep in mind that your Service layer is effectively an entry point for other points of access and integrations (as we will explore in a later chapter and throughout the book).

Thus the default concern of the Service layer should be to enforce sharing rules. Code implemented within the Service layer or called by it should inherit this. Code should only be elevated to running in a context where sharing rules are ignored when required, otherwise known as the without sharing context. This would be in cases where the service is working on records on behalf of the user. For example, a service might calculate or summarize some race data but some of the raw race data records (from other races) may not be visible to the user.

To enforce sharing rules by default within a Service layer the with sharing keyword is used on the class definition as follows:

public with sharing class RaceService

Other Apex classes you create, including those we will go on to discuss around the Selector and Domain patterns, should leave it unspecified such that they inherit the context. This allows them to be reused in either context more easily.

If a without sharing context is needed, a private inner class approach, as shown in the following example, can be used to temporarily elevate the execution context to process queries or DML operations in this mode:

// Class used by the Service layer
public class SomeOtherClass {

  // Work in this method inherits with sharing context from Service
  public static void someMethod { 
    // Do some work in inherited context
    // ...

    // Need to do some queries or updates in elevated context
    new ElevatedContext().restOfTheWork(workToDo);
  }

  private void restOfTheWork(List<SomeWork>workToDo) {
    // Additional work performed by this class
    // ...
  }

  private without sharingclass ElevatedContext {
    public void restOfTheWork(List<SomeWork>workToDo) {
      // Do some work in a elevated (without sharing) context
      SomeOtherClass.restOfWork(workToDo);
      }
  }

}

Note you can consider making the ability to run logic, a parameter of your Service layer if you feel certain callers will want to disable this enforcement. The preceding code sample could be adapted to conditionally execute the restOfWork method directly or via the ElevatedContext inner class in this case.

Note

Object and field level security are also an important consideration when deciding when to enforce and when not. Later chapters that focus more on the Domain and Selector patterns will discuss this topic further.

Defining and passing data

While defining data to be exchanged between the Service layer and its callers, keep in mind that the responsibility of the Service layer is to be caller-agnostic. Unless you're explicitly developing functionalities for such data formats, avoid returning information through JSON or XML strings; allow the caller (for example, a JavaScript remoting controller) to deal with these kinds of data-marshalling requirements.

As per the guidelines, using inner classes is a good way to express and scope data structures used by the service methods. The following code also illustrates a bulkified version of the multi-parameter non-bulkified method shown in the previous section.

Tip

Thinking about using inner classes this way can also be a good way to address the symptom of primitive obsession (http://c2.com/cgi/wiki?PrimitiveObsession).

Have a look at the following code:

public class ContestantService{

  public class RaceRetirement{
  public Id contestantId;
    public String reason;
  }

  public static void retireFromRace(List<RaceRetirement> retirements) {
    // Process race retirements... 
  }
}

Tip

Try to avoid too much service coupling by reusing inner classes between services. If this is starting to happen, it may be an indication that you perhaps need a new shared or common service.

Always keep in mind that the service method really only needs the minimum information to do its job, and express this through the method signature and related types so that callers can clearly see that only that information is required or returned. This avoids doubt and confusion in the calling code, which can result in it passing too little or redundant information.

The preceding example utilizes read and write member variables in the RaceRetirement class, indicating both are required. The inner class of the RaceRetirement class is only used as an input parameter to this method. Give some consideration before using an inner class such as both input and output type, since it is not always clear which member variables in such classes should be populated for the input use case or which will be populated in the output case.

However, if you find such a need for the same Apex type to be used as both an output and input parameter, and some information is not required on the input, you can consider indicating this via the Apex property syntax by making the property read-only. This prevents the caller from populating the value of a member field unnecessarily. For example, consider the following service methods in the RaceService method:

public static Map<Id, List<ProvisionalResult>>calculateProvisionalResults(Set<Id>raceIds) 
{
  // Implementation 
}

public static void applyRaceResults (Map<Id, List<ProvisionalResult>> provisionalResultsByRaceId)
{
  //Implementation
}

public class ProvisionalResult{
  public Integer racePosition {get; set;}
  public Id contestantId {get; set;}
  public String contestantName {get; private set;} 
}

While calling the calculateProvisionalResults method, the contestantName field is returned as a convenience, but is marked as read-only since it is not needed when applied in the context of the applyRaceResults method.

Considerations when using SObject in the Service layer interface

In the following example, the Service method appears to add the Race records as the input, requiring the caller to query the object and also to decide which fields have to be queried. This is a loose contract definition between the Service method and the caller:

RaceService.awardChampionShipPoints(List<Race__c> races)

A better contract to the caller is to just ask for what is needed, in the case of the following example, the IDs:

RaceService.awardChampionShipPoints(Set<Id>raceIds)

Even though IDs that relate to records from different object types could be passed (one might consider performing some parameter validation to reject such lists), this is a more expressive contract, focusing on what the service really needs. The caller now knows that only the ID is going to be used.

In the first example, when using the Race__c SObject type as a parameter type, it is not clear which fields callers need to populate, which can make the code fragile, as it is not something that can be expressed in the interface definition. Also, the fields required within the service code could change and require caller logic to be refactored. This could also be an example of failing in the encapsulation concern of the business logic within the Service layer.

It can be said that this design approach incurs an additional overhead, especially if the caller has already queried the Race record for presentation purposes and the service must then re-query the information. However, in such cases, maintaining the encapsulation and reuse concerns can be more compelling reasons. Careful monitoring of this consideration makes the service interface clearer, better encapsulated, and ultimately more robust.

Transaction management

A simple expectation of the caller when calling the Service layer methods is that if there is a failure via an exception, any work done within the Service layer up until that point is rolled back. This is important as it allows the caller to handle the exception without fear of any partial data being written to the database. If there is no exception, then the data modified by the Service method can still be rolled back, but only if the entire execution context itself fails, otherwise the data is committed.

This can be implemented using a try/catch block in combination with Database.Savepoint and the rollback method, as follows:

public static void awardChampionshipPoints(Set<Id>raceIds){
  // Mark the state of the database
System.SavepointserviceSavePoint = Database.setSavePoint();
Try{
    // Do some work 
  } catch (Exception e){
    // Rollback any data written before the exception
Database.rollback(serviceSavePoint);
    // Pass the exception on for the caller to handle
    throw e;
  }
}

Later in this chapter, we will look at the Unit Of Work pattern, which helps manage the Service layer transaction management in a more elegant way than having to repeat the preceding boilerplate code in each Service method.

Compound services

As your Service layer evolves, your callers may find themselves needing to call multiple Service methods at a time, which should be avoided.

To explain this, consider that a new application requirement has arose for a single custom button to update the Drivers standings (their position in the overall season) in the championship with a feature to issue a new season newsletter.

The code behind the Apex controller action method might look like the following code:

try {
  Set<Id> seasons = new Set<Id> { seasonId };
  SeasonService.updateStandings(seasons); 
  SeasonService.issueNewsLetters(seasons);
}
catch (Exception e) {
  ApexPages.addMessage(e);
}

The problem with the preceding code is that it erodes the encapsulation and thus reuses the application Service layer by putting functionality in the controller, and also breaks the transactional encapsulation.

For example, if an error occurs while issuing the newsletter, the standings are still updated (since the controller handles exceptions), and thus, if the user presses the button again, the driver standings in the championship will be updated twice!

The best way to address this type of scenario, when it occurs, is to create a new compound service, which combines the functionality into one new service method call. The following example also uses an inner class to pass the season ID and provide the issue newsletter option:

try {

  SeasonService.UpdateStandingsupdateStandings = new SeasonService.UpdateStandings();
  updateStandings.seasonId = seasonId;
  updateStandings.issueNewsletter = true;

  SeasonService.updateStandings(new List <SeasonService.UpdateStandings> { updateStandings })
}

catch (Exception e) {
  ApexPages.addMessage(e);
} 

It may be desirable to retain the original Service methods used in the first example in cases where this combined behavior is not always required.

Later in this chapter, we will see how the implementation of the preceding new Service method will reuse the original methods and thus introduce the ability for existing Service methods to be linked to each other.

A quick guideline checklist

Here is a useful table that summarizes the earlier guidelines:

When thinking about...

The guidelines are...

Naming conventions

  • Suffix class names with service
  • Stick to application vocabulary
  • Avoid acronyms
  • Class, method, and parameter names matter
  • Avoid redundancy in names
  • Utilize inner classes to scope by service

Sharing Rules

  • Use the with sharing keyword on Service classes
  • Elevate to without sharing only when needed and only for as short a time as possible

Bulkification

  • On Service methods, utilize list parameters over single parameters to encourage optimized service code for bulk callers
  • Single instance parameters can be used where it's overly expensive to engineer bulkified implementations and there is little functional benefit

Defining and passing data

  • Keep data neutral to the caller
  • Leverage inner classes to scope data
  • Pass only what is needed

Transaction management

  • Callers can assume that work is rolled back if exceptions are raised by the service methods, so they can safely handle exceptions themselves
  • Wrap service method code in SavePoint

Compound services

  • Maintain the Service layer's functional encapsulation by monitoring for multiple service method calls in one execution context
  • Create new service methods to wrap existing ones or merge existing methods as needed
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.54.136