LSP: Liskov Substitution Principle a.k.a Design By Protocol

Subclasses should behave nicely when used in place of their base class

Aaina jain
Swift India
5 min readFeb 4, 2019

--

In previous article, we discussed about OCP, it’s foundation for building maintainable and reusable code. The primary mechanisms behind OCP are abstraction and polymorphism, which can be achieved using protocols in swift.

The Liskov substitution principle (LSP) is a collection of guidelines for creating inheritance hierarchies or conform to protocol in which a consumer can reliably use any class or subclass without compromising the expected behavior.

— — — — — — — — — — — — — — — OR — — — — — — — — — — — — — — —

LSP recommends to model your classes in such way that any subclass/type conforming to that protocol, can be replaced with a subtype without altering the correctness of the program.

The LSP helps to enforce both the open/closed principle and the single responsibility principle.

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program.

— Barbara Liskov

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.

— Robert Martin

Liskov Substitution principle is also known as “Design by Contract” but I would rephrase it “Design by Protocol” in Swift.

In above diagram, Protocol LoginUseCase define method to be implemented by class. Client who consumes use case should be able to work with both DefaultLoginUseCase , MockLoginUseCase without any change in code.

LSP rules

There are several “rules” that must be followed for LSP compliance.

  • Contract rules
  • Variance rules

Contract rules

These rules relate to the contract of the supertype and the restrictions placed on the contracts that can be added to the subtype or when one class conformed to protocol is getting replace with class of same type.

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype — conditions that must remain true — must be preserved in a subtype.

To understand this, I am going to create shipping cost class to calculate shipping cost.

Contracts:

It is often said that developer should program to protocols, and a related idiom is to program to a contract.

**Preconditions

Preconditions are defined as all the conditions necessary for a method to run reliably and without fault. By default protocols doesn’t force such precondition fulfilled by any of the implementers of their methods. Preconditions should be written on method arguments so client will be aware in case of method throw any error.

Preconditions cannot be strengthened:

If subclass is replaced with superclass for existing method and preconditions are tightened then existing functionality will break as method will return without existing.

Let’s relate this phrase `Preconditions cannot be strengthened` to previous example: If in future you wants to increase business internationally then you will need to modify shipping cost calculation strategy, as now products are getting shipped overseas. If we create `WorldWideShippingStrategy` class then it should not break existing code. If we replace existing strategy with this strategy then existing code is going to break for sure.

Here comes the point to use correct strategy type for calculating shipping cost:

**PostConditions

Postconditions check whether an object is being left in a valid state before a method is returned. Whenever state is mutated in a method, there is a possibility for the state to be invalid due to logic errors.

Postconditions cannot be weakened:

When applying postconditions to subclasses, the opposite rule applies. Instead of not being able to strengthen postconditions, you cannot weaken them. For all the Liskov substitution rules relating to contracts, the reason that you cannot weaken postconditions is because existing clients might break when presented with the new subclass. Theoretically, if you comply with the LSP, any subclass you create should be usable by all existing clients without causing them to fail in unexpected ways.

**Variants

Let’s understand the diamond of variants:

  1. Contravariant

2. Covariant

3. Invariant

covariance is a relationship where subtypes go with each other, and contravariance is a relationship where subtypes go against each other.

When we talk about covariance and contravariance then subtypes and supertypes comes first in discussion.

Subtypes can substitute for Supertypes.

Let’s understand why it doesn’t work in opposite way.

Now on assigning instance of world strategy to base strategy, it produces compile time error:

This error makes sense as WorldShippingStrategy expects WorldShippingStrategy instance to be there.

Covariance: Covariance is when Function return values can changed to subtypes, moving down the hierarchy.

Contravariance: Contravariance is when function parameters can be changed to supertypes, moving up the hierarchy.

On modifying parameter type to subtype down the hierarchy we get Method doesn't override error

Actual implementation should be

Invariance: Invariance is when neither supertypes nor subtypes are accepted. Swift Generics are example of invariant.

For two generic types to be compatible in Swift, they must have identical generic parameters. Subtypes and supertypes are never allowed.

Invariants must be maintained

Whenever a new subclass is created, it must continue to honor all the data invariants that were part of the base class. The violation of this principle is easy to introduce because subclasses have a lot of freedom to introduce new ways of changing previously private data.

Conclusion

LSP requires a knowledge of both contracts and variance to build rules that guide toward writing more adaptive code.

By default, protocols do not convey rules for preconditions or postconditions to consumer. Creating guard clauses that halt the application at run time further narrows the allowed range of valid values for parameters. The LSP provides guidelines specifying that each subclass in a class hierarchy must not strengthen preconditions or weaken postconditions.

Similarly, the LSP suggests rules for variance in subtypes. There should be contravariance of method arguments in subtypes and covariance of return values in subtypes.

If the LSP is violated with respect to these rules, it becomes harder for consumer to treat all types in a class hierarchy the same. Ideally, clients would be able to hold a reference to a base type or protocol and not alter its own behavior depending on the concrete subclass that it is using at run time. Any violation of the LSP should be considered technical debt.

Thanks for reading article.

You can catch me at:

Linkedin: Aaina Jain

Twitter: __aainajain

--

--