In the last post we saw how OCP facilitates extensibility and reusability . OCP is the foundation on which several patterns have been written. The Strategy pattern is a great example of OCP where subclasses are written based on different algorithms. The manifestation of OCP happens in the third SOLID principle , the ‘L’ as we know , Liskov Substituion principle – perhaps the more involved and less understood principles in SOLID .
We saw how SRP can lead to OCP – LSP takes OCP and establishes clear rules that will ensure polymorphism is accomplished correctly. LSP attempts to achieve what we call subtype polymorphism through it’s rules. In short we can represent this in pseudocode – a client call to a subtype method call , through a base class interface:
public class Supertype{ public virtual outparam SomeMethod( inparam ); } public class Subtype : Supertype { public override outparam SomeMethod( inparam ); } //Client call: Supertype baseType = new Subtype(); outparam = baseType.SomeMethod(inparam);
Liskov Substitution statement translates to : Subtype(derived type) must be behaviorally equal to their base types. They must be usable through the base type interface without the need for the user to know the difference.
LSP needs to be understood from a client’s perspective . Client here means : Calling programs , Users of your interfaces and abstract classes within the organization or outside the organization. A Client perceives the behavior of a class through methods of a class : arguments passed , value returned and any state changes after the method execution. So basically to be able to use a subclass / subtype in place of a superclass/supertype the sub type successfully needs to preserve argument requirements and return expectations by the client. A client code that gets written keeping in mind the Supertype to call it’s method should not change or break when replaced with subclass method calls . Compilers do enforce Signature compliance when methods are overridden from abstract classes . However internally within the implementation if arguments or return values were treated in a manner that could break the client code , the code is in violation of LSP because essentially the contract with the client was broken . Compilers typically will not catch these violations.
It is the programmers responsibility to ensure LSP compliance for the most part. In order for the internal subtype behavior to keep in consistency with supertype behavior , these principles were formed.
There is a very subtle and interesting nuance that one needs to understand here. You could very well write a ‘Superclass’ , and then write a ‘Subclass’ – override the Superclass method to give a specific implementation. Use the subclass in your programs to execute the subclass specific method. You may never see anything wrong up until you expect the subclass to be a ‘subtype’ of the Superclass which is a Supertype. It’s when a ‘subclass’ is expected to become a ‘subtype’ is when LSP comes into play.
A lot of blogs have been written to explain these rules. I have cited at the end some good ones in order to understand them with code examples : instead in this blog let’s try and understand some of the confusing rules that mostly I have seen people asking questions about . Let’s just list all of them first :
- Contravariance of method arguments in the subtype.
- Covariance of return types in the subtype.
- No new exceptions should be thrown, unless the exceptions are subtypes of exceptions thrown by the parent.
- Preconditions cannot be strengthened in the subtype
- Postconditions cannot be weakened in the subtype
- Invariants must be preserved in the subtype.
- History Constraint – the subtype must not be mutable in a way the supertype wasn’t.
Now, let’s take just the two which seem to confuse most people :
- Preconditions cannot be strengthened in the subtype.
- Postconditions cannot be weakened in the subtype.
Preconditions apply to arguments which will be used as part of the implmentation. Postconditions mostly relate to return values or the state after the implementation is executed. Preconditions are requirements on users of the functions, while postconditions are requirements on the functions themselves. Preconditions get executed before the actual implementation is executed , whereas postconditions are executed after.
Preconditions cannot be strengthened in the subtype.
Wikipedia explains this as :
In the presence of inheritance, the routines inherited by descendant classes (subclasses) do so with their preconditions in force. This means that any implementations or redefinitions of inherited routines also have to be written to comply with their inherited contract. Preconditions can be modified in redefined routines, but they may only be weakened. That is, the redefined routine may lessen the obligation of the client, but not increase it.
What is the obligation of the client ? The arguments that need to be passed , is the obligation of the client. If the preconditions are set in such a way in the subclass method that the choice of the arguments which can be passed from the client is lesser or restricted then you actually strengthened the precondition . This increases the obligation of the client.
Let’s understand this with an example. We will modify the IFormatter interface from the last post to an abstract base class with some implementation.
abstract class Formatter{ public virtual string Format( String message) { if ( String.IsNullOrEmpty( message ) ) throw new Exception (); // do formatting } } // strengthened precondition public class MobileFormatter : Formatter{ public override string Format( String message) { if ( String.IsNullOrEmpty( message ) || message.Length > 250 ) throw new Exception (); // do formatting } } // weakened precondition public class MobileFormatter : Formatter { public override string Format(String message) { if ( message == null ) throw new Exception (); // do formatting } }
As we see above the MobileFormatter placed more restriction on the arguments in the strengthened precondition – this will force the client to change their code to accommodate for this if they want to avoid getting an exception. So behaviorally the base and subtype are different.
In the weakened precondition what happened is that the client now does not need to accommodate the code for MobileFormatter, the argument that gets passed to MobileFormatter , works for base Formatter as well because the validation in Formatter is stronger or the validation in MobileFormatter is weaker.
Postconditions cannot be weakened in the subtype.
Wikipedia explains this as :
In the presence of inheritance, the routines inherited by descendant classes (subclasses) do so with their contracts, that is their preconditions and postconditions, in force. This means that any implementations or redefinitions of inherited routines also have to be written to comply with their inherited contract. Postconditions can be modified in redefined routines, but they may only be strengthened. That is, the redefined routine may increase the benefits it provides to the client, but may not decrease those benefits.
Let’s understand this with a code example:
abstract class Formatter { public virtual string Format(String message) { // do formatting return message.Trim(); } } // weakened postcondition public class MobileFormatter : Formatter{ public override string Format( String message) { //do formatting return message; } } // strengthened postcondition public class MobileFormatter : Formatter { public override string Format(String message) { //do formatting return message.Trim().PadLeft(5); } }
What we see above in the MobileFormatter is that the postcondition got weakened by removing the Trim method. This provides the client less than what was provided in terms of the result .
Then to correct it , we strengthened the postcondition by adding left padding. This does not require the client to change any code , however the code gets the extra benefit of padding. The above example is rather crude but serves the purpose of explaining .
There is a very interesting pattern called Template pattern accomplishes LSP via template methods written inside a base class which are overridden in derived classes. For now , this is enough to contemplate about – more in the next blog .
Here are some really good blogs on Liskov that discuss other rules as well:
http://msdn.microsoft.com/en-us/magazine/hh288081.aspx
Until then happy programming !