1

According to Invariant rule in Liskov Substitution Principle, I know one of the form of violation of "Liskov Substitution Principle" is violating "invariants can't be weakened in a subtype", which the subclass allows some values that the base class forbids, eg:

public class Circle{ protected float radius; public void setRadius(float radius){ if(radius<0){ //throw some exception or show some error dialog then return } this.radius=radius; } } public class CustomCircle{ public void setRadius(float radius){ this.radius=radius; } } 

, which Circle forbids negative radius originally but CustomCircle allows it later.

But according to How does strengthening of preconditions and weakening of postconditions violate Liskov substitution principle, the example isn't also fit the description of violating "postconditions can't be weakened in a subtype"? ie : Circle guarantee that radius must not be negative after being set, but CustomCircle changes the behavior into allowing negative radius.

So what I don't know is, why are there two rules "postconditions can't be weakened in a subtype" and "invariants can't be weakened in a subtype" separately in "Liskov Substitution Principle"? Are "invariants can't be weakened in a subtype" just a type of "postconditions can't be weakened in a subtype"? If not, what is the difference between them? ie: Is there any example that violating "invariants can't be weakened in a subtype" but not violating "postconditions can't be weakened in a subtype"?

3
  • You can think of invariants as being both preconditions and postconditions that are added to every (non-private) method, or you can think of them as something separate. The author of that explanation is accounting for people who think of them separately. Commented Jul 26, 2024 at 10:37
  • It is aesthetically pleasing to that the terms of the question itself (“postcondition” and “invariant”) are themselves a subtype and a type. Commented Jul 26, 2024 at 13:36
  • 2
    Related question about pre-conditions and the LSP: Invariant rule in Liskov Substitution Principle. Commented Jul 26, 2024 at 15:09

3 Answers 3

3

Typical online accounts of LSP are often simplified or handwavy, or lack context, so you have to keep in mind that some of the stuff that you can find out there is not necessarily 100% representative of what was originally said. LSP comes from a computer science paper by Liskov and Wing1, where they formalize the idea of a subtype, and the notion of a type specification that subtypes need to satisfy.

In there, they establish that the specification should, among other things, specify certain properties of the methods (signatures, pre- and post-conditions), but they exclude constructors from this treatment, and add to the spec an explicit statement of type's invariant(s) to compensate (see addendum below for why).

This approach allows for more flexibility when defining subtype constructors, but the idea is that all constructors must respect the specified invariant, and that all methods must preserve it. The invariant also restricts the value space of the object, thus defining legal values (e.g., the value space underlying your Circle might be formally considered to be the value space of all floats, but the legal values for the type are restricted to nonnegative floats).

You can break an invariant without ever going through any of the methods - namely, in the subtype's constructor - by allowing an observable value that's not legal in the context of the supertype. Remember, a supertype might be a concrete class, but it could also be an abstract class (with or without constructors), or a pure interface, so it may have limited-to-no control over the initial state of the object. Also, depending on what the subtype is allowed to override, the subtype might, in principle, completely bypass any state coming from the supertype. (In fact, Liskov and Wing don't even assume a subtyping mechanism such as inheritance in their paper, so as far as they are concerned, the two types can be unrelated a priori.) So, that's one reason why you'd want to state that requirement separately.

But beyond that, yes - the methods must make sure to preserve the invariant (assuming the state of the object was legal upon entry), and it makes sense to connect this to pre- and post-conditions. However, within the context of the paper, the preservation of the invariant is treated as an implicit requirement for every mutator method, and so, the invariant itself is not formally counted among the explicitly stated pre- and post-conditions. It's more that you want the pre- and post-conditions to be compatible with the invariant, in the sense that its preservation follows from them. (Again, see the addendum below for additional context.)

As an example (modified from the paper), an invariant for a collection type (like a list or a vector) might be "lenght ≤ maxLength". On the other hand, the type's add(item) method might have the precondition that the "length is not equal to maxLength", and the postcondition "contains all the previous elements + the new one, and maxLength has not changed". You can then show that the invariant is preserved because the length is only increased by one, and only if the collection is not already full.

Now, given an incorrectly implemented subtype, it is possible for the corresponding add(item) method on the subtype to preserve the invariant, but violate the supertype's postcondition - say, buy simply ignoring the input. Explicitly placing restrictions that ensure compatibility of pre- and post-conditions ensures that the subtype's methods exhibit the same behavior as the supertype, when used in a context that expects the supertype (or rather, anything adhering to its spec).


Addendum: Another way to get some insight into why Liskov and Wing went about it in this particular way is to understand that they are sort of creating an inversion of a preexisting procedure called "data type induction", a formal method used to prove various properties of a type (outside the context of subtyping). The gist of it is, to infer some new, unstated but interesting or desirable property of the type, you'd start by demonstrating that it holds after construction, establishing your base case, and then proceed to prove that it remains true for the inductive case, for some method, by using other known properties, such as pre- and post-conditions. If such a property remains valid no matter what operation is applied, then it's an invariant. Again, in this context, this would be something that's inferred, something potentially not obvious in the code itself, not found among the explicitly stated properties of the type, method postconditions, and such. Well, Liskov and Wing don't allow constructors in the spec, meaning you can't establish the base case, so such invariants can't be inferred by this procedure - instead, you explicitly state in the spec what you want to be true of the type.


1 Liskov and Wing (1994) - A Behavioral Notion of Subtyping; https://doi.org/10.1145/197320.197383

0

I'm not a fan of these $10 words because they're open to nuanced misinterpretations. So I'm going to rephrase it in a more everyday phrasing:

Derivations of a class must be able to polymorphically moonlight as their derived base type(s) without in any way revealing that they are not concrete instances of that base type.

In other words, if it can't walk and talk like a duck, it shouldn't derive from Duck; the assumption in this example being that walking and talking are the only two features defined by the Duck class.

If a base class has defined certain behaviors; then it is the responsibility of a derived class to continue these defined behaviors without deviation. If I have a method which takes in a MyBase parameter and uses it in some way, then this method should never care whether you passed in an object of type MyBase, MyDerived : MyBase, or MyOtherDerived : MyBase.

If this method has to contain any logic that is specific to one of these concrete types, then your code is violating LSP.

which Circle forbids negative radius originally but CustomCircle allows it later

Whether or not that is an LSP violation depends on whether Circle explicitly defined behavior surrounding the rejection of negative radii. If it did, then any derived class from Circle has to maintain that behavior. CustomCircle would then be an LSP violation if it allowed that behavior.

But that's not a given. It's possible that this was left undefined, at which point there is no established behavior on how a Circle behaves with a negative radius, and therefore there is nothing for CustomCircle to deviate from, ergo it can't be an LSP violation. You can only deviate from something if you bothered to define it in the first place. This is the part that lots of LSP examples gloss over: not defining what the actually expected behavior of the base class is.

5
  • Can you clarify what do you mean by "defined/undefined"? Maybe with code examples? Would you consider throwing an exception (as in the question's example) a defined behavior? If yes – what could be an undefined behavior? Commented Jul 26, 2024 at 9:12
  • 1
    @Greg UB here implies - UB as declared by base class author. Literally a comment by author in the method documentation. Commented Jul 26, 2024 at 9:17
  • @Basilevs Thank you! Commented Jul 26, 2024 at 9:51
  • @Basilevs or sometimes the absence of a comment (covering a particular situation) Commented Jul 26, 2024 at 10:32
  • 1
    @Greg: Undefined means not considered. There was no consideration for how it behaves in this scenario. No guarantee that it works, if it works at all, and no elegant handling either. It's not impossible to explicitly document that you chose to leave a specific scenario undefined, but more commonly it refers to things that are never explicitly acknowledged nor relied on as a behavior. Commented Jul 26, 2024 at 13:44
0

Here is an example where the invariant gets broken, but not the pre- or postconditions.

class Rectangle { private: //invariant: width and height cam change independently int width; int height; public: void setWidth(int newWidth) { //post conditions: width has been updated width = newWidth; } void setHeight(int newHeight) { //post conditions: height has been updated height = newHeight; } }; class Square : public Rectangle { public: void setWidth(int newWidth) { Rectangle::setWidth (newWidth) ; Rectangle::setHeight(newWidth) ; } void setHeight (int newHeight) { setWidth (newHeight) ; } } 

Note that the post conditions on the setters don't mention anything about the other dimension, so the setters in Square don't violate the stated post conditions by updating two dimensions.

6
  • 1
    That's not an invariant, that's lack of one. It does not introduce any constraints. Commented Jul 26, 2024 at 9:19
  • 3
    @Basilevs: I respectfully disagree. The "invariant" in Rectangle is that width and height can be set to different values and still constitute a valid Rectangle. Being able to set the width and height to different values is allowable in Rectangle, but forbidden in the sub-type. The sub-type places an additional constraint on the width and height (these values must be the same), hence it strengthens the invariant and violates the Liskov Substitution Principle. Commented Jul 26, 2024 at 13:23
  • 1
    This is a contract, not an invariant. LSP is violated, but the term is misused. Commented Jul 26, 2024 at 13:46
  • @Basilevs: I believe there is some overlap between invariants and contracts. I've always thought of contracts as being a kind of invariant, where invariant is a contract in a more general sense. Commented Jul 26, 2024 at 15:03
  • 1
    More specifically, I've always thought of class invariants as a rule that must be kept true for all instances of a given type, rather than a value that cannot change. Commented Jul 26, 2024 at 15:04

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.