TL;DR
Explore the benefits of the composite pattern in Salesforce record filtering. This technique offers a novel way to manage complex conditions more efficiently, transforming traditional filtering approaches into a more structured and flexible system. It’s a strategic enhancement for Salesforce development, aimed at improving both functionality and maintainability in various scenarios.
What’s the composite pattern?
If you are familiar with software patterns, feel free to jump ahead. For those of us who might need a refresher, the composite pattern is one of the core fundamental approaches to structuring software to solve common architectural problems in a reliable and repeatable way. If you have enough programming experience, either by yourself or as part of a team, you may find that you have already applied *some * patterns without even realizing it. So, it might be worth your time to give it a deep dive.
But please keep in mind: these patterns are not dogmatic. Don’t try to force your organization into a pattern just for the sake of it. They should be employed only when they truly serve a need and provide a solution to a given problem. They are not the end-all-be-all approach to software development - at least not in my opinion.
So what is the composite pattern? A quick google search comes up with the following definition
In software engineering, the composite pattern is a partitioning design pattern. The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object. The intent of a composite is to "compose" objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly.
That’s quite a lot to digest, I understand. The key point for us to take away is the treat individual objects and compositions uniformly.
Conventional Filtering
Let’s now understand how we would conventionally achieve the task of filtering. I am a very big proponent of showcasing code so let’s play around together.
1
List<Account> someRecords = [SELECT Id, Name, ... FROM Account WHERE Name = 'Ymir'];
As we see, the first instance of filtering data already happens in a query. So naturally, we would only want to filter when there is certain business logic that should apply to a subset of the already queried records, and we want to avoid further queries. Also, consider the context of an Apex trigger - in that case, we don’t even do the query!
So, after we initialize those records, we would typically see something like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<Account> someRecords = ...
List<Account> filteredRecords = new List<Account>();
// can be rewritten in various forms but boils down to something like this
for(Account acc : someRecords) {
if(acc.Name == 'Eren') {
filteredRecords.add(acc);
}
if(more conditions) {
filteredRecords.add(acc);
}
// or using a helper method
if(shouldFilter(acc)) {
filteredRecords.add(acc);
}
}
This is a perfectly valid approach as long as your conditional logic is simple. But just let me give you a perspective on what might be possible issues here:
- When the business logic grows, this might lead to maintenance of deeply buried conditions.
- What if we want to filter into multiple sublists? Now I would need to duplicate those conditions in another loop or introduce some other form of logic.
That’s where the composite pattern comes to the rescue! What if we could compose the conditions however we see fit? Let’s start by creating some Apex that models the criteria.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// this defines how we compare values - feel free to expand
public enum FilterMode { EQUAL, UNEQUAL, GREATER, SMALLER }
private class FilterCriteria {
public Object value;
public FilterMode mode;
public String field;
public FilterCriteria(String field, FilterMode mode, Object obj) {
this.value = obj;
this.mode = mode;
this.field = field;
}
public Boolean applyCriteria(SObject record) {
switch on mode {
when EQUAL {
Boolean res = record.get(field)?.equals(value);
return res != null ? res : false;
}
when else {
return false;
}
}
}
}
As evident by the code snippet, the new class is mainly used to hold the relevant conditional values until it’s time to evaluate them properly. By itself, an instance of this object does not do anything - it is simply a condition package.
So, how could we potentially use those already? Using them would change our initial code into something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Account> someRecords = ...
List<Account> filteredRecords = new List<Account>();
List<FilterCriteria> criterias = new List<FilterCriteria>();
criteria.add(new FilterCriteria(Account.Name, FilterMode.EQUAL, 'Eren'));
criteria.add(...);
// etc.
for(Account acc : someRecords) {
for(FilterCriteria criteria : criterias) {
if(criteria.appyCriteria(acc)) {
filteredRecords.add(acc);
}
}
}
We now see that we got rid of those nasty if statements, but at what price? We simply moved them into a convoluted list-building exercise at the beginning of our code segment. Although for the moment hidden reasons, an improvement in the right context, we can still do better! That’s where we bring in some structure to support in handling those criteria objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public without sharing class RecordFilter {
private List<SObject> records;
private List<FilterCriteria> criterias;
public RecordFilter(List<SObject> records) {
this.records = records;
this.criterias = new List<FilterCriteria>();
}
public RecordFilter addFilter(FilterCriteria criteria) {
this.records.add(criteria);
// returning this instance will come in very handy later
return this;
}
public List<SObject> reduce() {
List<SObject> filtered = new List<SObject>();
for(SObject record : records) {
Boolean isValid = true;
for(FilterCriteria criteria : criterias) {
isValid &= filter.applyCriteria(record);
if(!isValid) {
break;
}
}
if(isValid) {
filtered.add(record);
}
}
return filtered;
}
}
As we see, the RecordFilter class is just as simple! It is just a form of collection that is able to traverse through its criteria data in a meaningful way! A concept “stolen” from functional programming. Now let’s see how we can change up our initial code!
1
2
3
4
5
List<Account> someRecords = ...
List<Account> filteredRecords = (List<Account>) new RecordFilter(someRecords)
.add(new FilterCriteria(Account.Name, 'Eren'));
.add(...)
.reduce();
And all of a sudden, it looks way less intimidating! Remember we returned this in the methods? That allows us to chain those method calls without assigning them necessarily to some variable or constant, improving the readability greatly.
There is just one improvement left that we can undertake! Consider our “Name” criteria. This is probably a very common occurrence. Actually, our FilterCriteria class already has very basic comparison capabilities - thus, we could hide it from the end-user altogether. This can be done by introducing the following method into our RecordFilter class.
1
2
3
4
public RecordFilter addFilter(Schema.SObjectField field, Object value) {
this.criterias.add(new FilterCriteria(field.getDescribe().getName(), FilterMode.EQUAL, value));
return this;
}
This allows us to build equality criteria in a very simple and straightforward way. Thus, resulting in our original code becoming:
1
2
3
4
5
List<Account> someRecords = ...
List<Account> filteredRecords = (List<Account>) new RecordFilter(someRecords)
.add(Account.Name, 'Eren');
.add(...)
.reduce();
And that’s it. We are now able to have very simple, concise, and reusable filtering logic on our records. But one big question remains!
Why should you care?
Good question! Introducing a logic like this achieves a couple of things. For the time being, the most relevant is that we made conditions into a handle thing. Those conditional statements inside our if-block became objects. Which is actually amazing! Now we can serialize, store, send, read, and do anything your imagination might dream up with them. This perspective will serve as a foundation for further abstractions to make our code more dynamic and reusable. Imagine being able to configure your org when certain pieces of code run, instead of needing to go through the hoops of touching your code base for any little change your PO’s might want? Sounds good? Then keep on reading and discover what we have in store for you!
Join Our Newsletter
Don't miss out on our latest Salesforce Dev News!