7

I am a bit confused as for what it really means. In the related questions (Is this a violation of the Liskov Substitution Principle?), it was said that the example clearly violates LSP.

But I wonder, if there is no new exception thrown, would it still be violation? Isn't it simply polymorphism then? I.e:

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
          {
              base.Close(); 
          }
     }
}
Vincent Savard
  • 1,906
  • 1
  • 17
  • 16
John V
  • 4,898
  • 10
  • 47
  • 73
  • It's important to note that in your linked answer, that the LSP violation is incurred as there are NO actual contracts in the base `Task Close` method, whereas the overridden `ProjectTask` implementation adds the precondition that status may NOT be Started, i.e. strengthening from No contract to some contract precondition. You don't have any contracts in your example. However there is bad OO practice - by making Close virtual, subclasses can bypass any contracts it may have contained, and by making Status set public, any external code can change state and bypass all contracts and rules anyway. – StuartLC Jan 18 '18 at 14:26
  • @StuartLC Hmm I thought that my contract in the ProjectTask is that the status has to be Started, which is strenghtening from nothing in the base class. – John V Jan 18 '18 at 17:18

6 Answers6

21

You can't decide whether some code violates the LSP by the code itself. You need to know the contract that each method is to fulfill.

In the example, there is no explicit contract given, so we have to guess what the intended contract of the Close() method might be.

Looking at the base class implementation of te Close() method, the only effect of that method is, that afterwards the Status is Status.Closed. My best guess of a contract for this method reads:

Do whatever is necessary to make the Status become Status.Closed.

But that's just a plausible guess. Nobody can be sure about that if it isn't explicitly written down.

Let's take my guess for granted.

Does the overridden Close() method also fulfill that contract? There are two possibilities that after running this method we have Status.Closed:

  • We already had Status.Closed before calling the method.
  • We had Status.Started. Then we call the base implementation, setting the field to Status.Closed.
  • In all other cases we end up with a different status.

If Status only has the two possible values Closed and Started (e.g. a 2-value enum), everything is fine, there's no LSP violation, because we always get Status.Closed after the Close() method.

But probably there are more possible Status values, ending up in a Status not being Status.Closed, thus violating the contract.


The OP asked about the famous sentence "wherever I am using the base class, its derived class can be used".

So I'd like to elaborate on that.

I read it as "wherever I am using the base class within its contract, its derived class can be used, without violating that contract.

So it's not only about not producing compile errors or running without throwing errors, it's about doing what the contract demands.

And it only applies to situations where I ask the class to do something that's within its intended range of operations. So we need not care about abuse situations (e.g. where preconditions aren't met).


After re-reading your question, I think I should add a paragraph on polymorphism in that context.

Polymorphism means that for instances of different classes, the same method call results in different implementations being run. So polymorphism technically allows you to override our Close() method with one that instead e.g. opens a stream. Technically, that's possible, but it's a bad use of polymorphism. And one principle about good and bad uses of polymorphism is the LSP.

Ralf Kleberhoff
  • 5,891
  • 15
  • 19
  • Well, but the principle says that wherever I am using the base class, its derived class can be used. So I believe it is possible to see right from the code too. But I hope I understand your point that no expected behavirour is clearly stated. – John V Jan 18 '18 at 11:31
  • @user970696: Well, we suffer a case of *insufficient information* for deciding that just on the code. As Ralf said, we don't know the definition of `Status`. Also, we use explicit contracts independent of the code implementing it mainly for two reasons: We might not be able to exhaustively study all instances where our code is used, for example because some of those might not yet be written, or simply be private to someone else. Second, without knowing the contract we cannot determine whether any change to the implementation is just a possible optimization or breaks the world. – Deduplicator Jan 18 '18 at 11:35
  • @user970696: So, LSP was formalized in a 1994 paper by B. Liskov and J. Wing; there they do a couple of things. First they define what subtyping *means*, and relate it to how a subtype behaves compared to a base type (the paper is called "A behavioral notion of subtyping"), and specify certain constraints that need to be met for this to work. Second they propose some language mechanisms and techniques that would enforce those constraints. However, most OO languages that we use *do not have* such mechanisms, and this is why it's not enough to just look at the code. (continues below...) – Filip Milovanović Jan 18 '18 at 12:26
  • The *contract* we speak of defines what is expected of a certain type in terms of behavior. But, since programming languages are generally not expressive enough to encode all aspects of this conctract, we have to rely on documentation, code comments, and other contextual information, as these define the expectations about how the type works. So, for example, if the documentation of a method says "Attempts to [do something]", it's OK for an override to fail. But if it says "Guarantees that [something will happen], and doesn't throw any exception", then it's not, and it's a violation of LSP. – Filip Milovanović Jan 18 '18 at 12:26
  • P.S. Good (and bad) method naming comes into it as well. – Filip Milovanović Jan 18 '18 at 12:33
  • @FilipMilovanović, "However, most OO languages that we use *do not have* such mechanisms..." I disagree: the mechanism exists and is commonly referred to as "unit tests". Such automated checks provide a guaranteed contract for code behaviour. – David Arno Jan 18 '18 at 12:49
  • @FilipMilovanović Off topic, I'd regard a contract like "... and doesn't throw any exception" a bad idea, as it implicitly creates the obligation "the method must never fail", and in real-world you can never guarantee that. – Ralf Kleberhoff Jan 18 '18 at 12:50
  • 2
    @RalfKleberhoff: Unit tests - yeah, but unit test are not a language mechanism (as in, built into the language), that's something we write. And they don't necessarily provide a guarantee, but rather document expectations. Off topic/exceptions: OK, good point - I tried, perhaps a bit clumsily, to illustrate how documentation affects the contract. – Filip Milovanović Jan 18 '18 at 12:57
  • Re: Polymorphism vs. LSP: Polymorphism allows you to have different implementations for the same operation. Common sense dictates that those different implementations should behave similarly. LSP gives a mathematically precise definition of the intuitive concept of what "behave similarly" means. – Jörg W Mittag Jan 18 '18 at 13:23
  • Thank you all. Also first I need to think about the contract, or rather expected behavior. Let's say my base class has a method Update that immediately performs an update. Its derivative overrides this method and only performs the update if the last update was more than a minute ago. How could I set the contract so that this does not violate LSP? – John V Jan 18 '18 at 13:37
  • @OP This "update at most once a minute" can turn out quite tricky. I'd recommend to ask that in its own question. – Ralf Kleberhoff Jan 18 '18 at 14:00
  • @RalfKleberhoff Never-failing methods are an important part of software engineering. In C++, there are many methods so designated. Within the bounds of what the program can control at all (i.e. not random bit flips and power failures), you can guarantee no-failure, although of course it quite limits you in what you can do. But never-throws is a guarantee you can uphold even through a power failure. – Sebastian Redl Jan 19 '18 at 07:27
7

Liskov Substitution Principle is all about contracts. It consists of preconditions (conditions that must hold true so the corresponding behavior could run), postconditions (conditions that must hold true so that behavior could be considered to finish its job), invariants (conditions that must hold true before, during and after the corresponding method execution) and history constraint (in my opinion it's a subset of invariant, so you'd better check wikipedia). In a question you linked to an implied contract of the Task class looks like the following:

  • Precondition: there is none
  • Postcondition: Status is closed
  • Invariant: can't see any

So if one the child classes doesn't close the task, it is considered a violation of LSP within some certain contract.

But if you explicitly postulate your contract to be like "Close the task only if it is started", then you're fine. You can do it in your code -- an example of that is this accepted answer. But very often you can't -- so you could use plain comments.

Basically, every time you think about LSP violation, you should already be familiar with the contract. There is no such thing as just "LSP violation", only "LSP violation within some contract".

Vadim Samokhin
  • 2,158
  • 1
  • 12
  • 17
  • Right, but now I wonder, isn't polymorphism about implementing different behaviours? – John V Jan 18 '18 at 10:39
  • Only the behaviors that fit the contract. – Vadim Samokhin Jan 18 '18 at 10:43
  • Also,isn't the precondition here the condition Status==Status.Started? Because that represents stronger criteria that conditionally executed the content of the method. – John V Jan 18 '18 at 10:46
  • I don't know, it's your code, so it's up to you to define a contract :) Moreover, contracts should be identified first, and the code is written only to satisfy them. Think about it: how do you usually model your domain? You try to find some useful abstractions, right? Abstractions that represent some replaceable parts of your system. These parts possess some responsibilities. Then you identify the ways the contact to each other. By now these responsibilities are getting more strict: they are becoming contracts. And then you write some code -- just to satisfy the contracts you've drawn. – Vadim Samokhin Jan 18 '18 at 11:02
  • 1
    @user970696: a precondition is something which must hold before even the method is allowed to be called. So if "Status==Status.Started" was a precondition of `Close`, calling it with another value for Status would always throw an exception. This is clearly not the case here. – Doc Brown Jan 18 '18 at 11:03
  • @DocBrown hmm but most examples that I found were using it just like that, see e.g. here (NonLiskov Calculator examples). https://www.codeproject.com/Articles/613304/SOLID-Principles-The-Liskov-Principle-What-Why-and – John V Jan 18 '18 at 11:07
  • 1
    @user970696: that example shows what I wrote: it throws an exception if the precondition is violated. Your example above does not throw any exceptions. – Doc Brown Jan 18 '18 at 11:16
  • @DocBrown What I meant was that they talk about preconditions when they mean the first "if" clause in the method, not anything before calling it. That is confusing me. Check those example with strenghtening preconditions. – John V Jan 18 '18 at 11:18
  • @user970696: that example tries to demonstrate contracts without special programming language support, so testing if the precondition holds in the first "if" statement is just a workaround. However, this does not work the other way round - just because a method starts with an "if" statement, the condition in there is not necessarily a precondition (and surely not as long as there will be no exception thrown in the "else" section). – Doc Brown Jan 18 '18 at 12:01
  • @DocBrown: If the precondition is violated, there is no contractual behavior. So, it might throw, abort, log, just return or behave eratically. That's all allowed, but of differing utility when debugging. – Deduplicator Jan 18 '18 at 12:04
  • @DocBrown, now I see! But if there is no language support, then I guess this is a good way of checking the precondition. And that is what I wanted to express with STatus==Status.Started. It is being checked first as it should serves as a precondition. – John V Jan 18 '18 at 12:07
  • @user970696: in your specific case, I am pretty sure you don't want your "Close" method having the precondition that the status is always "Started" when "Close" gets called, neither in the "Close" of the base class, not in the "Close" method of the derived class. You want *a different behaviour* in case Status is "Started", but you don't want to enforce that Status must be "Started" before Close will be called legally. – Doc Brown Jan 18 '18 at 13:18
  • ... and the problem with using standard "if" for contracts is, you cannot easily use it to define a precondition or post condition in the base class which will then also be inherited by the derived class. When it comes to inheritance, one needs language support if one wants more than contract by comments. – Doc Brown Jan 18 '18 at 13:20
  • @Deduplicator: what you wrote is true in general, however we were talking about how to implement a precondition in code, or how to determine from some piece of code if there is some precondition. The OP had the misconception that if a function starts with `if(someCheck)`, the "someCheck" part would automatically constiute as a precondition (in the "design-by-contract" sense), and that is wrong. But if a function copy looks like `if(someCheck)...else{throw ...}`, that could be a strong indicator for "someCheck" being a precondition of that function. – Doc Brown Jan 18 '18 at 14:42
6

It depends.

For validating the LSP, you need to know the precise contract of the Close function. If the code looks like this

public class Task
{
     // after a call to this method, the status must become "Closed"
     public virtual void Close()
     //...
}

then a derived class which ignores this comment violates the LSP. If, however, the code looks like this

public class Task
{
     // tries to close the the task 
     // (check Status afterwards to find out if it has worked)
     public virtual void Close()
     //...
}

then ProjectTask does not violate the LSP.

However, in case there is no comment, a function name like Close gives IMHO a the caller a pretty clear expectation of setting the status to "Closed", and if the function then does not work that way, it would be at least a violation of the "Principle of least astonishment".

Note also that some programming languages like Eiffel have in-built language supports for contracts, so one does not necessarily need to rely on comments. See this Wikipedia article for a list.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • So a solution, in general, would be that the base class would have something like "TryClose" and then the children could implement their own logic. Is that right? – John V Jan 18 '18 at 11:05
  • @user970696: `TryClose` would probably be a better name, if that is the behaviour you like to model. A comment about the precise expected behaviour in the base class would probably a good idea, too. And if your language supports this, a statement like `Postcondition(Status==Status.Started || Status==Status.Closed)` in the base class would also indicated that the Status is not necessarily set to "Closed". – Doc Brown Jan 18 '18 at 11:13
  • What I am trying to understand: lets say a base method X immediately outputs "X" to the console. Derived class overrides X and only outputs "X" when the time (seconds) are greater than 10. Is that violation of LSP? – John V Jan 18 '18 at 11:13
  • @user970696: add a contract, and one can tell. A method name like "X" does not serve well as a contract. It does not even serve well for guessing what the intended behaviour should be, – Doc Brown Jan 18 '18 at 11:18
  • To the user of the class, it would imply that X is written in the console. I mean, should it not be enough to just try to substitute the derived class for the base class, to see whether it is working? – John V Jan 18 '18 at 11:21
1

Yes, still a violation (probably)

Some client of Task relies on "after Task::Close(), Status is now Closed", and then breaks when it encounters a ProjectTask. You might currently not have any such clients, but then the postcondition of Task::Close() would have to be "Status is in a valid but unspecified state", which is basically pointless.

The much more natural thing is for Task::Close() to have the postcondition "Status is Closed", which precludes the implementation in ProjectTask from being valid.

This is a major problem with void DoStuff() methods: all you have are some side-effects, so you have things relying on those side-effects. bool TryClose() has the meaning "Close() if you can, and tell me about it"

Caleth
  • 10,519
  • 2
  • 23
  • 35
  • Actually, why Task.Close() is the postcondition? – John V Jan 18 '18 at 09:55
  • I am talking about the postcondition *of* Close – Caleth Jan 18 '18 at 10:08
  • Thanks. So the violation is really in having the condition there? I struggle with differentiating that from polymorphism – John V Jan 18 '18 at 10:10
  • It comes down to what `Close` means, to the users of `Task`. If **no-one is allowed to care** about what it does to `Status`, then you can do it. It's just that is very limiting, and probably confusing – Caleth Jan 18 '18 at 10:13
  • @user970696 - I guess it would be ok if your method was `bool TryClose()`. – Emerson Cardoso Jan 18 '18 at 10:23
  • @EmersonCardoso But why? I seem to be missing here the point how to find out what is allowed and what not. If I sustitute the base class for the derived one, the behaviour might change in this case, some tasks might not be closed. – John V Jan 18 '18 at 10:26
1

As Ralf and others have mentioned, you haven't actually implemented or enforced any contracts on your code, other than by the assumed "common sense" convention that Close() should leave the object in a closed state, and other than the comments that were added to the subclass.

In my opinion, the example you've provided (I know it's copied from a related post) has a design flaw to declare the Close() method as virtual on the base Task class - this is just inviting others to subclass Task and change the behaviour, even though you've provided a default implementation which observes the contract.

And worse, since Status isn't encapsulated at all, state is publicly mutable, so any contracts around Close are fairly meaningless as state can be randomly assigned externally in any event.

So if your class hierarchy doesn't require polymorphic behaviour of Close, I would simply remove the virtual keyword on Task.Close:

// Encapsulate status, to control state transition
public Status Status { get; private set; }

public void Close()
{
    Status = Status.Closed;
}

(and do the same for any other state transitions)

If however you DO require polymorphic behaviour (i.e. if subclasses need to provide custom implementations of Close), then I would convert your base Task class to an interface, and then enforce any pre and post conditions through Code Contracts, as follows:

[ContractClass(typeof(TaskContracts))]
public interface ITask
{
    Status Status { get; } // No externally accessible set

    void Close();
    // Other transition methods here.
}

With the corresponding contracts:

[ContractClassFor(typeof(ITask))]
public class TaskContracts : ITask
{
    public Status Status { get; }

    public void Close()
    {
        Contract.Requires(Status != Status.Closed, "Already Closed!");
        Contract.Ensures(Status == Status.Closed, "Must close Task on Completion!");
    }
}

The benefit of this approach is that the interface usage contract is clear (and enforceable!), and unlike the virtual Close() which could be bypassed, subclasses can provide any implementation they like, provided that the contract is met.

StuartLC
  • 1,246
  • 8
  • 14
  • 1
    Your example is technically ok, however, the OP probably wants to have a contract where there is no precondition, and the postcondition is `Status==Status.Closed|| Status==Status.Started`. – Doc Brown Jan 18 '18 at 11:51
  • The OP's question is clearly about LSP contract violation - the condition, `Status == Status.Started` is defined in a subclass, which wouldn't have been part of the original contract design, had one been in place. So hypothetically, if the `Status.Closed` contract had been in place, then yes, OP would have strengthened the post condition, and thus have violated LSP. – StuartLC Jan 18 '18 at 11:56
  • Well mainly I am still puzzled as for what is the precondition. In the examples I found it is often the very first IF in the called method. Yet Doc says it is not the case. – John V Jan 18 '18 at 11:56
  • You haven't defined any contracts, so as it stands, everything is 'fair game', especially since `Status` has a *public setter*. If your intention is to enforce DBC, then I would suggest using Code Contracts such as above (your code is C#, right?) and then you will get a compile time warning and a run time error if your `ProjectTask` subclass doesn't close Status. – StuartLC Jan 18 '18 at 12:03
  • Precondition => object and parameter state going INTO the method. PostCondition => State guarantees leaving the method. Doc is correct, I added the 'Can't already be closed' as an example of a precondition, even though you didn't actually state this as a requirement (and specifically remember that `IDisposable.Dispose` requires that an object not throw if it has already been Disposed). `virtual` methods aren't good places for enforcing code contracts, as they can be bypassed altogether in subclasses. – StuartLC Jan 18 '18 at 12:11
  • @StuartLC So, basically: if I document that the Close (at the base class level) method closes the task and sets the status to Closed, then that example violates LSP as it might not be true if I pass the child somewhere where the parent type can be used. If I document only that this method tries to close the window, would it be correct then with regards to LSP? – John V Jan 18 '18 at 12:42
  • @user970696 In the original [code](https://softwareengineering.stackexchange.com/questions/170138), there was no contract on the base `Close` method, but a new `throws` precondition was then added to the subclass (close may not be called if status is Started), so in that case, there's a clear strengthening of the precondition (from none, to "may not be in Started"), so also a clear violation of LSP (although again, the code example exhibits poor OO design, since virtual Close can be overridden and bypassed and Status can be set directly anyway) – StuartLC Jan 18 '18 at 14:12
  • [Perhaps this helps?](https://stackoverflow.com/a/20861211/314291) Strengthening of Preconditions is about halfway down. – StuartLC Jan 18 '18 at 14:13
  • @StuartLC: any function is the perfect place to enforce pre-conditions. Virtual functions just aren't so good at enforcing post-conditions, as that enforcement is coupled with the specific implementation, not their slot. – Deduplicator Jan 18 '18 at 14:25
  • There's a much better, out of the box tool for enforcing DBC in C# and .Net, supported by all the MS tools, viz Code Contracts. Implemented on interfaces, when enforced, they can't be bypassed at run time, and bonus, you get compile time checking as well. – StuartLC Jan 18 '18 at 14:34
  • @StuartLC Thanks for the link to that answer, yet I still can see the issue there in the part Violation - pre-condition strengtened. So the whole problem is that wherever Vehicle is expected and e.g. 299 miles argument is provided, Scooter instance would fail. So what I want to implement that correctly, without violating LSP? – John V Jan 18 '18 at 16:50
  • Avoiding LSP precondition strengthening violation is simple - there must only be 1 set of contracts, defined on the 'base class' or preferably on a 'base interface' definition, as above, which is then "law" for all subclasses / interface implementations. Subclasses may NOT add new preconditions, since good SOLID code will be built against the abstraction - base class or interface - and ALL subclasses must correctly function against this contract. Any subclass which adds new pre/postcondition rules will fail any Unit Tests (theories) which were built against the original contract. – StuartLC Jan 19 '18 at 06:47
  • @StuartLC An interesting thing is that weakening of preconditions does not break LSP and it does change behaviour - when base class expects A>10 and A<100 and subclass only A>10, then when I replace the base class with the subclass, it suddenly starts to accepts arguments that were before invalid. – John V Jan 19 '18 at 07:21
  • Well, no -if the base class preconditions are enforced correctly, then an bad value can't be passed to a subclass, even if subclass is ok with it. The problem with the example you've cited is that the whole design is FUBAR. Give the code contracts above a try (with interfaces) - you may need to install a .vsix, switch on all the rule enforcements, and I think everything will become clearer? – StuartLC Jan 19 '18 at 08:38
  • @StuartLC Well I do not thik so. If you weaken the precondition in the derived class, then apparently it can accept more than its parent. I would be really grateful if you could take a look at my other question with example: https://stackoverflow.com/questions/48329231/why-weakening-a-precondition-does-not-violate-liskov-substitution-principle – John V Jan 19 '18 at 08:54
0

Yes, it still is a violation of the LSP.

In the base Task class, after Close() has been invoked the Status is Closed.
In the derived ProjectTask class, after invoking Close() the Status may or may not be Closed.

Thus the postcondition (Status is Closed) is no longer fulfilled in the ProjectTask class.
Or in other words: A client only knowing about Task may rely on the fact that Status is Closed after invoking Close(). If you give him a ProjectTask "disguised" as a Task (which you are permitted to do), and he invokes Close() the result is different (Status might not be Closed).

CharonX
  • 1,633
  • 1
  • 11
  • 23
  • Thanks. And what if there was no status change? What if there is still the condition but the only executed command would be a console output or something like that. – John V Jan 18 '18 at 10:29
  • As Ralf Kleberhoff put it so elaborately in his excellent answer: What is the functionality that *Close()* promises to deliver in the base class (Task). Looking only at the name of the Function (*Close()*) and its code (set Status to **Closed**) I infer that (*something happens here* Status is now **Closed**) is the promised functionality / postcondition. In that sense, if printing on the console were the promised functionality, then the derived class would need to do that too to uphold the LSP. – CharonX Jan 18 '18 at 13:57