Embracing SOLID Principles in Apex: A Guide for Salesforce Developers

Deep Banerjee
5 min readJan 27, 2024

--

Solid Principles

Salesforce development often involves creating robust and scalable solutions to meet the dynamic needs of businesses. To achieve this, developers can turn to SOLID principles — a set of design principles that promote maintainability, flexibility, and scalability in object-oriented programming. In this blog post, we’ll explore how Salesforce developers can apply SOLID principles in Apex.

1. S- Single Responsibility Principle (SRP):

The Single Responsibility Principle states that a class should have only one reason to change. In Apex, this means each class should focus on a single task.

Business Case: Assume you are building a Salesforce application for a financial institution. You have a class responsible for handling both customer data validation and database interactions.

// Before applying SRP
public class CustomerHandler {
public Boolean validateCustomerData(Account customer) {
// validation logic
}

public void saveCustomerToDatabase(Account customer) {
// database interaction logic
}
}

After applying SRP:

Separate these responsibilities into two classes:

// After applying SRP
public class CustomerValidator {
public Boolean validateCustomerData(Account customer) {
// validation logic
}
}

public class CustomerDatabaseHandler {
public void saveCustomerToDatabase(Account customer) {
// database interaction logic
}
}

By doing this, each class has a single responsibility, making the code more maintainable and allowing for easier updates.

2.O- Open/Closed Principle (OCP):

The Open/Closed Principle encourages developers to design classes in a way that allows extension without modifying existing code. In Apex, we can achieve this through interfaces and abstract classes.

Business Case: Imagine you are developing a Salesforce application for inventory management. Initially, you have a class handling product pricing, but you want to extend it without modifying the existing code.

// Before applying OCP
public class ProductPricer {
public Decimal calculatePrice(Product product) {
// pricing logic
}
}

After applying OCP:

Introduce an interface and create specific implementations:

// After applying OCP
public interface PriceCalculator {
Decimal calculatePrice(Product product);
}

public class StandardPriceCalculator implements PriceCalculator {
public Decimal calculatePrice(Product product) {
// standard pricing logic
}
}

public class DiscountedPriceCalculator implements PriceCalculator {
public Decimal calculatePrice(Product product) {
// discounted pricing logic
}
}

Now, new pricing strategies can be added without modifying the existing .

3. L- Liskov Substitution Principle (LSP):

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In Apex, this means that subclasses should extend the behavior of their parent class without altering it.

Business Case: In your Salesforce project for a healthcare provider, you have a class hierarchy for managing patient records. Ensure that subclasses can be used interchangeably without affecting the functionality.

// Before applying LSP
public class PatientRecord {
public String generateReport() {
// common logic for generating a patient report
}
}

public class PediatricPatientRecord extends PatientRecord {
public String generateReport() {
// logic for generating a pediatric patient report
}
}

After applying LSP:

Use an abstract class to represent the common behavior:

// After applying LSP
public abstract class PatientRecord {
public abstract String generateReport();
}

public class PediatricPatientRecord extends PatientRecord {
public String generateReport() {
// logic for generating a pediatric patient report
}
}

Now, subclasses can be substituted without affecting the behaviour of the parent class.

4. I- Interface Segregation Principle (ISP):

The Interface Segregation Principle suggests that a class should not be forced to implement interfaces it does not use. In Apex, this means breaking down large interfaces into smaller, more specific ones.

Business Case: Consider a scenario in a Salesforce project where you are implementing a system for tracking customer interactions. You have a comprehensive logging interface, but not all classes need to implement all methods.

// Before applying ISP
public interface CustomerInteractionLogger {
void logInfo(String message);
void logError(String message);
void logDebug(String message);
}

After applying ISP:

Split the interface into smaller, more specific interfaces:

// After applying ISP
public interface InfoLogger {
void logInfo(String message);
}

public interface ErrorLogger {
void logError(String message);
}

public interface DebugLogger {
void logDebug(String message);
}

Now, classes can implement only the interfaces relevant to their needs, avoiding unnecessary dependencies.

5. D- Dependency Inversion Principle (DIP):

This principle states that “ High-level modules should not import anything from low-level modules.” Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

In simpler terms , “Dependency Inversion” is the strategy of depending upon interfaces or abstract functions and classes rather than upon concrete functions and classes. Simply put, when components of our system have dependencies, we don’t want directly inject a component’s dependency into another.

Business Case: In your Salesforce project, you have a class that directly depends on a specific external service for data retrieval, violating the Dependency Inversion Principle.

// Before applying DIP
public class DataService {
private ExternalApiService apiService;

public DataService() {
this.apiService = new ExternalApiService();
}

public List<Account> getAccounts() {
// using the external service to retrieve accounts
return apiService.retrieveAccounts();
}
}

After applying DIP:

Introduce an interface for the external service and use dependency injection to pass the service into the class.

// After applying DIP
public interface DataRetrievalService {
List<Account> retrieveAccounts();
}

public class ExternalApiService implements DataRetrievalService {
public List<Account> retrieveAccounts() {
// implementation for retrieving accounts
}
}

public class DataService {
private DataRetrievalService dataRetrievalService;

public DataService(DataRetrievalService dataRetrievalService) {
this.dataRetrievalService = dataRetrievalService;
}

public List<Account> getAccounts() {
// using the injected service to retrieve accounts
return dataRetrievalService.retrieveAccounts();
}
}

By applying the Dependency Inversion Principle, the DataService class is no longer tightly coupled to a specific implementation of the data retrieval service. This makes it easier to replace or update the external service without modifying the DataService class, promoting flexibility and maintainability in the Salesforce codebase.

Thanks for reading!

--

--