Read more at jamessimone.net, or return to the homepage





Idiomatic Salesforce Apex

We often hear the word "idiomatic" applied to programming languages to express the language-specific way of accomplishing routinely encountered problems. In this post, we dive into how to write idiomatic Salesforce Apex to make the most of each line of code as I refactoring some existing vendor code into an easier to understand format.

linkThe Problem

Following up on the Apex Logging Service post, I was looking to push exceptions generated in Apex to Rollbar. Organizations, if they're using plenty of services beyond Salesforce, will undoubtedly be using a centralized logging solution. I've seen many ELK-stack approaches out there, but Rollbar (and competitors like Sumo Logic) is commonly seen on the web development side as an easy vendor to opt into, without having to worry about hosting or dashboard building. Once a company's using a vendor for one side of their logging approach, you can best believe that getting any other service's exceptions into said solution is going to be desirable — centralize where you view exceptions being generated, and you increase the visibility of all code-related issues.

At the time, I naively assumed that because Rollbar had an Apex installed package, my conscience could rest easy going with the provided solution. I was wrong. Upon installing the Rollbar package into a sandbox, I realized quickly that there were going to be a few issues (I want to use this as an opportunity to say that I love Rollbar. I think they're a great company that provides a great service. I use their code as an opportunity to make a point in this post; I'm not trying to trash it):

That being said ... no need to reinvent the wheel, I thought. You can't see namespaced code in your org unless it's a global class (and even then you can't see the full object) ... but that wasn't the case for this code, so I dove right in.

I had already seen via the documentation on Rollbar's Apex documentation page that they were employing the Singleton pattern for accessing the logger, so that was the first thing I chose to look at:

classes/Rollbar.cls
1linkpublic with sharing class Rollbar {

2link

3link public static Rollbar instance() {

4link if (Rollbar.instance == null) {

5link Rollbar.instance = new Rollbar();

6link }

7link

8link return Rollbar.instance;

9link }

10link

11link public static Rollbar init()

12link {

13link return Rollbar.init(

14link RollbarSettings__c.getInstance().AccessToken__c,

15link UserInfo.getOrganizationName()

16link );

17link }

18link

19link public static Rollbar init(String accessToken, String environment) {

20link return Rollbar.init(new Config(accessToken, environment));

21link }

22link

23link public static Rollbar init(Config config) {

24link Rollbar instance = instance();

25link instance.config = config;

26link instance.notifier = new Notifier(instance.config);

27link instance.initialized = true;

28link return instance;

29link }

30link

31link public static HttpResponse log(String level, String message) {

32link Rollbar instance = initializedInstance();

33link return instance.notifier.log(level, message);

34link }

35link

36link public static HttpResponse log(Exception exc) {

37link Rollbar instance = initializedInstance();

38link return instance.notifier.log(exc);

39link }

40link

41link public static HttpResponse log(Exception exc, Map<String, Object> custom) {

42link Rollbar instance = initializedInstance();

43link return instance.notifier.log(exc, custom);

44link }

45link

46link public static HttpResponse log(ExceptionData exData) {

47link Rollbar instance = initializedInstance();

48link return instance.notifier.log(exData);

49link }

50link

51link private static Rollbar initializedInstance()

52link {

53link Rollbar instance = Rollbar.instance();

54link if (!instance.initialized) {

55link Rollbar.init();

56link }

57link

58link return instance;

59link }

60link

61link private Rollbar() {

62link }

63link

64link private static Rollbar instance = null;

65link private Config config = null;

66link private Notifier notifier = null;

67link private Boolean initialized = false;

68link}

linkRefactoring Into Idiomatic Apex

Let's just leave aside all those public init methods and hone in here ...

So we've got ~ 70 lines of code mostly dealing with the problem of initializing a singleton instance / logging with strings that are actually constants.

First let's review the idomatic way to initialize Singletons in Apex (if you're interested in more information on the singleton shown below, please read Building A Better Singleton to learn more!):

classes/SingletonExample.cls
1linkpublic class MyClass {

2link private MyClass() {

3link //prevent public initialization

4link //you can still use dependency injection within your constructor though

5link }

6link

7link //Singleton method

8link private static final MyClass Instance = new MyClass();

9link

10link //expose public static methods for using MyClass

11link public static void sayHi() {

12link Instance.say('hi');

13link }

14link

15link private void say(String sayString) {

16link System.debug(sayString);

17link }

18link}

With that in mind, let's streamline this implementation:

classes/Rollbar.cls
1linkprivate final Http http;

2linkprivate final RollbarDataBuilder dataBuilder;

3link

4linkprivate static final String API_URI = 'https://api.rollbar.com/api/1/item/';

5link

6linkpublic enum Level { Critical, Debug, Error, Info, Warning }

7link

8linkprivate Rollbar() {

9link this.dataBuilder = new RollbarDataBuilder();

10link this.http = new Http();

11link}

12link

13linkpublic static final Rollbar Instance = new Rollbar();

14link

15linkpublic static HttpResponse log(Level level, String message) {

16link Message payload = Instance.dataBuilder.build(level, message);

17link return send(payload);

18link}

19link

20linkpublic static HttpResponse log(Exception ex) {

21link Message payload = Instance.dataBuilder.build(ex);

22link return send(payload);

23link}

24link

25linkprivate static HttpResponse send(Message payload) {

26link HttpRequest request = new HttpRequest();

27link request.setEndpoint(API_URI);

28link request.setMethod(RestMethod.POST.name());

29link //Rollbar only wants non-null properties sent over

30link //The second argument suppresses null values

31link request.setBody(Json.serialize(payload, true));

32link

33link HttpResponse res = Instance.http.send(request);

34link if(res.getStatusCode() != 200) {

35link throw new CalloutException(

36link 'Rollbar callout failed with response: ' + res.getBody()

37link );

38link }

39link return res;

40link}

So what we have gained? We killed off the Notifier class (which wasn't shown, but also was just wrapping calls to the DataBuilder) in favor of the Rollbar object encapsulating the full callout. We got rid of a bunch of boilerplate related to initialization. We traded string constants for a Rollbar.Level enum which can easily be understood as the severity of the log being sent.

The astute reader might note that I'm not following the pattern I prescribed in Future Methods, Callouts & Callbacks. You'd be exactly right in saying that, but I was recently reminded of this age-old adage while reading something that came up on the React subreddit:

Duplication is far cheaper than the wrong abstraction

The Callout & Callback pattern that I documented previously is great when you have many consumers whose post-callout behavior is tightly coupled to interactions with the database or further processing is necessary. In this case, however, it would be a mistake to try to shoehorn this specific logging implementation into an abstraction meant for post-processing ... especially because having the Http object as a member of the Rollbar class will prove helpful for fully testing everything out.

But I'm getting ahead of myself. What does this DataBuilder object look like?

classes/DataBuilder.cls
1linkpublic with sharing class DataBuilder {

2link public DataBuilder(Config config) {

3link this.config = config;

4link }

5link

6link public Map<String, Object> buildPayload(String level, String message)

7link {

8link return buildPayloadStructure(level, buildMessageBody(message), null);

9link }

10link

11link public Map<String, Object> buildPayload(Exception exc)

12link {

13link return buildPayloadStructure('error', buildExceptionBody(exc), null);

14link }

15link

16link public Map<String, Object> buildPayload(Exception exc, Map<String, Object> custom)

17link {

18link return buildPayloadStructure('error', buildExceptionBody(exc), custom);

19link }

20link

21link public Map<String, Object> buildPayload(ExceptionData exData)

22link {

23link Map<String, Object> custom = new Map<String, Object>();

24link custom.put('context', exData.context());

25link

26link return buildPayloadStructure('error', buildTraceBody(exData), custom);

27link }

28link

29link private Map<String, Object> buildExceptionBody(Exception exc)

30link {

31link if (exc.getCause() == null) {

32link return buildTraceBody(exc);

33link } else {

34link return buildTraceChainBody(exc);

35link }

36link }

37link

38link private Map<String, Object> buildTraceChainBody(Exception exc)

39link {

40link Map<String, Object> outterExTrace = (Map<String, Object>)

41link this.buildTraceBody(exc).get('trace');

42link Map<String, Object> innerExTrace = (Map<String, Object>)

43link this.buildTraceBody(exc.getCause()).get('trace');

44link

45link List<Map<String, Object>> traceChainList = new List<Map<String, Object>>();

46link traceChainList.add(outterExTrace);

47link traceChainList.add(innerExTrace);

48link

49link

50link Map<String, Object> body = new Map<String, Object>();

51link body.put('trace_chain', traceChainList);

52link

53link return body;

54link }

55link

56link private Map<String, Object> buildPayloadStructure(

57link String level,

58link Map<String, Object> body,

59link Map<String, Object> custom

60link ) {

61link Map<String, Object> data = this.buildDataStructure(

62link level,

63link this.config.environment(),

64link body,

65link custom

66link );

67link

68link Map<String, Object> structure = new Map<String, Object>();

69link structure.put('access_token', this.config.accessToken());

70link structure.put('data', data);

71link return structure;

72link }

73link

74link private Map<String, Object> buildDataStructure(

75link String level,

76link String environment,

77link Map<String, Object> body,

78link Map<String, Object> custom

79link ) {

80link

81link Map<String, Object> notifierMap = new Map<String, Object>();

82link notifierMap.put('name', Notifier.NAME);

83link notifierMap.put('version', Notifier.VERSION);

84link

85link Map<String, Object> structure = new Map<String, Object>();

86link structure.put('notifier', notifierMap);

87link structure.put('level', level);

88link structure.put('environment', environment);

89link structure.put('framework', 'apex');

90link structure.put('body', body);

91link structure.put('custom', custom);

92link

93link return structure;

94link }

95link

96link private Map<String, Object> buildMessageBody(String message)

97link {

98link Map<String, Object> messageMap = new Map<String, Object>();

99link messageMap.put('body', message);

100link

101link Map<String, Object> body = new Map<String, Object>();

102link body.put('message', messageMap);

103link

104link return body;

105link }

106link

107link private Map<String, Object> buildTraceBody(ExceptionData exData)

108link {

109link List<Map<String, Object>> framesList = new List<Map<String, Object>>();

110link

111link Map<String, Object> frameMap = new Map<String, Object>();

112link frameMap.put('filename', exData.fileName());

113link frameMap.put('class_name', exData.className());

114link frameMap.put('method', exData.fileName());

115link frameMap.put('lineno', exData.line());

116link frameMap.put('colno', exData.column());

117link

118link framesList.add(frameMap);

119link

120link Map<String, Object> excMap = new Map<String, Object>();

121link excMap.put('class', exData.className());

122link excMap.put('message', exData.message());

123link

124link return buildTraceStructure(excMap, framesList);

125link }

126link

127link private Map<String, Object> buildTraceBody(Exception exc)

128link {

129link List<Map<String, Object>> framesList = new List<Map<String, Object>>();

130link

131link String[] frames = exc.getStackTraceString().split('\n');

132link for (String frameStr : frames) {

133link if (frameStr == '()') {

134link continue;

135link } else if (frameStr.toLowerCase() == 'caused by') {

136link break;

137link }

138link

139link Map<String, Object> frameMap = new Map<String, Object>();

140link frameMap.put('filename', frameStr);

141link

142link String className = frameStr.split(':')[0];

143link String methodName = '';

144link if (className != 'AnonymousBlock') {

145link className = className.split('\\.')[1];

146link methodName = frameStr.split(':')[0].split('\\.')[2];

147link }

148link

149link frameMap.put('class_name', className);

150link frameMap.put('method', methodName);

151link

152link Pattern linePattern = Pattern.compile('line (\\d+)');

153link Matcher lineMatcher = linePattern.matcher(frameStr);

154link lineMatcher.find();

155link frameMap.put('lineno', Integer.valueOf(lineMatcher.group(1)));

156link

157link Pattern colPattern = Pattern.compile('column (\\d+)');

158link Matcher colMatcher = colPattern.matcher(frameStr);

159link colMatcher.find();

160link frameMap.put('colno', Integer.valueOf(colMatcher.group(1)));

161link

162link framesList.add(frameMap);

163link }

164link

165link Map<String, Object> excMap = new Map<String, Object>();

166link excMap.put('class', exc.getTypeName());

167link excMap.put('message', exc.getMessage());

168link

169link return buildTraceStructure(excMap, framesList);

170link }

171link

172link private Map<String, Object> buildTraceStructure(

173link Map<String, Object> exceptionMap,

174link List<Map<String, Object>> framesList

175link ) {

176link Map<String, Object> body = new Map<String, Object>();

177link

178link Map<String, Object> traceMap = new Map<String, Object>();

179link

180link traceMap.put('exception', exceptionMap);

181link traceMap.put('frames', framesList);

182link

183link body.put('trace', traceMap);

184link

185link return body;

186link }

187link

188link private Config config;

189link}

Eep. Hopefully you just scrolled through that at a rapid staccato. We're using a strongly-typed language — let's try to take advantage of that to cut down on some of the string maps here. We can also make use of the same Rollbar.Level enum introduced above, making it easier to differentiate between method arguments. Unfortunately, not everything will be able to be strongly typed — if you'll notice, the JSON object that's being built here makes use of two properties, exception and class, both of which are reserved keywords in Apex.

Again, since the JSON object that's being built is singularly constrained to use with Rollbar, I'm less concerned with the tight coupling between this DataBuilder object and the Rollbar class, and more concerned with how I'd like to refer to things within a "Rollbar namespace" — in other words, let's put those strongly typed classes into the Rollbar class, and rename the "DataBuilder" so that it's the "RollbarDataBuilder". That's idomatic Apex — typings to help us and our tests verify that things are being shaped correctly, with descriptive names to assist:

classes/Rollbar.cls
1linkpublic class Message {

2link public String access_token { get; set; }

3link public Data data { get; set; }

4link}

5link

6linkpublic class Data {

7link public Data() {

8link notifier = new Notifier();

9link }

10link public Notifier notifier { get; set; }

11link public String level { get; set; }

12link public String environment { get; set; }

13link public String framework { get { set; }

14link public MessageBody body { get; set; }

15link}

16link

17linkpublic class MessageBody {

18link //trace can't be strongly typed because

19link //it has a property named "exception"

20link //which is a reserved word in Apex

21link //the "exception" property also has "class" and "message"

22link //strings, and "class" is another reserved word

23link public Map<String, Object> trace { get; set; }

24link public List<Map<String, Object>> trace_chain { get; set; }

25link public InnerMessage message { get; set; }

26link}

27link

28linkpublic class InnerMessage {

29link public String body { get; set; }

30link}

31link

32linkpublic class Notifier {

33link public String name { get; set;}

34link public String version { get; set; }

35link}

36link

37linkpublic class ExceptionFrame {

38link public String filename { get; set; }

39link public String class_name { get; set; }

40link public String method { get; set; }

41link public Integer lineno { get; set; }

42link public Integer colno { get; set; }

43link}

So we get a bunch of POJOs, which thanks to IDE intellisense are going to prove enormously helpful in making clear the shape of the log objects being constructed.

And the much-reduced RollbarDataBuilder:

classes/RollbarDataBuilder.cls
1linkpublic class RollbarDataBuilder {

2link public static final String FRAMEWORK = 'apex';

3link public static final String NAME = 'rollbar-sf-apex';

4link public static final String VERSION = '1.0.0';

5link

6link public Rollbar.Message build(Rollbar.Level level, String message) {

7link return buildPayloadStructure(level, buildMessageBody(message));

8link }

9link

10link public Rollbar.Message build(Exception ex) {

11link return buildPayloadStructure(Rollbar.Level.Error, buildExceptionBody(ex));

12link }

13link

14link private Rollbar.Message buildPayloadStructure(

15link Rollbar.Level level,

16link Rollbar.MessageBody body) {

17link Rollbar.Message message = new Rollbar.Message();

18link //wherever you store secrets, be it a custom setting, object, or metadata

19link message.access_token = someValue;

20link message.data = this.buildDataStructure(level, body);

21link return message;

22link }

23link

24link private Rollbar.Data buildDataStructure(Rollbar.Level level,

25link Rollbar.MessageBody body) {

26link Rollbar.Data data = new Rollbar.Data();

27link data.body = body;

28link //I didn't like using the Org name, preferring the granularity of the Org URL.

29link // You could realistically put any kind of domain-recognizable identifier here.

30link data.environment = Url.getSalesforceBaseUrl().toExternalForm();

31link data.framework = FRAMEWORK;

32link data.level = level.name().toLowerCase();

33link data.notifier.name = NAME;

34link data.notifier.version = VERSION;

35link return data;

36link }

37link

38link private Rollbar.MessageBody buildMessageBody(String messageBody) {

39link Rollbar.InnerMessage innerMessage = new Rollbar.InnerMessage();

40link innerMessage.body = messageBody;

41link

42link Rollbar.MessageBody body = new Rollbar.MessageBody();

43link body.message = innerMessage;

44link return body;

45link }

46link

47link private Rollbar.MessageBody buildExceptionBody(Exception ex) {

48link if (ex.getCause() == null) {

49link return buildTraceMessage(ex);

50link } else {

51link return buildTraceChainBody(ex);

52link }

53link }

54link

55link private Rollbar.MessageBody buildTraceMessage(Exception ex) {

56link //note that while the typings have changed in this method

57link //the underlying logic I left alone.

58link List<Rollbar.ExceptionFrame> framesList = new List<Rollbar.ExceptionFrame>();

59link

60link String[] frames = ex.getStackTraceString().split('\n');

61link for (String frameStr : frames) {

62link if (frameStr == '()') {

63link continue;

64link } else if (frameStr.toLowerCase() == 'caused by') {

65link break;

66link }

67link

68link Rollbar.ExceptionFrame frame = new Rollbar.ExceptionFrame();

69link frame.filename = frameStr;

70link

71link String className = frameStr.split(':')[0];

72link String methodName = '';

73link if (className != 'AnonymousBlock') {

74link className = className.split('\\.')[1];

75link methodName = frameStr.split(':')[0].split('\\.')[2];

76link }

77link

78link frame.class_name = className;

79link frame.method = methodName;

80link

81link Pattern linePattern = Pattern.compile('line (\\d+)');

82link Matcher lineMatcher = linePattern.matcher(frameStr);

83link lineMatcher.find();

84link frame.lineno = Integer.valueOf(lineMatcher.group(1));

85link

86link Pattern colPattern = Pattern.compile('column (\\d+)');

87link Matcher colMatcher = colPattern.matcher(frameStr);

88link colMatcher.find();

89link frame.colno = Integer.valueOf(colMatcher.group(1));

90link

91link framesList.add(frame);

92link }

93link

94link Map<String, Object> excMap = new Map<String, Object>();

95link excMap.put('class', exc.getTypeName());

96link excMap.put('message', exc.getMessage());

97link

98link return buildTraceBody(excMap, framesList);

99link }

100link

101link private Rollbar.MessageBody buildTraceBody(Map<String, Object> exceptionMap,

102link List<Rollbar.ExceptionFrame> framesList) {

103link Rollbar.MessageBody body = new Rollbar.MessageBody();

104link body.trace = new Map<String, Object>();

105link body.trace.put('exception', exceptionMap);

106link body.trace.put('frames', framesList);

107link return body;

108link }

109link

110link private Rollbar.MessageBody buildTraceChainBody(Exception ex) {

111link Map<String, Object> outterExTrace = this.buildTraceMessage(ex).trace;

112link Map<String, Object> innerExTrace = this.buildTraceMessage(ex.getCause()).trace;

113link

114link List<Map<String, Object>> traceChainList = new List<Map<String, Object>>();

115link traceChainList.add(outterExTrace);

116link traceChainList.add(innerExTrace);

117link

118link Rollbar.MessageBody body = new Rollbar.MessageBody();

119link body.trace_chain = traceChainList;

120link return body;

121link }

122link}

Because the DataBuilder class came with tests, it's easy to verify that this refactor, which cut the lines of code in half, is still doing exactly what we'd like it to. The tests themselves cleaned up nicely, because there was a heck of a lot less casting to Map<String, Object> happening. I also changed some method names; particularly for public methods, it's often a waste of characters to describe the return type in the name of the method — intellisense will tell you the return type!

Lastly, I'll just touch on what I was talking about before — using the Http member object on the Rollbar class to confirm end-to-end testing. Yes, we use Test.setMock(System.HttpCalloutMock) to properly mock API responses in tests, but in order to verify that our data is built correctly prior to sending out, having access to the body of the HttpRequest is paramount. Again, I typically don't like to utilize this kind of pattern, but because logging as an activity is something that's going to happen many layers deep into your application, sometimes it's necessary (as opposed to passing an object back up the entire stack):

classes/Rollbar.cls
1linkprivate final HttpWrapper http;

2linkprivate final RollbarDataBuilder dataBuilder;

3link

4linkprivate Rollbar() {

5link this.dataBuilder = new RollbarDataBuilder();

6link this.http = new HttpWrapper();

7link}

8link

9link//...

10link

11link@testVisible private static HttpRequest testReq;

12linkprivate class HttpWrapper {

13link private final Http http;

14link public HttpWrapper() {

15link this.http = new Http();

16link }

17link

18link public HttpResponse send(HttpRequest req) {

19link testReq = req;

20link return this.http.send(req);

21link }

22link}

This exhibits two other important idiomatic Apex patterns:


linkIdiomatic Apex Wrap-Up

In the end, it's a shame that some of the JSON object being sent to Rollbar made use of reserved words in Apex. This isn't something that they, as a vendor, could have anticipated; they have many SDKS across a variety of commonly used languages, and planning out their object structure with deference to one specific language is a little much to ask of anybody. Still, it made the object structure less clean than I would have liked. That the real world demands us to do things we'd prefer to avoid doing is another important lesson.

I realize I'm just brushing the surface of an extremely broad topic — "Idiomatic Apex" would be an enormous book, if printed, and Singleton initialization and strong typing is barely a start. Have some thoughts on other excellent idiomatic Apex examples? Feel free to reach out — perhaps your suggestions will inform more posts on this subject!

The original version of Idiomatic Salesforce Apex can be read on my blog.

The ProblemRefactoring Into Idiomatic ApexIdiomatic Apex Wrap-Up

Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests



Read more tech articles