SOLID Made Easy – Liskov Substitution Principle Posted on November 11, 2015 by Benjamin Medina III The third principle in the SOLID principles is the Liskov Substitution Principle (LSP), it was named after Barbara Liskov as together with Jeannette Wing she was able to come up with a definition for subtyping. They defined the principle as: Let (x) be a property provable about objects x of type T. Then (y) should be true for objects y of type S where S is a subtype of T. This principle simply states that all subclasses should operate the same way as the base class. It is also stated that in order to comply with the LSP we must follow these rules: The parameters in the subclass must be less restrictive or the same as those in the base class. The return types of the subclass must be more restrictive or the same as the return type of the base class. The preconditions of a base class must not be strengthened by the subclass. The postconditions of a base class must not be weakened by the subclass. The invariants of a base class must not be changed by a subclass. The subclass should not change the state of an object that is not permitted by the base class. The subclass cannot throw an exception that is not thrown by the base class unless the exception is a subtype of the exception that may be thrown in the base class. Here is a sample code that shows how we can violate this principle: public class CreditCardPaymentService { public string GetCreditCardTransactionToken(string creditCardID, string vendor) { return "A transaction token"; } public string ProcessPayment(decimal amountToPay, int transactionID, string token) { //// perform the payment process here and set the resulting response of the process return "Credit card payment success"; } } public class CashPaymentService { public string ProcessPayment(decimal amountToPay, int transactionID) { //// perform the payment process here and set the resulting response of the process return "Cash payment success"; } } public abstract class PaymentBase { public abstract string Process(decimal amountToPay, int transactionID); } public class CashPayment : PaymentBase { public override string Process(decimal amountToPay, int transactionID) { CashPaymentService service = new CashPaymentService(); string result = service.ProcessPayment(amountToPay, transactionID); return result; } } public class CreditCardPayment : PaymentBase { public string ID { get; set; } public string Vendor { get; set; } public override string Process(decimal amountToPay, int transactionID) { CreditCardPaymentService service = new CreditCardPaymentService(); string token = service.GetCreditCardTransactionToken(this.ID, this.Vendor); string result = service.ProcessPayment(amountToPay, transactionID, token); return result; } } public class PaymentFactory { public static PaymentBase GetPaymentService(PaymentType paymentType) { switch (paymentType) { case PaymentType.Cash: return new CashPayment(); case PaymentType.CreditCard: return new CreditCardPayment(); default: throw new NotImplementedException("Payment type not implemented."); } } } public class PaymentService { public bool ProcessPayment(PaymentType paymentType, decimal amount, int transactionID) { bool result = false; PaymentBase payment = PaymentFactory.GetPaymentService(paymentType); if ((payment as CreditCardPayment) != null) { ((CreditCardPayment)payment).ID = "Credit Card ID"; ((CreditCardPayment)payment).Vendor = "Credit Card Vendor"; } string response = payment.Process(amount, transactionID); if (response.Contains("Credit card payment success") || response.Contains("Cash payment success")) result = true; return result; } } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 public class CreditCardPaymentService{ public string GetCreditCardTransactionToken(string creditCardID, string vendor) { return "A transaction token"; } public string ProcessPayment(decimal amountToPay, int transactionID, string token) { //// perform the payment process here and set the resulting response of the process return "Credit card payment success"; }} public class CashPaymentService{ public string ProcessPayment(decimal amountToPay, int transactionID) { //// perform the payment process here and set the resulting response of the process return "Cash payment success"; }} public abstract class PaymentBase{ public abstract string Process(decimal amountToPay, int transactionID);} public class CashPayment : PaymentBase{ public override string Process(decimal amountToPay, int transactionID) { CashPaymentService service = new CashPaymentService(); string result = service.ProcessPayment(amountToPay, transactionID); return result; }} public class CreditCardPayment : PaymentBase{ public string ID { get; set; } public string Vendor { get; set; } public override string Process(decimal amountToPay, int transactionID) { CreditCardPaymentService service = new CreditCardPaymentService(); string token = service.GetCreditCardTransactionToken(this.ID, this.Vendor); string result = service.ProcessPayment(amountToPay, transactionID, token); return result; }} public class PaymentFactory{ public static PaymentBase GetPaymentService(PaymentType paymentType) { switch (paymentType) { case PaymentType.Cash: return new CashPayment(); case PaymentType.CreditCard: return new CreditCardPayment(); default: throw new NotImplementedException("Payment type not implemented."); } }} public class PaymentService{ public bool ProcessPayment(PaymentType paymentType, decimal amount, int transactionID) { bool result = false; PaymentBase payment = PaymentFactory.GetPaymentService(paymentType); if ((payment as CreditCardPayment) != null) { ((CreditCardPayment)payment).ID = "Credit Card ID"; ((CreditCardPayment)payment).Vendor = "Credit Card Vendor"; } string response = payment.Process(amount, transactionID); if (response.Contains("Credit card payment success") || response.Contains("Cash payment success")) result = true; return result; }} The code would work, but it could have been written in a better way. Here are some of the areas that we can focus on to improve the code: In the PaymentFactory class, we check for the type of payment first; we are violating the LSP because that shows that the subclass is not substitutable for its base class. Also, when a new payment service needs to be implemented we also need to modify the PaymentService to set the values to the parameters of the new payment service if necessary and to check for the response of the new payment service which violates the Open-Closed Principle. public abstract class PaymentBase { public abstract bool Process(decimal amountToPay, int transactionID); } public class CreditCardPayment : PaymentBase { public CreditCardPayment(string id, string vendor) { this.ID = id; this.Vendor = vendor; } public string ID { get; set; } public string Vendor { get; set; } public override bool Process(decimal amountToPay, int transactionID) { CreditCardPaymentService service = new CreditCardPaymentService(); string token = service.GetCreditCardTransactionToken(this.ID, this.Vendor); string response = service.ProcessPayment(amountToPay, transactionID, token); bool result = response.Contains("Credit card payment success"); return result; } } public class PaymentFactory { public static PaymentBase GetPaymentService(PaymentType paymentType) { switch (paymentType) { case PaymentType.Cash: return new CashPayment(); case PaymentType.CreditCard: //// the parameters will actually come from an object or built from a helper method return new CreditCardPayment("The credit card id", "The credit card vendor"); default: throw new NotImplementedException("Payment type not implemented."); } } } public class PaymentService { public bool ProcessPayment(PaymentType paymentType, decimal amount, int transactionID) { PaymentBase payment = PaymentFactory.GetPaymentService(paymentType); bool result = payment.Process(amount, transactionID); return result; } } 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253 public abstract class PaymentBase{ public abstract bool Process(decimal amountToPay, int transactionID);} public class CreditCardPayment : PaymentBase{ public CreditCardPayment(string id, string vendor) { this.ID = id; this.Vendor = vendor; } public string ID { get; set; } public string Vendor { get; set; } public override bool Process(decimal amountToPay, int transactionID) { CreditCardPaymentService service = new CreditCardPaymentService(); string token = service.GetCreditCardTransactionToken(this.ID, this.Vendor); string response = service.ProcessPayment(amountToPay, transactionID, token); bool result = response.Contains("Credit card payment success"); return result; }} public class PaymentFactory{ public static PaymentBase GetPaymentService(PaymentType paymentType) { switch (paymentType) { case PaymentType.Cash: return new CashPayment(); case PaymentType.CreditCard: //// the parameters will actually come from an object or built from a helper method return new CreditCardPayment("The credit card id", "The credit card vendor"); default: throw new NotImplementedException("Payment type not implemented."); } }} public class PaymentService{ public bool ProcessPayment(PaymentType paymentType, decimal amount, int transactionID) { PaymentBase payment = PaymentFactory.GetPaymentService(paymentType); bool result = payment.Process(amount, transactionID); return result; }} Here are the changes that we made on the code: By changing the return type of the Process method from the response string to the boolean result in the PaymentBase abstract class, we now force each payment subclass to evaluate the response that will be returned as the result. Eliminating the need to modify the PaymentService whenever a new kind of payment service response needs to be evaluated. By adding a constructor to the PaymentBase subclass i.e. the CreditCardPayment class we can now set the required parameters from the PaymentFactory instead of the PaymentService. With this change the PaymentService class does not need to be modified whenever a new type of payment service needs to be implemented. With these changes, the PaymentService simply tells the factory to use a subclass of the PaymentBase. The PaymentService no longer needs to check what payment type it is going to process to set the initial parameter values and to evaluate the response. In return, the PaymentService would receive whether the processing of the payment is successful or not. Practicing the LSP enables us to extend a base class without changing any behavior from the existing classes. It makes our work easier and makes us more efficient as we will write less code that of course, leads to fewer mistakes.