LSP in Apex: The Art of Predictable Code
How to ensure your subclasses aren't secretly breaking your Salesforce org.
LSP: Subtypes Should Just Work
The Liskov Substitution Principle (LSP) states that you should be able to replace a parent class with any of its child classes and everything should still work exactly as expected.
In Salesforce, LSP is the "Trust Principle." If your code expects a generic SObject processor, it shouldn't crash just because you handed it a Lead instead of an Account.
The "False Promise" (The Wrong Way)
A classic LSP violation in Salesforce is forcing a subclass to inherit a method it physically cannot perform.
public abstract class DataExporter {
public abstract void fetchData();
public abstract void sendToFtp(); // Every exporter must send to FTP?
}
public class CsvExporter extends DataExporter {
public override void fetchData() { /* Logic */ }
public override void sendToFtp() { /* Logic */ }
}
public class LocalPreviewExporter extends DataExporter {
public override void fetchData() { /* Logic */ }
public override void sendToFtp() {
// ERROR: This exporter is only for internal previews!
throw new UnsupportedOperationException('Previewers cannot use FTP');
}
}Why this fails: If a developer writes a batch job that calls .sendToFtp() on a list of DataExporter objects, your code will explode mid-transaction. You've broken the contract of the parent class.
The "Substitution" Fix (The Right Way)
To fix this, keep the base class (or interface) minimal. Only include methods that are guaranteed to be valid for every single child.
1. The Safe Base
public abstract class DataExporter {
public abstract void fetchData();
}
public interface IFtpUploadable {
void sendToFtp();
}2. The Reliable Implementation
Now, your code can safely treat every exporter as a data-fetcher, but only attempts FTP uploads on classes that actually support it.
public class CsvExporter extends DataExporter implements IFtpUploadable {
public override void fetchData() { /* ... */ }
public void sendToFtp() { /* ... */ }
}
public class LocalPreviewExporter extends DataExporter {
public override void fetchData() { /* ... */ }
// No FTP method, so no one can mistakenly call it.
}LSP and Governor Limits
In Apex, LSP also applies to resources. If your parent class is designed to be bulk-safe, your child class cannot suddenly run a SOQL query inside a loop. Doing so violates the "contract" of efficiency that the caller expects from the parent.
-
Input parameters: Subclasses shouldn't have stricter validation than parents.
-
Output values: Subclasses should return the same type (or a more specific subtype) as parents.
-
Exception types: Subclasses shouldn't throw new, unexpected exception types that the caller isn't prepared to catch.
Summary
When you follow LSP, you build predictable systems. You can swap components out like Lego bricks without worrying that a specific "color" of brick is going to cause a CPU timeout or a null pointer exception.