When it comes to Apex, many developers write imperative code that starts at the beginning and runs to the bottom. This works great for small amounts of code but when one needs a more complex design, Object-Oriented Programming is a better fit.
This article covers some Object-Oriented Design Principles that every Salesforce Developer needs to know.
What Is an Object-Oriented Design Principle?
It is a principle that guides one on how to think and write their code so that it is easier to work with, simpler to understand, and requires less maintenance overall. Often, it also leads to greater flexibility and faster enhancements.
There are many Object-Oriented Design Principles, but here are some important ones. Let’s dive in…
DRY Principle
The DRY Principle is an acronym for “Don’t Repeat Yourself”. It means that one’s code should not be copied and pasted throughout the codebase. At first, this might seem fast, and productivity is high. However, if a change is required, that change must be applied to every instance where the code was copied and pasted, which takes a lot of time and effort. Additionally, if one instance is missed to update, a bug is introduced.
DRY Example
Let’s say that a contact’s full name is composed of [FirstName] space [LastName], and there’s Apex code like this copied and pasted throughout the codebase:
String fullName = contact.FirstName + contact.LastName;
Now, the business comes along and requests that the person’s middle name should be included as well. A developer now has to update each code instance where that is done.
What should have been done instead? Well, one way is to put that logic in a function and then use that everywhere as needed:
public class ContactUtil {
public String getFullName(Contact contact) {
return contact.FirstName + ' ' + contact.LastName;
}
}
This class is a utility class where one puts simple one-off functions that are all related to something. In this case, it deals with contact-related functionality.
Now, all the code that needs a contact’s full name can invoke the ContactUtil.getFullName
to do so, like this:
String fullName = ContactUtil.getFullName(contact);
If the middle name is needed, now only the getFullName
function has to change:
public class ContactUtil {
public String getFullName(Contact contact) {
return contact.FirstName + ' ' + contact.MiddleName + ' ' + contact.LastName;
}
}
Single Responsibility Principle
The Single Responsibility Principle says that if a change is needed, it should only need to be done in one location within the codebase. This can be applied at various levels, such as the function level, class level, and higher.
Test Data Example
One common pattern used is to place all the SObject test data creation in a single class, such as TestData
or TestUtil
. At first, it seems like a good idea because it’s better than copying and pasting your code from one test class to another. However, it doesn’t scale as the number of objects and their fields grows, so it becomes very hard to maintain.
@isTest
public class TestData {
public static Account createAccount(String name) {
Account a = new Account(Name = name);
return a;
}
Public static Account insertAccount(String name) {
Account a = createAccount(name);
insert a;
return a;
}
public static Contact createContact(String name) {
// Contact Code Here
}
public static Contact insertContact(String name) {
// Insert contact code here
}
// Other Objects here
}
Some may be thinking that this doesn’t look so bad. However, this quickly becomes unwieldy as the number of fields increases and the number of objects does too. If multiple developers have to create test code in the same class, contention often becomes a problem, and someone’s changes are usually overwritten.
This violates the Single Responsibility Principle because it has multiple SObject creation responsibilities. One test class should be used per SObject, such as AccountTestData
and ContactTestData
. This allows each class to be responsible for one SObject instead of multiple. This leads to multiple classes that are smaller overall, which makes them easier to understand and more maintainable.
Function Example
Let’s look at a function example. A common way to create a test SObject record is like this:
public static Account createAccount(String name, Boolean doInsert) {
Account a = new Account(Name = name);
if (doInsert) {
insert a;
}
return a;
}
The createAccount
function has two responsibilities: building the account record and deciding whether to insert it or not, based on the doInsert
flag. A function should have one responsibility so that if a change is needed, it only has to be done in one location. The other problem with the doInsert
flag is that one needs to know what that parameter is in the calling code to know what value to pass to it.
Account a = createAccount('Name', false);
At first glance, you may think ‘false’ is a value for some field on the account when it’s not! A better implementation is to split that function into two functions: createAccount
and insertAccount
, like this:
public static Account createAccount(String name) {
Account a = new Account(Name = name);
return a;
}
public static Account insertAccount(String name) {
Account a = createAccount(name);
insert a;
return a;
}
Now, the responsibilities are split, and each function has one! Now, callers can decide which one to use based on their needs, and the intention is simpler and clearer. If createAccount
is too similar to insertAccount
, one can rename it as needed.
Using static functions like this for data creation isn’t ideal either, but it is good for illustrative purposes. A better way is to use a Test Data Framework, such as the SObject Test Data Framework. For a deeper dive into Apex testing, check out my Salesforce Apex Testing Essentials course.
Program to an Interface
Program to an interface is another Object-Oriented Design Principle that states one should have their code use interfaces and abstract classes, so they don’t know the actual implementation used. This allows the implementation class to be switched out with a different one if needed without the calling code knowing that it happened.
Pricing Example
Suppose that customer pricing is determined by the customer’s Type
, and Apex is used to determine it. Often, the code starts like this:
public Decimal getCustomerPrice(String customerType) {
Decimal price = null;
if (customerType == 'A') {
// complicated logic here
price = someValue;
}
else if (customerType == 'B') {
// complicated logic here
price = someValue;
}
else if (customerType == 'C') {
// complicated logic here
price = someValue;
}
return price;
}
This leads to a very complicated function that’s hard to maintain long-term, and it has to know all the details. Here’s another way of using an interface:
public interface CustomerPricer {
Decimal getPrice();
}
The interface defines the getPrice
function, which is left to classes to implement:
public class CustomerAPricer implement CustomerPricer {
public Decimal getPrice() {
// complicated logic here
}
}
One class is created for each customer type, and each implements the getPrice
function with its own specific logic. Imagine the CustomerBPricer
and CustomerCPricer
classes are also created, but they’re skipped for brevity.
Next, a Factory
class is used to create the Pricer
to use based on the given customer type:
public class CustomerPricerFactory {
public static CustomerPricer createPricer(String customerType) {
if (customerType == 'A') {
return new CustomerAPricer();
}
else if (customerType == 'B') {
return new CustomerBPricer();
}
else if (customerType == 'C') {
return new CustomerCPricer();
}
return new DefaultPricer();
}
}
This encapsulates the logic so that callers don’t need to know which pricer class is used per customer type. If one needs to swap out the Pricer
to use a different one, it can be done easily, and no calling code needs to change.
The original getCustomerPrice
function can change to this:
public Decimal getCustomerPrice(String customerType) {
return CustomerPricerFactory.createPricer(customerType).getPrice();
}
OR the calling code can directly use the Factory
, like this:
Decimal customerPrice = CustomerPricerFactory.createPricer(customerType).getPrice();
Final Thoughts
Here are some other resources to help continue your learning:
- Clean Code by Robert C. Martin: This is one of my favorite software engineering books. It advocates for the S.O.L.I.D. approach and goes into a lot more detail. The Single Responsibility Principle is the “S” in “S.O.L.I.D.”.
- Refactoring by Martin Fowler: This book shows how to take common coding problems and rework them so that they’re not a problem without breaking the code in a methodical way.
- Object Oriented Programming in Apex Course: This is my course on how to learn Object-Oriented Programming fundamentals in Apex.
Don’t forget to share your thoughts in the comments below!
Comments: