153

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?

7
  • 14
    Perfect to a T example of violating liskov substitution. Do not use inheritance here, and you'll be fine. Commented Oct 16, 2012 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
    Commented Oct 16, 2012 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
    Commented Oct 22, 2012 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.
    Commented Mar 3, 2016 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.
    Commented Mar 3, 2016 at 21:24

12 Answers 12

193

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();
    }
}
26
  • 21
    I don't like duplication of that check. I would prefer exception throwing going into Task.Close and remove virtual from Close.
    – Euphoric
    Commented Oct 16, 2012 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. Commented Oct 16, 2012 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
    Commented Oct 16, 2012 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
    Commented Oct 16, 2012 at 20:59
  • 5
    @Euphoric: Ah, I misunderstood. You're right, that makes for a much cleaner solution.
    – Brian
    Commented Oct 16, 2012 at 21:04
90

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);
    }
}
8
  • 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! Commented Oct 17, 2012 at 7: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? Commented Oct 18, 2012 at 20:51
  • 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
    Commented Oct 19, 2012 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
    Commented Mar 3, 2016 at 19:35
  • 2
    And is it OK to strengthen the preconditions for the CanClose property? I.e. adding the condition?
    – John V
    Commented Jan 17, 2018 at 16:55
27

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.

2
  • I use c# which I don't think has this possibility, but I know Java does. Commented Oct 17, 2012 at 7:56
  • 3
    @PaulTDavies You can decorate a method with what exceptions it throws, 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
    Commented Jul 19, 2013 at 6: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.

3
  • 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
    Commented Sep 4, 2013 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
    Commented Sep 5, 2013 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. Commented Mar 3, 2016 at 21:31
21

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.
8
  • @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. Commented Jun 30, 2015 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. Commented Jul 1, 2015 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
    Commented May 4, 2016 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
    Commented Oct 10, 2017 at 2:53
  • 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. Commented Oct 14, 2017 at 20:19
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().

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.

5
  • 3
    Every Task is closable, but not under every circumstance. Commented Oct 16, 2012 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. Commented Oct 16, 2012 at 20:47
  • If Task doesn't itself implement CloseableTask then they're doing an unsafe cast somewhere to even call Close().
    – Thorn G
    Commented Oct 16, 2012 at 20:49
  • @TomG that's what I'm afraid of Commented Oct 16, 2012 at 20:52
  • 1
    There is already a state machine. The object can't be closed because it's in the wrong state.
    – Kaz
    Commented Oct 17, 2012 at 3: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.

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().

13
  • 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. Commented Jul 26, 2018 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. Commented Jul 26, 2018 at 18:29
  • Which precondition? I do not see any. Commented Jul 26, 2018 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. Commented Jul 26, 2018 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. Commented Jul 26, 2018 at 20:00
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.

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.

Not the answer you're looking for? Browse other questions tagged or ask your own question.