Question

Sorry for the unspecific title but I can't find a concise way to state my issue.

I am reading the book Clean Architecture, by Robert C. Martin. It has a short chapter about the LSP with a couple examples, but I am unable to understand why the first example does respect the LSP.

The principle

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

The first example

Imagine that we have a class named License, as shown in Figure 9.1. This class has a method named calcFee(), which is called by the Billing application. There are two “subtypes” of License: PersonalLicense and BusinessLicense. They use different algorithms to calculate the license fee. This design conforms to the LSP because the behavior of the Billing application does not depend, in any way, on which of the two subtypes it uses.

The second example

The canonical example of a violation of the LSP is the famed (or infamous, depending on your perspective) square/rectangle problem (Figure 9.2). In this example, Square is not a proper subtype of Rectangle because the height and width of the Rectangle are independently mutable; in contrast, the height and width of the Square must change together. Since the User believes it is communicating with a Rectangle, it could easily get confused. The following code shows why:

Rectangle r = ...
r.setW(5);
r.setH(2);
assert(r.area() == 10);

If the ... code produced a Square, then the assertion would fail.

The issue

I understand the second example, and why it doesn't respect the LSP. What I don't get is why the first example is considered to respect it. As far as I can tell, the Billing application could print the result of calculateFee, which would give a different result depending of the License used. How is that not a different behavior?

Was it helpful?

Solution

While your understanding of the application of LSP in these examples is not fully correct, I agree that the Square/Rectangle problem is a flawed example.

The problem here is that there is a lot of implicit expectation of what a Rectangle, width, height and surface area is. These concepts are strongly defined in an external resource (geometry) but they are not clear from code.

I think part of the issue here is that programming used to be a scientific field, where experts in the field were all engineers with strong mathematical backgrounds, to whom mathematical constructions were very much considered as everyday concepts that have obvious implementations that need to further elaboration. Secondly, programming back then was mugh more oriented on calculations (i.e. performing operations) instead of data storage (i.e. storing and serving data without significantly altering it).

In such a scenario, I can understand that Liskov, who coined the example in 1994 (according to Wikipedia) based on a principle dating from 1988, used the square/rectangle example as a simple case.

Technically speaking, the Square/Rectangle problem violates SRP. It is the Rectangle's responsibility to figure out its surface area. The external caller cannot claim "but this is not the correct surface area!" when it gets a different value, since the external caller shouldn't even be aware of how the surface area is calculated.
The fact that the external caller can say that the received value is incorrect proves that the external caller has its own (independent) method of calculating the surface area of a rectangle, which violates SRP/DRY.

But the context in which this is an "obvious" example is no longer the case. Software development has shifted focus away from the mathematical, relying more on premade libraries. Developers nowadays will most commonly use the existing libraries rather than rewrite/append them, which means that they focus on data storage much more than calculation.

Developers nowadays often deal with much more abstract objects where there is no implicit expecation of behavior. For example:

public class CarrotSauce
{
    public int Batman { get; set; }
    public int ClintEastwood { get; set; }

    public virtual int GetLove()
    {
        return Batman * ClintEastwood;
    }
}

You may have noticed that this is a refactored version of Rectangle. However, because I've removed the context, no one would call this a violation of LSP anymore. Because I have removed the mathematical context which brought with it a lot of implicit expectations as to what you can do. If I now do this:

public class TheCommonCold : CarrotSauce
{
    public override int GetLove()
    {
        return (Batman + 3) * ClintEastwood;
    }
}

You wouldn't automatically identify that as a breach of the CarrotSauce contract, because there is no implicit expectation (by an external caller) of how setting the values of Batman and ClintEastwood will influence the return value of GetLove().


The first example

The first example retains the data contract. Notice how an external caller would handle the fees:

public void WriteFeesToConsole(License license)
{
    var fees = license.calcFees();

    Console.WriteLine($"The fees are ${fees}.");
}

This works for both the PersonalLicense and the BusinessLicense.

When talking about LSP, when we say "the contract is not broken", what we really mean is "an external caller can handle every subtype the same way as it can handle the base type".

It's very hard to give a simple example of how to violate LSP in this example. Stretching it a bit, consider if a LicenseInEuros : License object existed, which would return a value in euros instead of dollars.

Our method would have to account for this different subtype:

public void WriteFeesToConsole(License license)
{
    var fees = license.calcFees();

    if(license is LicenseInEuros)
        Console.WriteLine($"The fees are €{fees}.");
    else
        Console.WriteLine($"The fees are ${fees}.");
}

if(license is LicenseInEuros) is the violation. We are expecting the external caller (WriteFeesToConsole) to distinguish between different subtypes because they need to be handled differently. That's the core problem that LSP seeks to avoid.

The second example

The second example does not retain the data contract. Slightly reshaping the posted example:

public void SetAndLogRectangleArea(Rectangle rectangle)
{
    rectangle.Width = 5;
    rectangle.Heigth = 15;

    Console.WriteLine($"The rectangle has a width of 5 and a height of 15");
    Console.WriteLine($"The rectangle has a width of {rectangle.Width} and a height of {rectangle.Heigth}");
}

Think about how you would implement a Square : Rectangle. Even though a Rectangle has two independent dimensions, a Square only has one (as the heigth and width are always equal - when you set one, you set the other). In order to create a Square : Rectangle, you're inherently required to make it so that setting the width also sets the height (and vice versa).

You will notice that when you pass a Rectangle into this example method, you will see the same message printed twice:

The rectangle has a width of 5 and a height of 15
The rectangle has a width of 5 and a height of 15

But when you pass a Square, you will see two different messages:

The rectangle has a width of 5 and a height of 15
The rectangle has a width of 15 and a height of 15

This is the violation. The Square behaves differently than its base class Rectangle.

"A rectangle had two independent dimensions (width/height)" is part of the Rectangle contract. However, a Square can never, at the same time:

  • Have two independent dimensions (width/height)
  • Be a correct representation of a square

A square can have two dimensions, but they are not independent. This violates the Rectangle contract.

Your interpretation

As far as I can tell, the Billing application could print the result of calculateFee, which would give a different result depending of the License used.

The focus isn't on the value. The fact that PersonalLicense and BusinessLicense return differnt values isn't a problem, it's likely the exact reason why these two classes exists as two separate subtypes of License.

Besides, two BusinessLicense objects could each return a different value as well.

For example, let's say business licenses are calculated as number_of_users * $5.

BusinessLicense microsoftLicense = new BusinessLicense() { NumberOfUsers = 300 };
var microsoftFees = microsoftLicense.calcFees(); // = $1500

BusinessLicense appleLicense = new BusinessLicense() { NumberOfUsers = 25 };
var appleFees = appleLicense.calcFees(); // = $125

How is that not a different behavior?

You need to distinguish between internal and external behavior.

var myResult = myBlackBox.Do(myInput1, myInput2);

What the black box does inside of its method, we don't care about. This is SRP: it's the black box' responsibility to do its job. We don't know what it's supposed to do, we just expect it to do its job.

What matters to us (and LSP) is how we have to handle the black box. This is important to us, because we need to be able to handle the black box correctly. We don't care if Blackbox has subtypes or not. We get a black box, we don't care what type of black box we get. We need to be able to handle them all the same way.


Let's say a FuelCar is a base type. The contract stipulates that you put normal fuel in the tank and it drives you from place to place.

  • A BrokenCarWithNoWheels : FuelCar violates LSP, because it doesn't drive you from place to place.
  • A DieselCar : FuelCar violates LSP, as it requires the driver to handle the car differently (= do not use normal gas but use diesel instead), which requires the driver to know that it's a diesel car.
  • A HamsterFuelCar, where the engine is driven by hamsters in hamster wheels, who can be fed by putting normal fuel in the tank, does not violate LSP. The driver still puts normal fuel in the tank and can drive from place to place. The contract is maintained.

Effectively, a driver could think he was driving a normal FuelCar, while actually driving a HamsterFuelCar, and the HamsterFuelCar would work exactly as the driver expects his (perceived) FuelCar to work.

OTHER TIPS

My understanding of the square/rectangle LSP violation is that its not the fact that the area assertion fails which is the problem.

The problem is that setting the width changes the height and vice versa.

Code written for rectangle assumes that the order of setting height and width is unimportant.

Any Shape object which respects that can replace Rectangle and the code will function sensibly. You could implement Triangle with a different area calc, replace Rectangle with it and get correct results from your program.

With Square, the area will always be incorrect. Because the order of setting height and width is random in the calling code.

Effectively setting width to something other than height should throw an exception. If you added an exception the LSP violation would be obvious to see.

That the assertion fails is a symptom, the problem is you cant write a correct assertion. Should the area be 8 or 25?

In the billing example we can always write a correct assertion for each type

At least without any additional context, the 1st example actually has little to do with LSP. LSP is not about how two different subtypes relate to each other. It is about the relation between a base type and a subtype.

In the example, the calcFee() method probably isn't even defined for the base type (virtual method). Therefore an application wouldn't even be able to call it for an object of the base type. While this does not contradict LSP, it isn't really helpful for explaining it either.

By the way, I find the definition of LSP you quote rather hard to understand and would prefer any of the following:

Objects of the base type don't show any behavior that objects of the subtype don't.

or

You can always use an object of a subtype instead of an object of the base type without any change in behavior (but not necessarily the other way round).

You are misunderstanding what calcFee does. It doesn’t return any particular value, it returns the correct value for this object. Both personal and business licenses return the correct fee and therefore behave the same - even if the fees are different.

Licensed under: CC-BY-SA with attribution
scroll top