SOLID Design Principles
You have probably been hearing a lot about SOLID design principles and how they help you create a codebase that is legible, scalable, and agile. They can seem complex at first, especially on their own. If you can understand how they all work together, you can unlock strategies for writing reusable code and smaller classes. Read on to learn about the benefits of SOLID and how they can be used in Apex for some common scenarios with Financial Services Cloud.
Single Responsibility
The single responsibility principle states that there should never be more than one reason to change any class. Put another way, a class should only be responsible for doing one thing. Let’s say you had a class that represented a customer and had methods for different actions a customer could take.
Here’s an example of a class that is violating the single responsibility principle:
This class is responsible for relating the customer to a contact, getting a list of all the related contacts, adding a financial account, converting a lead, and buying products. If any one of those business rules changes, this class needs an update. If two of them need changes at the same time, you could run into issues with developers working on the same class simultaneously such as merge conflicts or overwriting each other’s work.
In general, fewer changes mean fewer opportunities for bugs to emerge and fewer dependencies.
However, it is important to understand that the single responsibility principle is fairly subjective, and you don’t want to take it too far in either direction.
This class technically only does one thing, updating the Customer, but that’s very vague and would probably need to be an overly large method.
On the other hand, these classes are unnecessarily granular:
Open-Closed
The Open-Closed principle states that software entities such as classes or methods should be open for extension but closed for modification. Essentially, this means when you have new requirements, it’s better to create new code around the existing working code than to update it. For example, let’s say you have a program with a default payment schedule, but there’s a promotion in certain states that allows you to extend it.
Instead of making an update to the existing method like this:
It would be better to create a new class that implements the same interface but has its own getLength method.
This way, all the existing codebase that uses the standard program isn’t affected by any changes caused by the promotional program. This principle allows you to build up a base of thoroughly tested and reliable code that changes very infrequently.
Liskov Substitution
This principle, introduced by Barbara Liskov in 1988, states that you should be able to replace any instance of a class with any of its children without causing any issues. In other words, a child class should implement all of its parents' methods and parents should only have attributes that apply to all of their children.
The following example violates the Liskov Substitution principle because a DepositOnlyAccount can’t use the withdraw method.
The following structure does not violate Liskov Substitution. Financial accounts could be replaced by any of its children since they all would make use of the deposit method. Withdrawal accounts can be substituted with any of its children.
Interface Segregation
The interface segregation principle is very similar to the Liskov substitution principle, and states that a class should not depend on methods it does not use. The result of the following interface segregation and Liskov substitution is smaller interfaces like we saw with the Financial Accounts. This example violates interface segregation:
Mortgages and Credit Cards are both products, but they are dissimilar enough that having only one interface for them creates dependencies on methods they won’t need. You might notice this example is very similar to the Liskov substitution one, however, it uses interfaces instead of virtual classes.
There might still be some commonalities between mortgages and credit cards where they need a shared interface, but more granular interfaces should also be created so they can only implement what is relevant for their specific use case.
Here’s one way to create the data structures so they no longer violate interface segregation:
Dependency Inversion
The dependency inversion principle that high-level modules should not depend on low-level modules, both should depend on abstractions. Similarly, abstractions should not depend on details, details should depend on abstractions. Essentially, a class should refer to interfaces and let the class that calls it set the details.
Dependency is frequently handled with dependency injection, which is when any dependencies for a class are set in the constructor. A quick way to tell if you’re violating this principle is if you’re using the new keyword in your class.
The following ProductHandler violates dependency inversion.
You would need to create a method for every type of product.
As long as we followed all of our other principles, we should be able to consolidate this into one method.
We’re using dependency injection here by having all of the dependencies set in the constructor. Since we followed the open-closed principle, interface segregation, and Liskov substitution, we can be confident that this code will work for all children of the Product class.
Summary
The SOLID design principles all work together to help you write stable, understandable, and extendable code. The benefits of using these guidelines are smaller apex classes, fewer dependencies, and smaller deployments. In order to implement these guidelines, you do need to spend a bit more time upfront planning and being more intentional with your design, but you will make that time up quickly with fewer bugs and quicker onboarding for new developers.
Additional Resources:
● Interfaces | Apex Developer Guide
● Extending a Class | Apex Developer Guide