Can You Explain Liskov Substitution Principle with a Good C# Example

Can you explain Liskov Substitution Principle with a good C# example?

(This answer has been rewritten 2013-05-13, read the discussion in the bottom of the comments)

LSP is about following the contract of the base class.

You can for instance not throw new exceptions in the sub classes as the one using the base class would not expect that. Same goes for if the base class throws ArgumentNullException if an argument is missing and the sub class allows the argument to be null, also a LSP violation.

Here is an example of a class structure which violates LSP:

public interface IDuck
{
void Swim();
// contract says that IsSwimming should be true if Swim has been called.
bool IsSwimming { get; }
}

public class OrganicDuck : IDuck
{
public void Swim()
{
//do something to swim
}

bool IsSwimming { get { /* return if the duck is swimming */ } }
}

public class ElectricDuck : IDuck
{
bool _isSwimming;

public void Swim()
{
if (!IsTurnedOn)
return;

_isSwimming = true;
//swim logic
}

bool IsSwimming { get { return _isSwimming; } }
}

And the calling code

void MakeDuckSwim(IDuck duck)
{
duck.Swim();
}

As you can see, there are two examples of ducks. One organic duck and one electric duck. The electric duck can only swim if it's turned on. This breaks the LSP principle since it must be turned on to be able to swim as the IsSwimming (which also is part of the contract) won't be set as in the base class.

You can of course solve it by doing something like this

void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}

But that would break Open/Closed principle and has to be implemented everywhere (and thefore still generates unstable code).

The proper solution would be to automatically turn on the duck in the Swim method and by doing so make the electric duck behave exactly as defined by the IDuck interface

Update

Someone added a comment and removed it. It had a valid point that I'd like to address:

The solution with turning on the duck inside the Swim method can have side effects when working with the actual implementation (ElectricDuck). But that can be solved by using a explicit interface implementation. imho it's more likely that you get problems by NOT turning it on in Swim since it's expected that it will swim when using the IDuck interface

Update 2

Rephrased some parts to make it more clear.

What is an example of the Liskov Substitution Principle?

A great example illustrating LSP (given by Uncle Bob in a podcast I heard recently) was how sometimes something that sounds right in natural language doesn't quite work in code.

In mathematics, a Square is a Rectangle. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square derive from Rectangle, then a Square should be usable anywhere you expect a Rectangle. This makes for some strange behavior.

Imagine you had SetWidth and SetHeight methods on your Rectangle base class; this seems perfectly logical. However if your Rectangle reference pointed to a Square, then SetWidth and SetHeight doesn't make sense because setting one would change the other to match it. In this case Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.

Sample Image

Y'all should check out the other priceless SOLID Principles Explained With Motivational Posters.

Understanding the Liskov substitution principle

To violate the LSP you need a client class which makes some assumption on the class interface. The assumption must not exactly be expressed in a formal way, sometimes it just comes from a context of the usage.

Supose you have an enumerable class which allows you to add elements. The assumption of the client can be for example that if it adds N elements then exactly N elements can be read from the collection. Then you derive a set from your collection which removes duplicate elements upon adding. The client expectation is wrong now, as even if N elements are added, sometimes LESS THAN N elements can be read.

To me then the violation of the LSP needs a context which defines some expectations. Since there are no expectations in your code, the LSP is not violated.

This need of a context mean also that two classes can violate the LSP for one client context while the same classes possibly do not violate the LSP in other contexts.

How to comply with Liskov's Substitution Principle (LSP) and still benefit from polymorphism?

LSP says that you must be able to use a derived class in the same way you use it's superclass: "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program". A classic inheritance that breaks that rule is deriving Square class from Rectangle class since the former must have Height = Width, while the latter can have Height != Width.

public class Rectangle
{
public virtual Int32 Height { get; set; }
public virtual Int32 Width { get; set; }
}

public class Square : Rectangle
{
public override Int32 Height
{
get { return base.Height; }
set { SetDimensions(value); }
}

public override Int32 Width
{
get { return base.Width; }
set { SetDimensions(value); }
}

private void SetDimensions(Int32 value)
{
base.Height = value;
base.Width = value;
}
}

In this case, the behavior of Width and Height properties changed and this is a violation of that rule. Let's take the output to see WHY the behavior changed:

private static void Main()
{
Rectangle rectangle = new Square();
rectangle.Height = 2;
rectangle.Width = 3;

Console.WriteLine("{0} x {1}", rectangle.Width, rectangle.Height);
}

// Output: 3 x 2

How Liskov substitution principle is different from normal inheritance?

Here's a common definition:

https://www.tomdalling.com/blog/software-design/solid-class-design-the-liskov-substitution-principle/

The Liskov Substitution Principle (LSP): functions that use pointers
to base classes must be able to use objects of derived classes without
knowing it.

Here's a more rigorous explanation:

https://en.wikipedia.org/wiki/Liskov_substitution_principle

..the Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping, that was initially introduced by Barbara Liskov in a 1987 conference keynote address titled Data abstraction and hierarchy. It is a semantic rather than merely syntactic relation, because it intends to guarantee semantic interoperability of types in a hierarchy, object types in particular. Barbara Liskov and Jeannette Wing described the principle succinctly in a 1994 paper as follows:

Subtype Requirement:

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.

Liskov's principle imposes some standard requirements on signatures that have been adopted in newer object-oriented programming languages (usually at the level of classes rather than types; see nominal vs. structural subtyping for the distinction):

  • Contravariance of method arguments in the subtype.
  • Covariance of return types in the subtype.
  • No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.

In addition to the signature requirements, the subtype must meet a number of behavioural conditions. These are detailed in a terminology resembling that of design by contract methodology, leading to some restrictions on how contracts can interact with inheritance:

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.
  • History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Because subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this.

Q: Does your example conform?

A: I don't think so. Because A.MethodA() is semantically "different" from B.MethodA(). ClassB doesn't pass The Duck Test.

I propose this counter-example:

public class ClassA {
public virtual void MethodA() {
Console.WriteLine("ClassA.MethodA");
}

public virtual void MethodB(){
Console.WriteLine("ClassA.MethodB");
}
}

public class ClassC: ClassA {
public void MethodC() {
Console.WriteLine("ClassC.MethodC");
}
}

This is also an excellent example of why LSP is NOT "the same as" inheritance.

Liskov substitution principle and Streams

The Can... methods mean that Stream doesn't break LSP. Stream provides the capability to read, write and seek, but no guarantee that any implementing class will honour it. The Can... methods make this an explicit feature of the Stream contract - derived classes must implement them to allow client code to check whether a derived class implements certain behaviour before a call is made. So, any code that attempts to write to a Stream should check CanWrite before calling Write, for example, and this can be done with any correctly-implemented derivative of Stream. Ergo, they're interchangeable, as LSP requires.

I think it's certainly true that adding methods to flag whether or not derived classes implement a particular feature could be abused - if a team were undisciplined, they may end up with a very broad, bloated interface that breaks ISP. I think that Stream and IList<T> are well-designed in this respect - they don't break LSP, and they define a narrow enough contract of closely-related behaviours to stay within ISP. Clearly, their design has been considered.

I think that in the case of Square inheriting from Rectangle, you could certainly add DoesHeightAffectsWidth and DoesWidthAffectsHeight to fix the issue, but the team must decide whether that's acceptable, or whether the addition of those methods breaks ISP. Is the addition of AreAllInternalAnglesEqual to support trapezoids too far? To a certain extent, it's up to the engineers writing the code.

Liskov substitution principle and Interface

I can see why you'd think so. There's a function that expects a collection, and it expects it to be modifiable. Passing an array will make it fail, so clearly you can't substitute the interface with this particular implementation, right?

Is it a problem? Maybe. It depends on how often you expect ideals to hold. Are you going to use an array instead of a collection by accident and then be surprised ten years later that it breaks down? Not really. The type system .NET applications use isn't perfect - it doesn't tell you this particular ICollection<T> usage requires the collection to be modifiable.

Would .NET be better off if arrays didn't pretend to implement ICollection<T> (or IEnumerable<T>, which they also don't "really" implement)? I don't think so. Is there a way to keep the convenience of arrays "being" ICollection<T> that would also avoid the same LSP violation? Nope. The underlying array would still be fixed-length - at best, you'd be violating more useful principles instead (like the fact that reference types are not expected to have referential transparency).

But wait! Let's look at the actual contract of ICollection<T>.Add. Does it allow for a NotSupportedException to be thrown? Oh yes - quoting MSDN:

[NotSupportedException is thrown if ...] The ICollection is read-only.

And arrays do return true when you query IsReadOnly. The contract is upheld.

If you consider Stream not to break LSP because of CanWrite, you must consider arrays to be valid collections, since they have IsReadOnly, and it is true. If a function accepts a read-only collection and tries adding to it, it's an error in the function. There's no way to specify this explicitly in C#/.NET, so you have to rely on other parts of the contract than just types - e.g. the documentation for the function should specify that a NotSupportedException (or ArgumentException or whatever) is thrown for a collection that is readonly. A good implementation would do this test right at the start of the function.

One important thing to note is that types aren't quite as constrained in C# as in the type theory where LSP is defined. For example, you can write a function like this in C#:

bool IsFrob(object bobicator)
{
return ((Bob)bobicator).IsFrob;
}

Can bobicator be substituted with any supertype of object? Clearly not. But it just as clearly isn't a problem of the poor Frobinate type - it's an error in the IsFrob function. In practice, a lot of code in C# (and most other languages) only works with objects far more constrained than would be indicated by the type in the method signature.

An object only violates the LSP if it violates the contract of its supertype. It cannot be responsible for other code violationg LSP. And often you'll find it quite pragmatic to make code that doesn't perfectly hold up under LSP - engineering is, and always has been, about trade-offs. Weigh the costs carefuly.



Related Topics



Leave a reply



Submit