Our values make up who we are. In the Groundswell development team, we hold ourselves accountable to these values in every project, which inspired the creation of our company-wide coding guidelines. We have decided to share some of them with you in two parts to help anyone passionate about their code.
Pure Functions
Pure Functions as a concept originated from Functional Programming, focusing on two key aspects:
- Its return value is the same for the same arguments
- Its evaluation does not modify the application
When building Aura and Lightning Web Components (LWC), we learned from the principles described by Pure Functions to achieve Reliability and Extensibility across all our products from anywhere in the platform.
// controller.js
centralController: function
(numberOne, numberTwo)
let result =
helper.pureAddHeIper(
component. get('v.numberOne'),
component. get('v.numberTwo')
)
component. set('v. result', result);
}
//helper.js
pureAddHelper: function
(numberOne, numberTwo) {
return numberOne + numberTwo;
}
//helper.js
unpureAddHelper: function
(component, event, helper) {
//reading from external
let a = component.get('v.numberOne');
//reading from external
let b = component.get('v.numberTwo');
let result = a + b;
//modifying state!
component. set('v.result', result);
}
In the above code, we see that our pure function pureAddHelper() does not rely on the program state. Its evaluation for the same parameters will always return the same values, and will not alter the application in any way. In contrast, the unpureAddHelper() method relies on reading values from the application state and directly alters the application values during its evaluation. The unpure method will be difficult to re-use for a later component, as we can never be sure of its output, and how it may change our application. The pure method is more friendly for re-use, as we are in direct control of the results of its evaluation, and we can be sure its functionality is independent of other components.
In the case of LWC, since it follows the Web Component standards, we can fully utilize the capabilities offered by JS and ES6 via method exports. This way, we create a strict separation of concern that allows us to abstract core logic away from simple rendering components, so that we can re-use the same calculations and data processing across the entire platform.
In the case of Javascript unit testing with frameworks like Jest, Pure Functions isolate and decouple all of our business-critical logic, allowing for more meaningful and extensive unit testing on the logic that matters. At Groundswell, we encourage the implementation of all core business logic as standalone pure functions. In doing so, not only do we achieve clearly defined state management through immutability, but we can also speed up our products through various browser level optimization. Memoization, Just-In-Time compilation, and Loop optimizations are available to Pure Functions and can drastically speed up page rendering, as well as logic execution times.
@api result;
centralController() {
//... some work
// controlled change of state
// from a centralized controller
this.result = pureAdd(
this.number-One,
this.numberTwo);
//... some work
}
pureAdd(numberOne, numberTw0) {
return numberOne + numberTwo;
}
@api result;
unpureAdd( ) {
this.result =
this.numberOne + this.numberTwo;
}
Separation Of Concern Patterns
With Teamwork being one of our core values, it’s no surprise that we put heavy emphasis and guidelines around team Cooperation and execution. One practice that drives this value is the use of Separation of Concern Patterns.
Many developers appreciate the benefits of Salesforce development but are also aware of the overhead that comes from it. Before the release of SFDX and scratch Orgs, it was common for developers to work in a single sandbox. While excellent for synchronizing changes in real-time, it was also prone to code overwrite and loss of work. To achieve maximum productivity across the development team, we must establish a development methodology that separates each other’s concerns.
To create a clear split between each member’s work, we created rules and guidelines to detail each member’s responsibility and defined their vertical in a project.
This separation breaks down into three distinct levels:
1. Domain—Encapsulates behaviour that affects or interacts with data such as applying default values and validations. This level includes inserting, updating, and deleting operations. Irrespective of the source (i.e. Updates via API or via UI) and with respect at an application level, all validations, calculations & default values associated with data should always reside in the domain layer.
class UserDomain {
//domain
public static List<User> updateUsers(List<User> toUpdate) {
//enforce update rules/business rules
for (User usr : toUpdate) {
if (!usr.isVerified_c) {
throw new UserDomainException('Non verified Users cannot be updated') ;
}
}
update toUpdate;
return toUpdate;
}
}
2. Service—Core business logic that consumes and processes data. Establishes a set of available operations for any client to use and coordinates the application’s response in each operation. Any logic associated with data should reside in the service layer and require no respect at an application level.
class UserService {
//service
public static List<User> handleUserEmail(Map<Id, String> idToEmailMap) {
List<User> selectedUsers = UserSelector.selectUsersById(idToEmailMap.keySet());
for (User usr : selectedUsers) {
usr. Email = idToEmailMap.get(usr.Id);
}
try {
return UserDomain.updateUsers(selectedUsers);
} catch (Exception excptn) {
//handle exceptions
}
}
}
3. Selector—Establishes a set of available operations to be used by any client and coordinates the application’s response in each operation. This layer should also enforce read access checks. Here we recommend the use of SObjectSelectors from fflib-apex-common to achieve a streamlined and predictable selection pattern.
class UserSelector {
// selector
public static List<User> selectUsersById(Set<Id> ids) {
return [SELECT Id, Name, Email FROM User WHERE Id = :ids];
}
}
With this paradigm, we have defined the boundaries of responsibility between each member of the development team. As a by-product, we have also created libraries of reusable code across the organization.
The next time a developer needs to select a specific group of records, duplicate/extend existing business logic, or even write to the database, they can instead leverage and extend existing logic. When changes get made, it minimizes the impacts and regressions on other areas, and a healthier and more adaptable program evolves.
Our guidelines are extensive because we are passionate about producing quality work; that’s why we are publishing them in two parts. We hope these can influence your work for the better as much as it has ours.