150

Say we have a list of Task entities, and a ProjectTask sub type. Tasks can be closed at any time, except ProjectTasks which cannot be closed once they have a status of Started. The UI should ensure the option to close a started ProjectTask is never available, but some safeguards are present in the domain:

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) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}

Now when calling Close() on a Task, there is a chance the call will fail if it is a ProjectTask with the started status, when it wouldn't if it was a base Task. But this is the business requirements. It should fail. Can this be regarded as a violation of the Liskov substitution principle?

Deduplicator
  • 8,591
  • 5
  • 31
  • 50
Paul T Davies
  • 3,144
  • 2
  • 22
  • 22
  • 14
    Perfect to a T example of violating liskov substitution. Do not use inheritance here, and you'll be fine. – Jimmy Hoffa Oct 16 '12 at 20:39
  • 9
    You might want to change it to: `public Status Status { get; private set; }`; otherwise the `Close()` method can be worked around. – Job Oct 16 '12 at 20:52
  • 6
    Maybe it's just this example, but I see no material benefit to complying with the LSP. To me, this solution in the question is clearer, easier to understand, and easier to maintain than one the complies with LSP. – Ben Lee Oct 22 '12 at 20:21
  • 5
    @BenLee It's not easier to maintain. It only looks that way because you're seeing this in isolation. When the system is large, making sure subtypes of `Task` don't introduce bizarre incompatibilities in polymorphic code which only knows about `Task` is a big deal. LSP isn't a whim, but was introduced precisely in order to help maintainability in large systems. – Andres F. Mar 03 '16 at 21:21
  • 12
    @BenLee Imagine you have a `TaskCloser` process which `closesAllTasks(tasks)`. This process obviously doesn't attempt to catch exceptions; after all, it's not part of the explicit contract of `Task.Close()`. Now you introduce `ProjectTask` and suddenly your `TaskCloser` starts throwing (possibly unhandled) exceptions. This is a big deal! – Andres F. Mar 03 '16 at 21:24
  • But Andres, your objections are contrived. This is not a large complex system. This is a simple encoding of business logic, and the business logic here is always going to be consistent with itself. Accidents of the sort you are talking about will not happen in practice. The LSP obfuscates the meaning in this case without offering a tangible benefit. – Ben Lee Oct 03 '17 at 04:14
  • 2
    So why not just make it a stated condition of `Task::Close` that sometimes you can't close tasks? For whatever (unspecified) reason, some `Task`s are just unclosable. They throw. And when that happens that task is in some state (probably the state they were in before the close was attempted) and you've just got to deal with it. Some subtypes of Task never throw on `Close` - and so what? – davidbak Mar 07 '21 at 18:38

12 Answers12

189

Yes, it is a violation of the LSP. Liskov Substitution Principle requires that

  • 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). Since 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.

Your example breaks the first requirement by strengthening a precondition for calling the Close() method.

You can fix it by bringing the strengthened pre-condition to the top level of the inheritance hierarchy:

public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}

By stipulating that a call of Close() is valid only in the state when CanClose() returns true you make the pre-condition apply to the Task as well as to the ProjectTask, fixing the LSP violation:

public class ProjectTask : Task {
    public override bool CanClose() {
        return Status != Status.Started;
    }
    public override void Close() {
        if (!CanClose()) 
            throw new Exception("Cannot close a started Project Task");
        base.Close();
    }
}
Sergey Kalinichenko
  • 17,393
  • 4
  • 57
  • 73
  • 21
    I don't like duplication of that check. I would prefer exception throwing going into Task.Close and remove virtual from Close. – Euphoric Oct 16 '12 at 20:53
  • 5
    @Euphoric That is true, having the top-level `Close` do the checking, and adding a protected `DoClose` would be a valid alternative. However, I wanted to stay as close as possible to the OP's example; improving upon it is a separate question. – Sergey Kalinichenko Oct 16 '12 at 20:57
  • 5
    @Euphoric: But now there is no way of answering the question, "Can this task be closed?" without trying to close it. This unnecessarily forces the use of exceptions for flow control. I will admit, however, that this kind of thing can be taken too far. Taken too far, this kind of solution can end up yielding an an enterprisy mess. Regardless, the OP's question strikes me as more about principles, so an ivory tower answer is very much appropriate. +1 – Brian Oct 16 '12 at 20:58
  • 32
    @Brian The CanClose is still there. It can still be called to check if Task can be closed. The check in Close should call this too. – Euphoric Oct 16 '12 at 20:59
  • 5
    @Euphoric: Ah, I misunderstood. You're right, that makes for a much cleaner solution. – Brian Oct 16 '12 at 21:04
  • 2
    In a multi-tasking system, it may be useful to also have a `TryClose` method, to allow for the possibility that an object might become uncloseable between the execution of a `CanClose` that returns true, and an actual attempt to close the object. Note that such possibility would not make `CanClose` useless, since knowing that an object was uncloseable might allow a code to skip work that would only be useful if a close was expected to succeed. – supercat Oct 17 '12 at 01:25
  • Thanks, a nice simple solution that has a good explanation and justification. – Paul T Davies Oct 17 '12 at 07:53
  • @dasblinkenlight Thanks for mentioning the 4 points on how to check for a violation of LSP. However, can you provide an example on a class hierarchy that violates these 4 points and how to solve them? Do you think I should post this as a new question instead? – Songo Oct 17 '12 at 09:36
  • @Songo The four points come straight from the Wikipedia page linked at the top of my answer. I think that elaborating on the last three would be well beyond the context of the OP's question, but it would make a nice question on its own. – Sergey Kalinichenko Oct 17 '12 at 09:39
  • If have a base method / property CanClose, it seems reasonable that the base Close method check CanClose and throw if it returns false. – Andy Oct 17 '12 at 14:06
  • @dasblinkenlight I posted the question [here](http://programmers.stackexchange.com/questions/170189/how-to-verify-the-liskov-substitution-principle-in-an-inheritance-hierarchy) maybe you can provide a nice answer ;) – Songo Oct 17 '12 at 19:06
  • Does the relationship between CanClose() and Close() introduce a temporal coupling between those two methods? Shouldn't that be avoided? – Ryan Michela Oct 18 '12 at 19:40
  • 1
    @RyanMichela Absolutely, it should. Ideally, it should be a "close it if you can, and tell me if you closed it or not" method. However, I wanted to keep the original `Close` method in place, otherwise my "fix" would have looked more like a "rewrite". – Sergey Kalinichenko Oct 18 '12 at 19:44
  • 2
    “Preconditions cannot be strengthened in a subtype.” Does the precondition have to be expressed in code? Couldn't you just document that `Task.Close()` can throw an exception if the `Task` can't be closed and keep the code the same? – svick Oct 18 '12 at 23:46
  • 2
    @svick The precondition is used very informally here, expressing the "things one must know before invoking a method". If `Task` allows subclasses to throw exceptions, then yes, it's OK to leave the code as is. Note, however, that "subclasses can throw" may need to be expressed in code: for example, in Java a base class method must declare all checked exceptions that its overrides are allowed to throw. – Sergey Kalinichenko Oct 18 '12 at 23:52
  • Now it only violates dependency inversion! – VF1 May 17 '14 at 01:27
  • 4
    still not getting it...the subclass still can throw an error that a superclass doesn't. So going by the definition: now when we substitute the supertype by its subtype it can happen that a supertype works just fine, whereas subtype throws an error. That's the definition of the LSP, right?! – Tomy Mar 29 '15 at 18:31
  • I'd make Close nonvirtual, and add a protected CloseCore method for subclasses to override. That way Close can throw an exception of CanClose returns false, removing the need for subclasses to do that check. – Andy Jun 30 '15 at 14:41
  • It is not clear from the original question what the precondition is. @dasblinkenlight has made the assumption that the original precondition for `Task.Close` is true and that the precondition for `ProjectTask.close` is `Status != StatusStarted`. But there are other assumptions that can be made. The real problem with the original code is that it is so under-documented that it is not even possible to tell whether the LSP is violated. If we assume the precond of `Task.Close` is that the task is closable, there is no violation. – Theodore Norvell Jul 13 '15 at 19:53
  • So, actually you're saying that in order to make sure that the derived type doesn't violate LSP, you'd expose one more method (for checking preconditions) to the client which he would need to call before calling Close method? How would you handle situation when he forgets to call `CanClose()` before calling `Close()`? In this situation for client it would still be a violation of LSP since he would get an exception when type is substituted by subtype. I guess using `TryClose` instead of `Close` would help here, since it returns bool instead of throwing an exception. – dragan.stepanovic Dec 07 '15 at 12:49
  • Great comment though I would agree that the Close method in the super class should check for CanClose and throw if it cannot, the sub-classes then only need to implement CanClose rather than both. Easier to use/implement + even more LSP compliant (still got my +1 regardless though) – Newtopian Mar 03 '16 at 19:37
  • 2
    I am surprised by the upvotes here. If consuming code does not know to check that CanClose method, a ProjectTask could still be closed early, triggering the exception. If I read the definition of the parent class, there's no hard connection between CanClose and Close(). – Graham Jul 28 '17 at 16:38
  • @Graham If consuming code does not know to check that CanClose method, the consuming code is in violation of the contract. Imagine consuming code deciding equality based on `GetHashCode()` alone, without following it by `Equals`. This code would get wrong results, despite there being no apparent connection between `GetHashCode()` and `Equals()`. – Sergey Kalinichenko Jul 28 '17 at 17:11
  • 2
    This code doesn't sit right for me, it trades one violation of LSP for another. The signature of the method `Task.Close` is that no exception is thrown, yet the overriding `ProjectTask.Close` then throws an exception. This itself is a violation of LSP. The public behaviour of the `Close` method has changed, which is the heart of the LSP idea. If instead the check is rolled up into `Task.Close`, then we can ensure consistent public behaviour between `Task` subtypes, avoiding the LSP violation. – e_i_pi Aug 16 '17 at 02:26
  • 2
    Fixed solution will still violate LSP. _Functions that use base classes must be able to use objects of derived classes without knowing it_ - In fixed solution as well as in original solution calling `Close()` will have different behaviour (throwing exception when use derived class). Argument "Read documentation and call `CanClose()` before" will not stop this design from violating LSP. – Fabio Oct 12 '17 at 03:52
  • 1
    @Fabio the LSP is about contracts, not about actual behavior. Code that fails to check CanClose() before calling Close() is violating the contract, and may fail (or may happen to succeed, but isn't guaranteed to). – mbrig Jan 18 '18 at 19:44
89

Yes. This violates LSP.

My suggestion is to add CanClose method/property to base task, so any task can tell if task in this state can be closed. It can also provide reason why. And remove the virtual from Close.

Based on my comment:

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

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public class ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}
netotz
  • 3
  • 3
Euphoric
  • 36,735
  • 6
  • 78
  • 110
  • 3
    Thanks for this, you took dasblinkenlight's example one stage further, but I did like his explanationa nd justification. Sorry I can't accept 2 answers! – Paul T Davies Oct 17 '12 at 07:55
  • I'm interested to know why the the signature is public virtual bool CanClose(out String reason) -- by using out are you merely future-proofing? Or is there something more subtle that I'm missing? – Reacher Gilt Oct 18 '12 at 20:51
  • @ReacherGilt What? No. The override is using it. Yes, you might return reason or null, but that would require client making the check. – Euphoric Oct 19 '12 at 05:36
  • but specifically, why out? It sets the expectation that reason will be modified by CanClose and I have a hard time understanding the value of modifying reason within the body of CanClose given its purpose. – Reacher Gilt Oct 19 '12 at 17:42
  • 3
    @ReacherGilt I think you should check what out/ref do and read my code again. You are confused. Simply "If task cannot close, I want to know why." – Euphoric Oct 19 '12 at 17:46
  • 2
    out is not available in all language, returning a tuple (or a simple object encapsulating the reason and boolean would make it better portable across OO languages albeit at the cost of loosing the ease of directly having a bool. That said, for languages that DO support out, nothing wrong with this answer. – Newtopian Mar 03 '16 at 19:35
  • 2
    And is it OK to strengthen the preconditions for the CanClose property? I.e. adding the condition? – John V Jan 17 '18 at 16:55
  • I dont understand how this violates LSP.Could you please explain a little bit please? – I Love Stackoverflow Jun 24 '19 at 12:51
26

Liskov substitution principle states that a base class should be replaceable with any of his sub-classes without altering any of the desirable properties of the program. Since only ProjectTask raises an exception when closed, a program would have to be changed to acommodate for that, should ProjectTask be used in substitution of Task. So it is a violation.

But If you modify Task stating in its signature that it may raise an exception when closed, then you would not be violating the principle.

Tulains Córdova
  • 39,201
  • 12
  • 97
  • 154
  • I use c# which I don't think has this possibility, but I know Java does. – Paul T Davies Oct 17 '12 at 07:56
  • 2
    @PaulTDavies You can decorate a method with what exceptions it throws, http://msdn.microsoft.com/en-us/library/5ast78ax.aspx . You notice this when you hover over a method from the base class library you will get a list of exceptions. It is not enforced, but it makes the caller aware nonetheless. – Despertar Jul 19 '13 at 06:19
26

An LSP violation requires three parties. The Type T, the Subtype S, and the program P that uses T but is given an instance of S.

Your question has provided T (Task) and S (ProjectTask), but not P. So your question is incomplete and the answer is qualified: If there exists a P that does not expect an exception then, for that P, you have an LSP violation. If every P expects an exception then there is no LSP violation.

However, you do have a SRP violation. The fact that the state of a task can be changed, and the policy that certain tasks in certain states should not be changed to other states, are two very different responsibilities.

  • Responsibility 1: Represent a task.
  • Responsibility 2: Implement the policies that change the state of tasks.

These two responsibilities change for different reasons and therefore ought to be in separate classes. Tasks should handle the fact of being a task, and the data associated with a task. TaskStatePolicy should handle the way tasks transition from state to state in a given application.

elias
  • 162
  • 5
Robert Martin
  • 1,161
  • 9
  • 6
  • 3
    Responsibilities heavily depend on domain and (in this example) how complex task states and its changers are. In this case, there is no indication of such thing, so there is no problem with SRP. As for the LSP violation, I believe we all assumed that caller doesn't expect an exception and application should show reasonable message instead of getting into erroneous state. – Euphoric Sep 04 '13 at 16:40
  • Unca' Bob responds? "We're not worthy! We're not worthy!". Anyway... *If every P expects an exception then there is no LSP violation.* BUT if we stipulate a T instance cannot throw an `OpenTaskException` (hint, hint) and *every P expects an exception* then what does that say about *code to interface, not implementation?* What am I talking about? I don't know. I'm just jazzed that I'm commenting on an Unca' Bob answer. – radarbob Sep 05 '13 at 16:32
  • 3
    You are correct that proving an LSP violation requires three objects. However, the LSP violation exists if there is ANY program P that was correct in the absence of S but fails with the addition of S. – kevin cline Mar 03 '16 at 21:31
20

This may or may not be a violation of the LSP.

Seriously. Hear me out.

If you follow the LSP, objects of type ProjectTask must behave as objects of type Task are expected to behave.

The problem with your code is that you have not documented how objects of type Task are expected to behave. You have written code, but no contracts. I'll add a contract for Task.Close. Depending on the contract I add, the code for ProjectTask.Close either does or does not follow the LSP.

Given the following contract for Task.Close, the code for ProjectTask.Close does not follow the LSP:

     // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Given the following contract for Task.Close, the code for ProjectTask.Close does follow the LSP:

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Methods that may be overridden should be documented in two ways:

  • The "Behaviour" documents what can be relied on by a client who knows the recipient object is a Task, but doesn't know what class it is a direct instance of. It also tells designers of subclasses which overrides are reasonable and which are not reasonable.

  • The "Default behaviour" documents what can be relied on by a client who knows that the recipient object is a direct instance of Task (i.e. what you get if you use new Task(). It also tells designers of subclasses what behaviour will be inherited if they don't override the method.

Now the following relations should hold:

  • If S is a subtype of T, the documented behaviour of S should refine the documented behaviour of T.
  • If S is a subtype of (or equal to) T, the behaviour of S's code should refine the documented behaviour of T.
  • If S is a subtype of (or equal to) T, the default behaviour of S should refine the documented behaviour of T.
  • The actual behaviour of the code for a class should refine its documented default behaviour.
Deduplicator
  • 8,591
  • 5
  • 31
  • 50
Theodore Norvell
  • 858
  • 6
  • 10
  • @user61852 raised the point that you can state in the method's signature that it can raise an exception, and by simply doing this (something that has no actual effect code wise) you are no longer breaking LSP. – Paul T Davies Jun 30 '15 at 15:39
  • @PaulTDavies You are right. But in most languages the signature is not a good way to declare that a routine may throw an exception. For example in the OP (in C#, I think) the second implementation of `Close` does throw. So the signature declares that an exception *may* be thrown -- it doesn't say that one won't. Java does a better job in this regard. Even so, if you declare that a method might declare an exception, you should document the circumstances under which it may (or will). So I argue that to be sure about whether LSP is violated, we need documentation beyond the signature. – Theodore Norvell Jul 01 '15 at 16:32
  • 6
    Plenty of answers here seem to completely ignore the fact that you can't know if a contract is validated if you don't know the contract. Thanks for that answer. – gnasher729 May 04 '16 at 16:37
  • Good answer, but the other answers are good as well. They infer that the base class does not throw exception because there is nothing in that class that shows signs of that. So the program, which uses the base class should not prepare for exceptions. – inf3rno Oct 10 '17 at 02:53
  • You are right that the exception list should be documented somewhere. I think the best place is in the code. There is a related question here: https://stackoverflow.com/questions/16700130/attribute-to-inform-method-caller-of-the-type-of-the-type-of-exceptions-thrown-b But you can do this without annotations, etc... too, just write something like `if (false) throw new Exception("cannot start")` to the base class. The compiler will remove it, and still the code contains what is needed. Btw. we still have an LSP violation with these workarounds, because the precondition is still strengthened... – inf3rno Oct 10 '17 at 03:02
  • 1
    Code is not the place to document behaviour. (a) This violates the whole idea of procedural abstraction; I should not have to read the body of a procedure to understand what it does. (b) In object-oriented languages such as Java and C# any nonfinal method can be overridden. (c) If the code is the documentation, we can never fix a bug in the code; it is correct by definition. (d) In languages like C# and Java, methods can be abstract in which case there is no code. These points apply mostly to classes and methods that lie on some sort of abstraction boundary, but that's where the LSP matters. – Theodore Norvell Oct 14 '17 at 20:19
  • Furthermore code like `if (false) throw new Exception("cannot start")` fails to inform the implementers of subclasses of what circumstances are reasonable for the exception to be thrown. And it fails to inform the client of what circumstances might lead to an exception being thrown. – Theodore Norvell Oct 14 '17 at 20:22
  • @inf3mo w.r.t your comment of Oct 10. I'm not sure that "infer" is the right word. I think the right word is "guess". Are we all okay with leaving engineering up to guess work? – Theodore Norvell Oct 14 '17 at 20:31
7

It is not a violation of the Liskov Substitution Principle.

The Liskov Substitution Principle says:

Let q(x) be a property provable about objects x of type T. Let S be a subtype of T. Type S violates the Liskov Substitution Principle if an object y of type S exists, such that q(y) is not provable.

The reason, why your implementation of the subtype is not a violation of the Liskov Substitution Principle, is quite simple: nothing can be proven about what Task::Close() actually does. Sure, ProjectTask::Close() throws an exception when Status == Status.Started, but so might Status = Status.Closed in Task::Close().

Oswald
  • 171
  • 3
5

Yes, it is a violation.

I would suggest you have your hierarchy backwards. If not every Task is closeable, then close() does not belong in Task. Perhaps you want an interface, CloseableTask that all non-ProjectTasks can implement.

Thorn G
  • 411
  • 2
  • 10
  • 3
    Every Task is closable, but not under every circumstance. – Paul T Davies Oct 16 '12 at 20:46
  • This approach seems risky to me as people may write code expecting all Task's to implement ClosableTask, though it does accurately model the problem. I'm torn between this approach and a state machine because I hate state machines. – Jimmy Hoffa Oct 16 '12 at 20:47
  • If `Task` doesn't itself implement `CloseableTask` then they're doing an unsafe cast somewhere to even call `Close()`. – Thorn G Oct 16 '12 at 20:49
  • @TomG that's what I'm afraid of – Jimmy Hoffa Oct 16 '12 at 20:52
  • 1
    There is already a state machine. The object can't be closed because it's in the wrong state. – Kaz Oct 17 '12 at 03:04
3

In addition to being a LSP issue, it seems like it is using exceptions to control program flow (I have to assume that you catch this trivial exception somewhere and do some custom flow rather than let it crash your app).

It seems like this be a good place to implement the State pattern for TaskState and let the state objects manage the valid transitions.

Ed Hastings
  • 968
  • 5
  • 6
1

I am missing here an important thing related to LSP and Design by Contract - in preconditions, it is the caller whose responsibility is to make sure the preconditions are met. The called code, in DbC theory, should not verify the precondition. The contract should specify when a task can be closed (e.g. CanClose returns True) and then the calling code should ensure the precondition is met, before it calls Close().

Ezoela Vacca
  • 881
  • 1
  • 6
  • 12
  • The contract should specify whatever behavior the business needs. In this case, that Close() will raise an exception when called on a started `ProjectTask`. This is a post-condition (it says what happen **after** the method is called) and fulfilling it is the called code's responsibility. – Stop harming Monica Jul 26 '18 at 18:19
  • @Goyo Yes, but as others said the exception is raised in the subtype which strengthened the precondition and thus violated the (implied) contract that calling Close() simply closes the task. – Ezoela Vacca Jul 26 '18 at 18:29
  • Which precondition? I do not see any. – Stop harming Monica Jul 26 '18 at 18:34
  • @Goyo Check the accepted answer, for example :) In the base class, Close has no preconditions, it is called and it closes the task. In the child, however, there is a precondition about status not being Started. As others pointed out, this is stronger criteria and the behavior is thus not substitutable. – Ezoela Vacca Jul 26 '18 at 18:37
  • Never mind, I found the precondition in the question. But then there is nothing wrong (DbC-wise) with the called code checking pre-conditions and raising exceptions when they are not met. It is called "defensive programming". Furthermore, if there is a post-condition stating what happens when the pre-condition is not met as in this case, the implementation has to verify the pre-condition in order to ensure that the post-condition is met. – Stop harming Monica Jul 26 '18 at 20:00
  • Also the contract **does** specify when a task can be closed: "at any time, except ProjectTasks which cannot be closed once they have a status of Started". – Stop harming Monica Jul 26 '18 at 20:00
  • @Goyo The problem in this case is that the contract is not stated and the user of the class can reason that calling Close() will close the task. If the contract specified that an exception can occur, LSP would not be violated as Theodore mentioned. However, in the current code, it is implied that the base class Close() just does that, and therefore derived classes should too. Otherwise - as in the example - they are not substitutable and LSP is broken. – John V Jul 26 '18 at 20:36
  • @user970696 On the contrary, it is explicitly stated that "tasks can be closed at any time, except ProjectTasks which cannot be closed once they have a status of Started" and that "[closing a started `ProjectTask`] should fail". I would expect the calling code to be aware of this. – Stop harming Monica Jul 26 '18 at 21:07
  • @Goyo :) No, the OP mentioned that just here, in the code there is no indication of that. In addidition, you cannot have a precondition on the base class mention handling of subtypes as they likely not even exist at that time. The precondition in the contract describes what must be true for a method to execute. I suggest you take a look at Theodore Norvell answer below. – John V Jul 26 '18 at 21:19
  • You see that would be like stating that rectangle area method returns a*b, except when it is a square. And I think this classical LSP problem has been beaten to death everywhere. – John V Jul 26 '18 at 21:22
  • @user970696 "in the code there is no indication of that" so what? Contracts don't have to be in the code (and more often than not they aren't). I hope the OP didn't mentioned it just here but also told the poor guys that had to implement the calling code. They deserve that. "[Y]ou cannot have a precondition on the base class mention handling of subtypes" Of course you can. That the subtypes don't exist yet (as code artifacts if that is what you mean) is not a problem, specifications can (and probably should) be written before the code. – Stop harming Monica Jul 26 '18 at 21:47
  • The precondition is only concerning the type it is specified on, otherwise it would have no meaning. The LSP states that behaviour of the base class must be guaranteed by its derivates, the precondition must be the same or weaker. Again, revisit the square and rectangle problem, which Liskov used to introduce LSP. The point there is that a rectangle cannot be a parent of square, as rectangle users expect that SetWidth does not alter the Height. It is all about behaviour being guaranteed by the subtype. I agree though contracts can be specified elsewhere, but I doubt OP meant that. – John V Jul 26 '18 at 22:17
  • Anyway, the precondition for the subtype is stronger than for its parent, no matter how and where it is specified. Liskov gave similar example with argument values being negative for base, and lower than -1 in the child. The reason for LSP violation was the same, the precondition was stronger (accepted less values) – John V Jul 26 '18 at 22:25
1

It seems to me that this is an example where the violation of LSP arise, in fact, from a prior violation of some other SOLID principle, in this case, the Single Responsibility.

Let's work with the problem at hand: whats Task's responsibility? Its not clear. Lets say it is to ... perform a Task. If that's the case, it should not worry about whatever close has to do. Clients of, lets say..., AbstractTask interface should only worry about tasks, so every AbstractTask concrete implementation job should be only to implement its task in a predictable and stable way. LSP rules are for that.

On the other hand, there's a requirement of a task always being able to be closed. But there are tasks that can be closed at any time, there are tasks that that must satisfy some conditions before being ready to be closed. This is kinda similar to the canonical LSP violation example of the Real vs Toy duck:

class Duck{
   void quack() = 0 //abstract interface
   ...
}
class RealDuck() {  
   void quack() {// say "quack!"}
   ...
}
class ToyDuck() {  
   void quack() { if(has_batteries) // say quack}
   ...
}

At this point I believe that the best choice depends on your problem requirements. If one expects to have a lot of concrete implementations that violates LSP this way I think that the best choice is to use the Strategy Design Pattern. Strategy decouples the client object from the components that implement its behaviour. This way LSP is intact (no strict requirements in derived classes, change of behaviour chosen at runtime) and greater flexibility and reusability is achieved.

But it doesn't seems to be the case of task/projectTask. In this scenario, where few exceptions to the general rule are expected, a simple and effective solution is to just implement SRP and segregate Closeable from other tasks (e.g., regular tasks implement Closeable, projectTask doesn't) and making clients that may close tasks to rely upon Closeable and not upon Tasks. Static checks at compile time guarantee that projectTask will never be used when freely Closeable objects are required, even if projectTask has a close() method itself.

Sounds good? I hope it helps.

1

You can’t decide whether something is an LSP until you define what a method is supposed to do. And then you check that the baseclass and the derived class match that definition. If the caller relies on behaviour that is not defined, that’s a bug in the caller. But if the behaviour is defined, but one class doesn’t follow it, that’s an LSP violation.

In your case, I can define “Close() either closes the task or throws an exception”. Your two classes don’t violate LSP. Another subclass that quietly ignores Close() calls in the wrong state violates LSP.

I could define Close() in a different way: “Throws an exception in the Started state, otherwise sets the state to closed”. Now Task violates LSP.

There’s the classical example of Rect and Square: It’s just very hard to make Square a subclass of Rect that doesn’t violate LSP.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
0

Yes, it is a clear violation of LSP.

Some people argue here that making explicit in the base class that the subclasses can throw exceptions would make this acceptable, but I don't think that is true. No matter what you document in the base class or what abstraction level you move the code to, the preconditions will still be strenghtened in the subclass, because you add the "Cannot close a started Project Task" part to it. This is not something you can solve with a workaround, you need a different model, which does not violate LSP (or we need to loosen on the "preconditions cannot be strenghtened" constraint).

You can try the decorator pattern if you want to avoid LSP violation in this case. It might work, I don't know.

inf3rno
  • 1,209
  • 10
  • 26