Tech Point Fundamentals

Sunday, January 9, 2022

Liskov Substitution Principle of SOLID | LSP

Liskov Substitution Principle of SOLID | LSP

Liskov-Substitution-Principle

SOLID Principles are one of the mandatory questions in each and every interview. This is the fourth part of the SOLID Design Principle. In this part, we will walk through the third SOLID Design Principle i.e Liskov Substitution Principle (LSP) in detail with live real examples. 


Please read the previous parts over here below links:


SOLID Design Principles

Single Responsibility Principle

Open Closed Principle


Watch our videos here




Introduction


In the SOLID acronym, the third letter "L" represents the "Liskov Substitution Principle"The Liskov Substitution Principle focuses on the behavioral relationship between parent class and child class object.


The primary mechanisms behind the Open-Closed Principle are abstraction and polymorphism and one of the key mechanisms that support both abstraction and polymorphism is inheritance. So Liskov Substitution Principle extends the Open-Closed Principle by focusing on the behavior of a superclass and its subtypes. 


The Liskov Substitution Principle is applicable when there’s a supertype-subtype inheritance relationship by either extending a class or implementing an interface. 


The Liskov Substitution Principle states that subclasses should be substitutable for their base classes.


At first glance, this principle looks pretty easy to understand. At second glance it seems redundant with the OOPS concept of polymorphism. But it is not as simple as that.


This principle is in fact a caveat for developers that polymorphism is both powerful and tricky. In reality, the usage of polymorphism often leads to a dead-end situation, it must be wisely used.




Recap from Previous Articles


There are three things basically on which this principle is based on i.e inheritance, polymorphism, and subtyping. Let's understand them before jumping on the LSP.



Inheritance


In object-oriented programming, inheritance is the mechanism of basing an object or class upon another object or class, retaining similar implementation. So the ability of a class to derive properties and characteristics from another class is called Inheritance.


When we use inheritance we assume that the child class inherits everything that the superclass has. The child class extends the behavior but never narrows it down. Therefore, when a class does not obey this LSP principle, it leads to some nasty bugs that are hard to detect. i.e. if we substitute a superclass object reference with an object of any of its subclasses, the program should not break.


Classical Inheritance works on "IS-A Relationship" while the LSP works on "IS-SubstituteFor Relationship".


If the child classes start having properties and methods that don’t make sense anymore for them, even they come from a parent class, it is time to re-think the inheritance to be sure that should still exist in order to follow the LSP.





Polymorphism


In programming languages and type theory, polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types. 


In OOPS objects can behave in one way in a certain situation, and in another way in some other situation. In object-oriented programming, this is called context-dependent behaviorSo Polymorphism is the ability of an object to have many forms


In OOPS, at run time, objects of a derived class may be treated as objects of a base class in places. When this polymorphism occurs, the object's declared type is no longer identical to its run-time type.


In OOPS, base classes may define and implement virtual methods, and derived classes can override them to provide their own definition and implementation. At run-time, when client code calls that method, the CLR looks up the run-time type of the object and invokes that override of the virtual method. So in your source code, you can call a method on a base class, and cause a derived class's version of the method to be executed.


Please read more about polymorphism here:


Dynamic Polymorphism and Method Overriding

Static Polymorphism and Method Overloading

Method Overloading and Method Overloading Dilemma





Subtyping and Subtype Polymorphism 


Subtyping is a concept that is not identical to polymorphism. However, the two terms are so tightly connected and fused together in common languages like C# or Java that the difference between them is practically non-existent. 


In programming language theory, subtyping (subtype polymorphism or inclusion polymorphism) is a form of type polymorphism in which a subtype is a datatype that is related to another datatype (supertype) by some notion of substitutability, meaning that program elements (typically subroutines or functions) written to operate on elements of the supertype can also operate on elements of the subtype. i.e.


"If S is a subtype of T, the subtyping relation is often written S <: T, to mean that any term of type S can be safely used in a context where a term of type T is expected."





Liskov Substitution Principle (LSP)


Liskov's Substitution Principle is the third SOLID Design Principle. This principle extends the Open-Closed Principle by focusing on the behavior of a SuperClass and its SubTypes.  


This principle is based on the "Design by Contract (DbC)" approach of software designing. It prescribes that software designers should define formal, precise, and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions, and invariants. These specifications are referred to as "contracts". The DbC approach assumes all client components that invoke an operation on a server component will meet the preconditions specified as required for that operation.


The Liskov Substitution Principle is a particular definition of a strong Behavioral Subtyping Relation, that was initially introduced by Barbara Liskov in the year 1987 in a conference keynote titled "Data Abstraction and Hierarchy"


The Liskov Substitution Principle states that subclasses should be substitutable for their base classes.




Definition of LSP


Substitutability is a principle in Object-Oriented Programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program. i.e an object of type T may be substituted with any object of a subtype S.


According to Barbara Liskov, this principle states that:


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.


In other words:


"If S is a subtype of type T, then objects of type T may be replaced with objects of type S without any breaking change."


This means that every subclass or derived class should be substitutable for their base or parent class.


OR


Since Liskov's notion of a behavioral subtype defines a notion of substitutability for objects:


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


The LSP states that in an Object-Oriented Program if we substitute a superclass object reference with an object of any of its subclasses, the program should not breakThat requires the objects of your subclasses to behave in the same way as the objects of your superclass. 


If substituting a superclass object with a subclass object changes the program behavior in unexpected ways, the LSP is violated. LSP violation is a design smell. If client code cannot substitute a superclass reference with a subclass object freely, it would be forced to do a run-time-type identification by instanceof checks and specially handle some subclasses. If this kind of conditional code is spread across the codebase, it will be difficult to maintain.





Explanation


This means that every subclass or derived class should be substitutable for their base or parent class without breaking the application.


For example, if class S is a subclass of class B, we should be able to pass an object of class S (child class) to any method that expects an object of class B (base class) and the method should not give any weird output in that case.


We can think of the methods defined in the supertype as defining a contract and every subtype is expected to stick to this contract. If a subclass does not adhere to the superclass’s contract, it’s violating the LSP. 


A class’s contract tells its clients what to expect. If a subclass extends or overrides the behavior of the superclass in unintended ways, it would break the clients. 




LSP Implementation Guideline


You can achieve the LSP by following a few rules, which are pretty similar to the design by contract concept defined by Bertrand Meyer.  There are several rules that LSP actually enforces. The two main categories of these rules are variance rules and contract rules.




A. Variance Rule of LSP (Inheritance Rule)


In C# variance is a reference transition between related types through inheritance. Generally, variance is a term applied to the expected behavior of subtypes in a class hierarchy containing complex types. There are two types of variance called Covariant and Contravariant


Behavioral Subtyping is a stronger notion than typical subtyping of functions defined in type theory, which relies only on the contravariance of parameter types and covariance of the return type. Please read more about Covariant vs Contravariant here.


Behavioral subtyping is undecidable in general. However, Liskov Substitution Principle imposes some standard requirements on signatures that have been adopted in newer object-oriented programming languages(class level rather than type):


i) Contravariance of method parameter types in the subtype. This means there must be a contravariance of method arguments in the subtype. So the subtype reverses the ordering of types. A method of a subclass can accept a parent type as a method parameter (Contravariance)


The “idea” of this is that a subclass can override a method and accept a more general parameter, but not the opposite. The overriding method of a subclass must need to accept the same input parameter values as the method of the superclass.


ii) Covariance of method return types in the subtype. This means there must be covariance of return types from the method in the subtype. So the subtype keeps types in the same order, from specific to most generic. A method of a subclass can return a subtype as a parameter (Covariance). Liskov allows it to be “more” specific than the original return type.


So the covariance rules also apply to the return value of the method. The return value of a method of the subclass needs to comply with the same rules as the return value of the method of the superclass. 


iii) New exceptions cannot be thrown by the methods in the subtype, except if they are subtypes of exceptions thrown by the methods of the supertype.


Please read more about the Covariance and Contravariance here.





B. Contract Rule of LSP (Method Behavior)


In addition to the signature requirements, the subtype must meet a number of behavioral conditions defined in the Design by Contract methodology.  


When creating a class, the contract basically states in formal terms how to use the object. You (client or subclass) expect the name of the methods and the parameters data types and then what data type to expect as a return value. LSP places below restrictions on contract rules:


i) Preconditions cannot be strengthened in the subtype. Preconditions are the things required by the method to run reliably. Typically, guard clauses are used to enforce that parameters are of the correct values. This would require the class to get instantiated correctly and required parameters are passed to the method.


ii) Postconditions cannot be weakened in the subtype. Postconditions verify that the object is left in a reliable state when the method returns. The guard clauses are again used to enforce postconditions.


iii) Invariants must be preserved in the subtype. In programming, invariants are things that must remain true during the lifetime of the object once object construction is finished. This could be a field value that can be set in the constructor and is assumed not to change. The subtype should not change these types of fields to any invalid values. Read-only fields guarantee this rule is followed.


iv) Subclass can implement only less restrictive validation rules. You are not allowed to enforce stricter ones in your subclass implementation than the base class. Otherwise, any code that calls this method on an object of the superclass might cause an exception, if it gets called with an object of the subclass.


v) Subclass should implement all the methods of the base class. This means there should be no methods that throw NotImplementedException.




How can a method in a subclass break a superclass method’s contract? 

How the LSP is violated by someone, everybody use the inheritance and subtyping?


Well, there are different possible ways to violate the LSP:


1. Returning an object that’s incompatible with the object returned by the superclass method.

2. Throwing a new exception that’s not thrown or understandable by the superclass method.

3. Changing the semantics or introducing side effects that are not part of the superclass’s contract.


Unfortunately, there is no easy way to enforce this principle. The compiler only checks the structural rules defined by the C# or Java, it can’t enforce a specific behavior. You need to implement your own checks to ensure that your code follows the Liskov Substitution Principle.  





LSP Violation Real Example in the .Net Framework


Can all collections really be modified? What happens if anyone tries to add or remove an element from an array?


One dreaded LSP violation is .NET System. Array implementing the ICollection<T> and IList<T>  interface.  


icollection-and-ilist-interface


Since Array is inherited from ICollection and IList, it has to implement the ICollection<T>.Add() method. 


array-implementing-icollection




But calling this method on an array throws at runtime a NotSupportedException. The C# compiler doesn’t even warn on such a simple erroneous program because there is no way to check for the LSP violation.


lsp-violation-in-dotnet

We know that we should ensure that ICollection<T>.IsReadOnly is false before modifying a collection through a reference of IList<T> or an ICollection<T>, however, this is an error-prone design that is only due to the LSP violation.


Avoiding-LSP-error




Though IReadOnlyCollection<T> has been introduced with .NET V4.5 in 2012, the original initial design cannot be changed. Which avoids the above runtime error.


ireadonlycollection




Rectangle-Square Paradigm or Circle-Elipse Problem of LSP


A very common and famous example to explain and understand the Liskov Substitution Principle in the software industry is the Rectangle-Square Paradigm or Circle-Elipse Problem


The problem is, we need to substitute square for rectangle, or vice-versa, with only changing the class that gets instantiated. But, when creating a square, if we pass two parameters for height and width, which parameter is the correct one? We could require that you pass the same number twice, but that seems rather useless as only one is needed. 


Is a square really a rectangle? What happens if anyone changes the width of a square?


A square is a rectangle, isn’t it?  Think twice before applying the ISA trick here.



In the above example, we know that "rect" is a rectangle, so we can set both height and width.  But how the behavior is being changed for square and also different results on swapping of the parameter.




In the above example, the Square class changes the behavior of the Rectangle class by assigning the width to Rectangle height. So Square class violated the LSP.


If the developer overrides the Height and Width in the Circle class the client will get a runtime StackOverflowException for Square.


Clearly, this is a wrong usage of the ISA principle which is a variation of LSP. Yes, a square is a rectangle but the client doesn’t expect that when modifying the height of the rectangle, the width gets modified also. We know that the inheritance uses the famous "IS-A" relationship. But is it always true?


Example



The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class.




So according to the LSP, if we assign the EvenNumbersArrayCalculator object to the base class ArrayCalculator, nothing should be changed. But here when we are doing so we are getting the wrong output, which means the behavior is getting changed.


"The sum of all the even numbers: 55"


This is wrong because the sum of even numbers should be 30. So let's fix this. One solution you might think is that if we can override the Calculate() method in the derived class, the problem will be resolved. So let's do that.


 

public class ArrayCalculator
{
	protected readonly int[] _array;

	public ArrayCalculator(int[] numArray)
	{
		_array = numArray;
	}

	public virtual int Calculate() => _array.Sum();
}

public class EvenNumbersArrayCalculator : ArrayCalculator
{
   public EvenNumbersArrayCalculator(int[] numArray) : base(numArray) { }
   
   public override int Calculate() => _array.Where(x => x % 2 == 0).Sum();
}	    



So now you got the expected output by the small code modification:


The sum of all the numbers: 55
The sum of all the even numbers: 30
The sum of all the even numbers: 30


What is happening here basically, since we are overriding the Calculate() method in the derived class, so at runtime, the derived class Calculate() method is being called instead of the parent class i.e due to Run-Time Polymorphism.




But still, there is a problem here. The behavior of our derived class has changed (at run-time) and it can’t substitute the base class exactly. So it is still violating the LSP. Therefore we have to upgrade the solution with some abstract contract.



We have the desired result again. But now, we can see that we can store any subtype reference into a base-type variable and the behavior won’t change which is the ultimate goal of LSP.




Identifying the LSP Violation


There are some common ways to identify violations of LSP principles as follows:


i)  A non-implemented method in the subclass. As you have seen in the case of Array which does not implement the ICollection.Add() method.

ii) Subclass method overrides the base class method to give it a new meaning. If you see in the ArrayCalculator example.

iii) Deriving the subclass only based on the "IS-A" relationship. If you see the rectangle-square paradigm example.

iv) Use of Run-Time Type Information (RTTI) checking to select a method based on the type of an object.




Conclusion


Every subclass should be substitutable for its base class without breaking the application. So in the inheritance or subtyping if this LSP must be followed to avoid any un-predictable issue in the application.


No comments:

Post a Comment

Please do not enter any HTML. JavaScript or spam link in the comment box.