3

After reading some books about the Service Layer I realized all the methods implemented in this layer are static. What's the reason to do that?

https://trailhead.salesforce.com/content/learn/modules/apex_patterns_sl/apex_patterns_sl_apply_sl_principles

2 Answers 2

4

In addition to @sfdcfox answer, let me throw in how the fflib pattern supports dependency injection for services during unit testing

Here's a Contact Service

public inherited sharing class ContactsService {

 public static void scrubPII(Set<Id> contactIds) {
    service().scrubPII(contactIds);
 }

 public static void someOtherService(...) {
    service().someOtherService(...);

 }

 // Factory creation of service implementation
 private static IContactsService service() {
    return (IContactsService) Application.Service.newInstance(IContactsService.class);
 }

}  

Here's Application.cls

public class Application {
  ...
 public static final fflib_Application.ServiceFactory Service =
    new fflib_Application.ServiceFactory( 
        new Map<Type, Type> {
                IAccountsService.class              => AccountsServiceImpl.class,
                IContactsService.class              => ContactsServiceImpl.class,
        ... 

        });
}

Here's the ContactsService implementation that can be mocked in a testmethod

public inherited sharing ContactsServiceImpl implements IContactsService {

 public void scrubPII(Set<Id> contactIds) {
  // do the work
 }

 public void someOtherService(...) { ... }
}

A class that invokes ContactsService

public class Foo {
  public void doWork() {
    ...
    ContactsService.scrubPII(someSetOfContactIds);
  }
}

Finally, a test method for doWork() that needs to stub out the ContactsService (perhaps to verify it is called with proper args or to throw a mock exception to test exception handling

I'm using ApexMocks here to show mocking; there are other mocking frameworks like AMoss. ApexMocks and fflib are documented in working together in the Andy Fawcett Lighting Enterprise Architecture book in the Unit Testing chapter.

@IsTest static void givenContactsVerifyDoWork() {
  fflib_ApexMocks apm = new fflib_ApexMocks(); 

  // Given a mock ContactsService
  ContactsServiceImpl mockContactsService = (ContactsServiceImpl) mocks.mock(ContactsServiceImpl.class);
  // if the service methods return things or you want to mock an exception, add `mocks.startStubbing() -... - mocks.stopStubbing()` block
  Application.Service.setMock(IContactsService.class,mockContactsService); // inject

 // given some test data (mocked with DML or as sobjects, depends on what you're testing
 ...
 // when the code-under-test is invoked
 new Foo().doWork();

 // then verify ContactsService.scrubPii called with expected args
 ((ContactsServiceImpl)mocks.verify(mockContactsService,mocks.times(1)
                        .description('service sb called for mocked ContactIds')))
        .scrubPII(someSetOfMockedContactIds);
  
}

Summary

While the service methods are static, if you want to mock a service method, you need to generate an object. That is where the service() method comes in within the ContactsService.cls. service() invokes Application to get the runtime version of the relevant method. Application will grab the mocked implementation if it exists otherwise will use the "production" implementation. Note that the StubAPI won't allow mocking of static methods.

All of this might seem long-winded if you are just getting into fflib but the pattern is super powerful at supporting unit testing through mockable service methods, domain methods, selector methods and unit of work.

6

I've gone in to a lot of detail about static and instance methods. Generally speaking, a method like the one written in the Trailhead falls under the category of a "utility method." A utility method is one that has no internal state (or needs only static storage), and which doesn't need dependency injection applied to it directly via the Stub API.

The benefits of static methods include:

  • Less code to write
  • Reduces heap/memory usage
  • Reduces CPU usage

There is no inherent benefit to using an instance method when a static method will suffice. You will always end up writing the same or more amount of code, and suffer CPU and heap penalties, when you otherwise don't need to.

Instance methods, on the other hand, are useful for a large range of features, including implementing sorting algorithms for List.sort, using a custom class as a key for a Map or a value in a Set, storing state for use in a JSON object, a Visualforce page, injecting code into the class, particularly as a means of unit test mocking, etc.

In summary, we don't use instance methods in a Service Layer because, for the most part, it offers no benefit and actually harms the performance of code, even if only-so-slightly. I would also personally argue that it's slightly less readable when you're constructing objects all over the place:

new OpportunityService().validateInput(opps);
new OpportunityService().buildProductPages(opps);
new OpportunityService().applyDiscounts(oppIds, 0.2);
new OpportunityService().updateCreditRating(oppIds);

Versus simply:

OpportunityService.validateInput(opps);
OpportunityService.buildProductPages(opps);
OpportunityService.applyDiscounts(oppIds, 0.2);
OpportunityService.updateCreditRating(oppIds);

This last point is a matter of personal opinion, but the other points from above are objectively true. You will incur performance penalties for no good reason if you're instantiating unnecessarily.

In fact, consider some of the required static situations, like @RemoteAction methods, @AuraEnabled methods, and future methods, versus some required instance situations, like Batchable classes, Queueable classes, and Schedulable classes. In Camp Static, you have things that do not inherently carry state and do not require state, while in Camp Instance, you have things that can carry state and generally do.

The way of Team Instance always requires more code, but also gives you more functionality in return. If you don't need the extra functionality, don't enable it. It has no practical benefits and carries only additional penalties.

2
  • 2
    Taking a purist's position I would say that all methods should be testable without having to rely on methods that they themselves call, and thus you should always be able to mock out all (non-framework/factory) methods. OK, I don't take it that far, but for me testability outweighs the slight performance/heap impact pretty much any day. Especially when your system has even modest complexity and could benefit from polymorphism or any of the design patterns (GoF or other).
    – Phil W
    Aug 2, 2021 at 20:48
  • @PhilW When possible, I prefer to test every possible method. That means that Service layer methods will be called at least once by themselves, then they will also usually be called in integration tests, of which is more about testing the upper-level integrations and performance. But both are potentially valid solutions.
    – sfdcfox
    Aug 2, 2021 at 20:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .