Tip #28: The Liskov Substitution Principle (LSP)
Word to remember: behavior
Definition: Subtypes must be substitutable for their base types.
This is another Robert Martin’s definition, but Barbara Liskov wrote this principle in 1988 is her Data Abstraction and Hiearchy paper like this:
If for each object of type S there is an object of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when is substituted for then S is a subtype of T
And she also defines subtypes, so we can have a better picture for the definition:
The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra.
(Remember, this is from 1988—impressive!)
Let’s revisit our example from the last article:
public interface Payment {
void processPayment(double amount);
}
public class CreditCardPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
}
public class PayPalPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}
With interfaces, it’s easier to see, right?
In this case, PayPalPayment
(S) is a subtype of Payment
(T).
Now, let’s modify the interface by adding a refund method:
public interface Payment {
void processPayment(double amount);
void refundPayment(double amount); // New method added
}
public class CreditCardPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
public void refundPayment(double amount) {
System.out.println("Refunding credit card payment of $" + amount);
}
}
public class PayPalPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
// No implementation or unsupported behavior for refund
public void refundPayment(double amount) {
throw new UnsupportedOperationException("Refunds not supported for PayPal payments.");
}
}
Now we have an issue, right? You can see the service code here:
Our service will break if we need to refund a payment and it’s not a credit card. The throw exception
changes the expected behavior, breaking the LSP.
If you don’t add refundPayment
to the interface but only implement it in the CreditCardPayment
class and inject it into the service, you will break LSP again.
How to avoid?
Maybe you don’t need inheritance, or perhaps the behavior of your classes isn’t well-defined. This is a good exercise in object-oriented design, and with experience, you’ll get better at it.
One thing to keep in mind: If you’re breaking LSP, you’re likely breaking OCP as well. So, mastering both principles will help you design better software.
Happy coding!