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.
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.
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:
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.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.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.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.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.RaceService.awardChampionshipPoints
, SeasonService.issueNewsLetter
, and DriverService.issueWarnings
. These examples are named according to what they do, correctly located, and unbiased to the caller.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.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.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.SeasonService.SeasonSummary
and DriverService.DriverRaceData
. These examples repeat the parent class name.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.
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.
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.
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.
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.
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... } }
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.
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.
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.
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.
Here is a useful table that summarizes the earlier guidelines:
When thinking about... |
The guidelines are... |
---|---|
Naming conventions |
|
Sharing Rules |
|
Bulkification |
|
Defining and passing data |
|
Transaction management |
|
Compound services |
|
18.224.54.136