Beyond If Statements: Ways to avoid IFs – Polish Dreamin’ 24
Introduction
Polish Dreamin' 2024!
Code is a representation of business requirements. Business requirements vary for each company, but most of them have an IF-THEN structure. It's quite common to see IF statements in our code. However, an increasing number of IFs can make our code hard to read and understand.
In the following post, I will cover different ways to mitigate IFs while keeping the original logic.
Let's start!
Cyclomatic Complexity
Cyclomatic Complexity was developed by Thomas J. McCabe, Sr. in 1976 and is "a quantitative measure of the number of linearly independent paths through a program's source code." [2].
For a long time, Cyclomatic Complexity was the standard for measuring the complexity of a method's control flow. Its role was simply to identify the number of forks in a code snippet (if
, switch
, etc.).
Cyclomatic Complexity shows us the number of decisions (IFs) a given block of code needs to make, meaning that we know how difficult the code will be to test.
More IFs means more test cases to cover.
Example
Cyclomatic Complexity tells us how difficult code will be to test.
Below you will find a method that requires at least 2 test cases to cover the IF-ELSE statement.
function fun(a) {
let x;
if (a) { // +1 (if)
x = 1;
} else { // +1 (else)
x = 2;
}
return x;
} // cyclomatic complexity = 2
Issue
Complex code without forks can have a Cyclomatic Complexity equal to 0, even if it’s extremely difficult to read and understand.
On the other hand, simple methods like myFun
, which are easy to understand, have Cyclomatic Complexity equal to 4.
function myFun(param) {
let response;
switch (param) {
case 'A':
response = 1;
break;
case 'B':
response = 2;
break;
case 'C':
response = 3;
break;
default:
response = 0;
}
return response;
} // cyclomatic complexity = 4
While Cyclomatic Complexity is a good indicator of testing difficulty, it doesn't help us understand how difficult the code is for programmers. This is why a new measure called Cognitive Complexity was introduced.
Cognitive Complexity
Cognitive Complexity is a term derived from psychology and describes how individuals perceive and make sense of the world around them.
In software development Cognitive Complexity is a measure of how difficult a piece of code (function, class) is to intuitively understand.
Less Cognitive Complexity more Readability.
How does Cognitive Complexity increase?
Code is more complex for each "break in the linear flow of the code" and when "flow breaking structures are nested".
Breaks in flow
What can break code flow?
- loops (
for
;while
;do while
) - conditionals (
if
; ternary operators;switch
) - recursion
- try-catch
- jump-to labels: (
continue
;break
) - complex sequences of logical operators (e.g.
a || b && c
)
function myFun(a, b, c) {
try {
for (let i = 0; i < 10; i++) {
if (a === i && (b || c)) {
break;
}
}
} catch (err) {
console.error(err)
}
}
Nesting
The more deeply-nested your code gets, the harder it can be to understand.
What counts as nesting:
- conditionals (
if
; ternary operators;switch
) - loops (
for
;while
;do while
) - try-catch blocks
function nestedExample(a, b, c) {
if (a) { // +1
if (b) { // +1
if (c) { // +1
// ...
}
}
}
}
function myFun(param) {
let response;
if (param === 'A') { // +1
response = 1;
} else if (param === 'B') { // +1
response = 2;
} else if (param === 'C') { // +1
response = 3;
} else { // + 1
response = 0;
}
return response;
} // cognitive complexity = 4
Why is Cognitive Complexity dangerous?
- Difficulty in collaboration - Cognitevely complex code can be challenging for team members to collaborate on, leading to communication gaps, misunderstandings, and potentially conflicting implementations.
- Higher risk of technical debt - Complex codebases accumulate technical debt rapidly, making it increasingly difficult and costly to maintain, refactor, or enhance the system over time.
- Maintainability issue - Complex code often becomes a maintenance nightmare, requiring significant time and effort to understand, debug, and modify.
- Limited Extensibility - Code with high cognitive complexity, particularly filled with numerous conditional statements, becomes rigid and difficult to extend or modify without introducing errors.
- Complex code decreases productivity - Developers spend more time deciphering intricate logic rather than efficiently implementing new features or fixing existing issues. This inefficiency reduces overall productivity.
Cognitive vs Cyclomatic Complexity?
- Cyclomatic Complexity is a measure of how difficult a piece of code is to test.
- Cognitive Complexity is a measure of how difficult a piece of code is to understand.
function sum(max) {
let total = 0;
for (let i = 0 ; i <= max ; i++) {
for (let j = 0 ; j <= max ; j++) {
total += i + j;
}
}
return total;
}
// cyclomatic complexity = 3
// cognitive complexity = 2
function play(number) {
switch (number) {
case 1:
console.log('Numer 1 selected');
break;
case 2:
console.log('Numer 2 selected');
break;
case 3:
console.log('Numer 3 selected');
break;
default:
console.log('Number not found!');
}
// cyclomatic complexity = 3
// cognitive complexity = 1
Both metrics are complementary, and it's essential to consider them together for a comprehensive understanding of code quality. While Cyclomatic Complexity primarily focuses on the structural complexity and testing effort, Cognitive Complexity delves deeper into how humans perceive and comprehend code, offering insights into readability, maintainability, and collaboration aspects.
How to reduce Code Complexity?
Reducing cognitive complexity is crucial for maintaining a healthy and sustainable codebase. Here are some strategies to achieve this:
Linear Code
Linear code is your friend. Linear code is easy to follow.
Code is a sequence of instructions listed one after another. Simple, linear code should be easy to follow. However, more IF statements and more loops make code more difficult to understand which leads to increased cognitive complexity. Master programmers consider systems to be stories to be told rather than programs to be written.
❌
function calculateBonus(yearsOfExperience, performanceRating) {
let bonus = 0;
if (yearsOfExperience >= 5) {
if (performanceRating >= 8) {
bonus = 10000;
} else {
if (performanceRating >= 6) {
bonus = 7500;
} else {
if (performanceRating >= 4) {
bonus = 6000;
} else {
bonus = 5000;
}
}
}
} else {
if (yearsOfExperience >= 3) {
if (performanceRating >= 8) {
bonus = 8000;
} else {
if (performanceRating >= 6) {
bonus = 5500;
} else {
if (performanceRating >= 4) {
bonus = 4500;
} else {
bonus = 3500;
}
}
}
} else {
if (performanceRating >= 8) {
bonus = 6000;
} else {
if (performanceRating >= 6) {
bonus = 4000;
} else {
if (performanceRating >= 4) {
bonus = 3000;
} else {
bonus = 2000;
}
}
}
}
}
return bonus;
}
Shorthand
K.I.S.S - Keep It Simple, Stupid! The code should be as simple as possible.
How we can make our code simpler?
Use Code directly
Use code directly - that rule applies especially to Boolean values. There is no need to create complex structures. Always review your approach and try to make it simpler. Check the examples below.
❌
let isServicePageVisible = false;
if (currentUser.hasServicePageAccess) {
isServicePageVisible = true;
} else {
isServicePageVisible = false;
}
✅
let isServicePageVisible = currentUser.hasServicePageAccess;
❌
const isPartner = this.userType === 'Partner' ? true : false;
✅
const isPartner = this.userType === 'Partner';
Remember!
- Do not use IF-ELSE to set Boolean value - just move IF's condition to variable assignment as we did in the examples above.
- Do not use the Ternary (
? :
) operator to return Boolean. More details in the post: Do not use the Ternary operator to return Boolean.
Safe Navigation Operator (?.)
Lightning Web Components
The optional chaining (?.) operator accesses an object's property or calls a function. If the object accessed or function called using this operator is
undefined
ornull
, the expression short circuits and evaluates toundefined
instead of throwing an error. [4]
❌
let userLanguage;
if (user.language != null) {
userLanguage = user.language;
}
✅
let userLanguage = user?.language;
Apex
Use the safe navigation operator (?.) to replace explicit, sequential checks for null references. This operator short-circuits expressions that attempt to operate on a null value and returns null instead of throwing a NullPointerException. [5]
❌
String profileUrl = null;
if (user.getProfileUrl() != null) {
profileUrl = user.getProfileUrl().toExternalForm();
}
✅
String profileUrl = user.getProfileUrl()?.toExternalForm();
Null Coalescing Operator (??)
Null Coalescing Operator (??) covers cases where the left-hand side is
null
- LWC ; Apexundefined
- LWC
Lightning Web Components
The nullish coalescing (??) operator is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined, and otherwise returns its left-hand side operand. [7]
❌
function getUserLanguage() {
if (currentUser.preferredLanguage) {
return currentUser.preferredLanguage;
}
return 'en_US';
}
✅
function getUserLanguage() {
return currentUser.preferredLanguage ?? 'en_US';
}
Apex
The ?? operator returns the left-hand argument if the left-hand argument isn’t null. Otherwise, it returns the right-hand argument. Similar to the safe navigation operator (?.), the null coalescing operator (??) replaces verbose and explicit checks for null references in code. [6]
❌
public static String getUserLanguage() {
if (currentUser.preferredLanguage == null) {
return 'en_US';
}
return currentUser.preferredLanguage;
}
✅
public static String getUserLanguage() {
return currentUser.preferredLanguage ?? 'en_US';
}
OR Operator (||)
OR Operator (||) covers the following cases where the left-hand side is
null
undefined
NaN
- empty string ("" or '' or \
\
) - 0
Lightning Web Components
❌
this.showToast({
title: 'Unexpected error',
message: error.message ? error.message : 'Contact your administrator!',
wariant: 'error'
});
✅
this.showToast({
title: 'Unexpected error',
message: error?.message || 'Contact your administrator!',
wariant: 'error'
});
Reduce Nesting
Avoid deep nesting of conditional and loops.
Return Early
Early return can be accomplished by reverting the if
conditions, handling the necessary error, and returning or throwing an adequate exception. Early return is a great technique for decreasing nesting thereby code complexity.
❌
function calculateVisibleItems(mediaQuery) {
if (mediaQuery) {
let visibleItems = null;
if (mediaQuery.isMobile()) {
visibleItems = 2;
} else if (mediaQuery.isTablet()) {
visibleItems = 3;
} else if (mediaQuery.isSmallDesktop()) {
visibleItems = 4;
} else if (mediaQuery.isDesktop()) {
visibleItems = 6;
} else {
visibleItems = 1;
}
return visibleItems;
} else {
throw new Error('mediaQuery is not defined!');
}
}
✅
function calculateVisibleItems(mediaQuery) {
if (!mediaQuery) {
throw new Error('mediaQuery is not defined!');
}
if (mediaQuery.isMobile()) {
return 2;
}
if (mediaQuery.isTablet()) {
return 3;
}
if (mediaQuery.isSmallDesktop()) {
return 4;
}
if (mediaQuery.isDesktop()) {
return 6;
}
return 1;
}
Invert if statement
Inverting ifs is a technique used to improve readability, simplify logic, or make the code more expressive. Instead of having nested or deeply nested if statements, the conditionals are rearranged to reduce complexity and improve clarity.
❌
function calculateBonus(salary, performanceRating) {
if (performanceRating >= 0 && performanceRating <= 10) {
if (salary >= 0) {
if (salary < 50000) {
if (performanceRating < 5) {
return "No bonus";
} else {
if (salary < 70000) {
if (performanceRating >= 5) {
return "Small bonus";
} else {
if (salary < 100000) {
if (performanceRating >= 8) {
return "Medium bonus";
} else {
return "Large bonus";
}
} else {
return "Large bonus";
}
}
} else {
if (performanceRating >= 8) {
return "Medium bonus";
} else {
return "Large bonus";
}
}
}
} else {
if (salary < 70000) {
if (performanceRating >= 5) {
return "Small bonus";
} else {
if (salary < 100000) {
if (performanceRating >= 8) {
return "Medium bonus";
} else {
return "Large bonus";
}
} else {
return "Large bonus";
}
}
} else {
if (performanceRating >= 8) {
return "Medium bonus";
} else {
return "Large bonus";
}
}
}
} else {
return "Invalid salary";
}
} else {
return "Invalid performance rating";
}
}
✅
function calculateBonus(salary, performanceRating) {
if (performanceRating < 0 || performanceRating > 10) {
return "Invalid performance rating";
}
if (salary < 0) {
return "Invalid salary";
}
if (salary < 50000 && performanceRating < 5) {
return "No bonus";
}
if (salary < 70000 && performanceRating >= 5) {
return "Small bonus";
}
if (salary < 100000 && performanceRating >= 8) {
return "Medium bonus";
}
return "Large bonus";
}
Extract Methods
Extracting methods in programming refers to breaking down a larger block of code into smaller, more manageable and reusable units.
Create separated methods for complex code to reduce cognitive complexity.
❌
function handleClick(e) {
if (e.detail.action === 'save') {
// ...
// ...
// ...
} else if (e.detail.action === 'remove') {
// ...
// ...
// ...
} else if (e.detail.action === 'cancel') {
// ...
// ...
// ...
}
}
✅
function handleClick(e) {
const actionToHandler = {
save: this.save,
remove: this.remove,
cancel: this.cancel
};
actionToHandler?.[e.detail.action]?.();
}
const save = () => {
// ...
}
const remove = () => {
// ...
}
const cancel = () => {
// ...
}
Avoid Flags
Flag (Boolean/Checkbox) stores only two values: true
or false
. It's serious problem if we are talking about system extendability.
Apex
❌
public class UserFeatures {
public static Features getStandardUserFeatures() {
// ...
}
public static Features getPremiumUserFeatures() {
// ...
}
public static Features getGoldUserFeatures() {
// ...
}
}
public class CurrentUser {
private Boolean isStandardType = true;
private Boolean isPremiumType = false;
private Boolean isGoldType = false;
public CurrentUser(
Boolean isStandardType,
Boolean isPremiumType,
Boolean isGoldType
) {
this.isStandardType = isStandardType;
this.isPremiumType = isPremiumType;
this.isGoldType = isGoldType;
}
public Features getUserFeatures() {
if (isStandardType) {
return UserFeatures.standardUserFeatures();
}
if (isPremiumType) {
return UserFeatures.premiumUserFeatures();
}
if (isGoldType) {
return UserFeatures.getGoldUserFeatures();
}
}
}
✅
public enum UserType {
STANDARD, PREMIUM, GOLDEN
}
public interface UserFeatures {
Features get();
}
public class StandardUserFeatures implements UserFeatures {
public Features get() {
// ...
}
}
public class PremiumUserFeatures implements UserFeatures {
public Features get() {
// ...
}
}
public class GoldUserFeatures implements UserFeatures {
public Features get() {
// ...
}
}
public class UserTypeFeatureConfig {
public final Map<UserType, System.Type> USER_TYPE_TO_FEATURES_CLASS = new Map<UserType, System.Type>{
UserType.STANDARD => StandardUserFeatures.class,
UserType.PREMIUM => PremiumUserFeatures.class,
UserType.GOLD => GoldUserFeatures.class
}
public Features getFeatureForUserType(UserType userType) {
return ((UserFeatures) USER_TYPE_TO_FEATURES_CLASS.get(userType).newInstance()).get();
}
}
public class CurrentUser {
private UserType userType = null;
public CurrentUser(UserType userType) {
this.userType = userType;
}
public Features getUserFeatures() {
return UserTypeFeatureConfig.getFeatureForUserType(this.userType);
}
}
❌
const user = {
isStandardUser: false,
isPremiumUser: true,
isGoldUser: false
};
function getUserFeatures() {
if (user.isStandardUser) {
return getStandardUserFeatures();
}
if (user.isPremiumUser) {
return getPremiumUserFeatures();
}
if (user.isGoldUser) {
return getGoldUserFeatures();
}
return null;
}
const getStandardUserFeatures = () => {}
const getPremiumUserFeatures = () => {}
const getGoldUserFeatures = () => {}
✅
const USER_TYPES = {
STANDARD: 'STANDARD',
PREMIUM: 'PREMIUM',
GOLD: 'GOLD'
};
const user = {
type: USER_TYPES.PREMIUM
};
function getUserFeatures() {
const userTypeToFeature = {
[USER_TYPES.STANDARD]: getStandardUserFeatures,
[USER_TYPES.PREMIUM]: getPremiumUserFeatures,
[USER_TYPES.GOLD]: getGoldUserFeatures
};
return userTypeToFeature?.[user.type]?.() || null;
}
const getStandardUserFeatures = () => {}
const getPremiumUserFeatures = () => {}
const getGoldUserFeatures = () => {}
❌
function calculateVisibleItems() {
if (this.mediaQuery.isMobile()) {
this.visibleItems = 2;
} else if (this.mediaQuery.isTablet()) {
this.visibleItems = 3;
} else if (this.mediaQuery.isSmallDesktop()) {
this.visibleItems = 4;
} else if (this.mediaQueries.isDesktop()) {
this.visibleItems = 6;
} else {
this.visibleItems = 1;
}
}
✅
const SCREEN_TYPES = {
MOBILE: 'MOBILE',
TABLET: 'TABLET',
SMALL_DESKTOP: 'SMALL_DESKTOP',
DESKTOP: 'DESKTOP'
};
function calculateVisibleItems() {
const screenTypeToItemsAmount = {
[SCREEN_TYPES.MOBILE]: 2,
[SCREEN_TYPES.TABLET]: 3,
[SCREEN_TYPES.SMALL_DESKTOP]: 4,
[SCREEN_TYPES.DESKTOP]: 6
};
this.visibleItems = screenTypeToItemsAmount[this.mediaQuery.screenType] ?? 1;
}
Use Expressions
Try to transform your code into formula or expression.
It allows you to avoid if
statements and make code easier to read and understand.
❌
function hasAllRequiredFields(account) {
if (!account.Name) {
throw new Error('Name is required!');
} else if (!account.BillingCity) {
throw new Error('BillingCity is required!');
} else if (!account.BillingCountry) {
throw new Error('BillingCountry is required!');
} else if (!account.BillingPostalCode) {
throw new Error('BillingPostalCode is required!');
} else if (!account.Industry) {
throw new Error('Industry is required!');
}
return true;
}
✅
function hasAllRequiredFields(account) {
const requiredFields = [
{ apiName: 'Name', error: 'Name is required!' },
{ apiName: 'BillingCity', error: 'Billing City is required!' },
{ apiName: 'BillingCountry', error: 'Billing Country is required!' },
{ apiName: 'BillingPostalCode', error: 'Billing Postal Code is required!' },
{ apiName: 'Industry', error: 'Industry is required!' }
];
const errorMessage = requiredFields
.filter(fieldConfig => !account[fieldConfig.apiName])
.map(fieldConfig => fieldConfig.error)
.join(' ');
if (errorMessage) {
throw new Error(errorMessage);
}
return true;
}
❌
get alertCssClasses() {
let cssClasses = 'slds-notify slds-notify_alert';
if (this.variant === VARIANTS.WARNING) {
cssClasses += 'slds-alert_warning';
} else if (this.variant === VARIANTS.ERROR) {
cssClasses += 'slds-alert_error';
} else if (this.variant === VARIANTS.OFFLINE) {
cssClasses += 'slds-alert_offline';
}
return cssClasses;
}
✅
function classSet(classToVisibility) {
return Object.keys(classToVisibility)
.filter(cssClass => classToVisibility[cssClass])
.join(' ');
}
get alertCssClasses() {
return classSet({
'slds-notify': true,
'slds-notify_alert': true,
'slds-alert_warning': this.variant === VARIANTS.WARNING,
'slds-alert_error': this.variant === VARIANTS.ERROR,
'slds-alert_offline': this.variant === VARIANTS.OFFLINE
});
}
Use Lists
Why is a List/Array better than a direct condition?
- Easier to understand: An additional variable provides additional context.
- Flexibility: Sage names can be stored as constants or even moved to custom metadata/settings.
- Easier to change: Need a few more stages? No problem, just add them to the list.
❌
if (opportunity.StageName == 'Prospecting' ||
opportunity.StageName == 'Qualification' ||
opportunity.StageName == 'Needs Analysis') {
// ...
}
✅
if (new List<String>{
'Prospecting', 'Qualification', 'Needs Analysis'
}.contains(opportunity.StageName)) {
// ...
}
List<String> emailNotificationOpportunityStages = new List<String>{
'Prospecting', 'Qualification', 'Needs Analysis'
};
if (emailNotificationOpportunityStages.contains(opportunity.StageName)) {
// ...
}
Use Map
❌
public class FileLoaderService {
public ContentVersion getFile(String sourceSystem, Id fileId) {
if (sourceSystem == 'Salesforce') {
return getFileFromSalesforce(fileId);
}
if (sourceSystem == 'AWS') {
return getFileFromAws(fileId);
}
if (sourceSystem == 'Firebase') {
return getFileFromFirebase(fileId);
}
// ...
}
public ContentVersion getFileFromSalesforce(Id fileId) {
// ...
}
public ContentVersion getFileFromAws(Id fileId) {
// ...
}
public ContentVersion getFileFromFirebase(Id fileId) {
// ...
}
}
✅
public enum FileSourceSystem {
SALESFORCE, AWS, FIREBASE
}
public interface FileLoader {
ContentVersion getFile(Id fileId);
}
public class SalesforceFileLoader implements FileLoader {
public ContentVersion getFile(Id fileId) {
// ...
}
}
public class AwsFileLoader implements FileLoader {
public ContentVersion getFile(Id fileId) {
// ...
}
}
public class FirebaseFileLoader implements FileLoader {
public ContentVersion getFile(Id fileId) {
// ...
}
}
public class FileLoaderService {
public final Map<FileSourceSystem, System.Type> FILE_SOURCE_SYSTEM_TO_LOADER = new Map<UserType, System.Type>{
FileSourceSystem.SALESFORCE => SalesforceFileLoader,
FileSourceSystem.AWS => AwsFileLoader,
FileSourceSystem.FIREBASE => FirebaseFileLoader,
}
public FileLoader getFile(FileSourceSystem sourceSystem, Id fileId) {
return ((FileLoader) FILE_SOURCE_SYSTEM_TO_LOADER.get(sourceSystem).newInstance()).getFile(fileId);
}
}
Use Objects
Objects can help you replace if statements with simple mapping. That technique decreases code complexity to 0!
❌
function myFun(param) {
let response;
if (param === 'A') {
response = 1;
} else if (param === 'B') {
response = 2;
} else if (param === 'C') {
response = 3;
} else {
response = 0;
}
return response;
}
✅
function myFun(param) {
const paramToValue = {
A: 1,
B: 2,
C: 3
};
return paramToValue[param] || 0;
}
❌
handleRowAction(event) {
const actionName = event.detail.action.name;
const row = event.detail.row;
if (actionName === 'delete') {
this.deleteRow(row.Id, row.Name);
} else if (actionName === 'edit') {
this.editRow(row);
} else if (actionName === 'navigate') {
this.navigateToRow(row);
}
}
✅
handleRowAction(event) {
const actionToHandler = {
delete: this.deleteRow,
edit: this.editRow,
navigate: this.navigateToRow
};
const actionName = event.detail.action.name;
const row = event.detail.row;
return actionToHandler[actionName]?.(row);
}
Use proper JavaScript Methods
JavaScript has a lot of predefined methods that can help you avoid IFs. Understand the purpose of each method to make your code cleaner. Below you will find examples for the most common ones.
Array.find
The find() method of Array instances returns the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.
❌
function findAccountWithId(accountId) {
let searchedAccount = null;
this.accounts.forEach(account => {
if (account.Id === accountId) {
searchedAccount = account;
}
});
return searchedAccount;
}
✅
function findAccountWithId(accountId) {
return this.accounts.find(account => account.accountId);
}
Array.filter
The filter() method of Array instances creates a shallow copy of a portion of a given array, filtered down to just the elements from the given array that pass the test implemented by the provided function.
❌
function extractPersonAccounts() {
const personAccounts = [];
this.accounts.forEach(account => {
if (account.IsPersonAccount) {
personAccounts.push(account);
}
});
return personAccounts;
}
✅
function containsPersonAccount() {
return this.accounts.filter(account => account.IsPersonAccount);
}
Array.some
The some() method of Array instances tests whether at least one element in the array passes the test implemented by the provided function. It returns true if, in the array, it finds an element for which the provided function returns true; otherwise it returns false. It doesn't modify the array.
❌
function containsPersonAccount() {
let hasPersonAccount = false;
this.accounts.forEach(account => {
if (account.IsPersonAccount) {
hasPersonAccount = true;
}
});
return hasPersonAccount;
}
✅
function containsPersonAccount() {
return this.accounts.some(account => account.IsPersonAccount);
}
Array.every
The every() method of Array instances tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.
❌
function areFieldsValid() {
let allFieldsValid = true;
this.fields.forEach(field => {
if (!field.validate()) {
allFieldsValid = false;
}
});
return allFieldsValid;
}
✅
function areFieldsValid() {
return this.fields.every(field => field.validate());
}
Design Patterns
Utilize well-known design patterns such as Factory Method, Abstract Factory, Builder or Strategy to encapsulate complex behaviors and reduce cognitive overhead. Design patterns provide standardized solutions to common software design problems, making codebases more modular and maintainable.
❌
public class DataFactory {
public static SObject createRecord(SObjectType type) {
if (type == Account.SObjectType) {
return createAccount();
} else if (type == Contact.SObjectType) {
return createContact();
}
}
private static Account createAccount() {
Account account = new Account(
Name = 'My Account',
Email = 'myAccount@email.com'
);
insert account;
return account;
}
private static Contact createContact() {
Contact contact = new Contact(
LastName = 'My Contact'
);
insert contact;
return contact;
}
}
Account account = (Account) DataCreator.createRecord(Account.sObjectType);
Contact contact = (Contact) DataCreator.createRecord(Contact.sObjectType);
✅
// DataFactory.cls
public abstract class DataFactory {
public abstract sObject getRecord();
public sObject createRecord() {
sObject record = this.getRecord();
insert record;
return record;
};
}
// AccountFactory.cls
public class AccountFactory extends DataFactory {
public sObject getRecord() {
return new Account(
Name = 'My Account'
);
}
}
// ContactFactory.cls
public class ContactFactory extends DataFactory {
public override sObject getRecord() {
return new Contact(
LastName = 'My Contact'
);
}
}
// DataCreator.cls
public class DataCreator {
private final Map<sObjectType, System.Type> OBJECT_TO_FACTORY = new Map<sObjectType, System.Type>{
Account.sObjectType => AccountFactory.class,
Contact.sObjectType => ContactFactory.class
};
public static Object createRecord(sObjectType objectTypeToCreate) {
DataFactory factory = (DataFactory) OBJECT_TO_FACTORY.get(objectTypeToCreate).newInstance();
return factory.createRecord();
}
}
// Usage
Account account = (Account) DataCreator.createRecord(Account.sObjectType);
Contact contact = (Contact) DataCreator.createRecord(Contact.sObjectType);
Extract rules
Your code works perfectly, which is great, but it doesn’t mean it's ready. As people, we often don’t see the bigger picture at the beginning. Always review your code and try to extract general rules!
Business rules presented by data structures are fantastic! Code like that is really easy to follow, easy to change, and flexible. It also gives us the possibility to move those rules to a database so they're not hardcoded.
❌
function calculateBonus(yearsOfExperience, performanceRating) {
let bonus = 0;
if (yearsOfExperience >= 5) {
if (performanceRating >= 8) {
bonus = 10000;
} else {
if (performanceRating >= 6) {
bonus = 7500;
} else {
if (performanceRating >= 4) {
bonus = 6000;
} else {
bonus = 5000;
}
}
}
} else {
if (yearsOfExperience >= 3) {
if (performanceRating >= 8) {
bonus = 8000;
} else {
if (performanceRating >= 6) {
bonus = 5500;
} else {
if (performanceRating >= 4) {
bonus = 4500;
} else {
bonus = 3500;
}
}
}
} else {
if (performanceRating >= 8) {
bonus = 6000;
} else {
if (performanceRating >= 6) {
bonus = 4000;
} else {
if (performanceRating >= 4) {
bonus = 3000;
} else {
bonus = 2000;
}
}
}
}
}
return bonus;
}
✅
function calculateBonus(yearsOfExperience, performanceRating) {
const bonusConfig = [
{
yearsOfExperience: { from: 5, to: 100 },
bonusPerRating: [
{ from: 1, to: 3, amount: 5000 },
{ from: 3, to: 5, amount: 6000 },
{ from: 5, to: 8, amount: 7500 },
{ from: 8, to: 10, amount: 10000 }
]
},
{
yearsOfExperience: { from: 3, to: 5 },
bonusPerRating: [
{ from: 1, to: 3, amount: 3500 },
{ from: 3, to: 5, amount: 4500 },
{ from: 5, to: 8, amount: 5500 },
{ from: 8, to: 10, amount: 8000 }
]
},
{
yearsOfExperience: { from: 1, to: 3 },
bonusPerRating: [
{ from: 1, to: 3, amount: 2000 },
{ from: 3, to: 5, amount: 3000 },
{ from: 5, to: 8, amount: 4000 },
{ from: 8, to: 10, amount: 6000 }
]
}
];
const currentBonusForExperienceYears = bonusConfig.find(setting => setting.yearsOfExperience.from <= yearsOfExperience && yearsOfExperience <= setting.yearsOfExperience.to);
const currentBonusForPerformanceRating = currentBonusForExperienceYears?.bonusPerRating.find(bonusRating => bonusRating.from <= performanceRating && performanceRating <= bonusRating.to);
return currentBonusForPerformanceRating?.amount || 0;
}
Conclusion
Reducing cognitive complexity is essential for building maintainable, scalable, and collaborative codebases. By prioritizing readability, simplicity, and clarity in software design and implementation, teams can mitigate the risks associated with complex code, such as decreased productivity, higher technical debt, and increased maintenance overhead. By leveraging tools, techniques, and best practices aimed at reducing cognitive complexity, developers can create software that is not only functional but also comprehensible, adaptable, and resilient in the face of changing requirements and evolving business needs. Remember, the true measure of code quality is not just its correctness but also its clarity and simplicity. Strive for code that is not only elegant and efficient but also easy to understand and maintain by both current and future contributors.
If you have any questions feel free to ask in the comment section below. 🙂
Was it helpful? Check out our other great posts here.