The principle of substitution Liskov directly implies that preconditions should not be strengthened in subclasses. This is logical (because the overridden method of a subclass for which the input data will be unacceptable will not work correctly / throws an exception). But how then to design the following architecture:

There is a class, say Vehicle (you can make it abstract, it doesn’t matter), it has a virtual Weight method (even if this method does some abstruse calculations for each type of vehicle depending on weight, it’s not so important):

public abstract class Vehicle { public virtual void Weight(int w) { } } 

Now we have the Motorcycle class, which we inherit from Vehicle, but we have to limit its max mass to say 200 kg.

 public class Motorcycle : Vehicle { public override void Weight(int w) { if (w > 200) throw new WeightOverflowException(); } } 

everything seems fine, logical inheritance, with inheritance of all properties and behavior, but the LSP breaks:

  List<Vehicle> list2 = new List<ConsoleApp1.Vehicle>(); list2.Add(new Motorcycle()); foreach (var item in list2) { item.Weight(400); } 

a WeightOverflowException exception will fly, about which the base class should not know anything at all. How to design such a problem?

    2 answers 2

    In the design of the contract there is the concept of "accessibility preconditions". This means that client code must be able to find out whether it satisfies the preconditions or not.

    Here we can say that since the argument that the client transmits is involved in the precondition, this rule is observed. But the precondition itself varies depending on the type and is not explicit.

    In this case, we can select the precondition in a separate method and make the precondition itself polymorphic:

     public abstract class Vehicle { public virtual bool IsWeightValid(int w) {return true;} public virtual void Weight(int w) { Contract.Requires(IsWeightValid(w)); } } 

    Now, we say to our customers: "Before you pull the Weight method, make sure that the argument is valid." Yes, it may look like a brute force. And I would resort to this trick as a last resort, so as not to complicate the API. But this is a fairly common pattern. For example, collections in BCL expose the IsReadOnly property, which (theoretically) should be checked before calling the Add method.

    Someone considers the behavior of BCL a violation of the LSP, but I don’t think so: the method has the right to express its preconditions in a more abstract way, through its own properties or methods. Contract.Requires(ImInValidState) is no worse (theoretically) than Contract.Requires(methodArgument != null) .

    • Well, this is actually the second approach from my answer: to weaken the condition on the base class method. - VladD
    • Sori, I wrote the answer before I saw yours. But here the condition is not weakened: it remains as strict as it was originally. His behavior just becomes polymorphic. IMHO, this is not the same thing. - Sergey Teplyakov
    • When anything can throw an exception and the client does not have a chance to understand when it happens, whether it is an exception, a violation of a precondition / postcondition / invariant, then this is simply the absence of DBD. ZY For this, it is necessary not to weaken the precondition, but to strengthen it. Those. The base method should always throw an exception. Then it will follow the principle of substitution. - Sergey Teplyakov
    • one
      The absence of conditions in the base class is equivalent to Contract.Requires (true). If the heir starts to demand something (for example, the argument must be in a certain range), then he needs more. This is a violation of the LSP. If the precondition in the base method is Contract.Require (false), i.e. method can never be used. That the successor with the validation of arguments already weakens a precondition. And it is permissible. - Sergey Teplyakov
    • one
      @ OlegSh is essentially yes. The thrown exception is part of the contract. Usually we define in the base class that the method can throw SomeBaseExpception, and the heirs can throw it or a more special type of exception. - Sergey Teplyakov

    The approach that I like is immunity.

    At the same time, your weight will be set in the constructor, and will be checked there. Accordingly, you simply can not create an object with the wrong mass.

    The mutated Weight method (which in theory is more correct to call SetWeight , right?) Is not needed.


    Another approach in which the mass still remains mutable is to declare that the installation of the mass always has the right to throw an exception , even for the base object. I like this approach less, but he also has the right to life.

    • "The mutated Weight method (which in theory is more correct to call SetWeight, right?) Is not needed." - I specifically stated that this method does something “useful”, otherwise it would be enough properties :) - Oleg Sh
    • Those. in the base class set max. weight and check there (you can still in the virtual method Weight () of the base class to describe the precondition that the mass should not be more than the stated maximum)? And if the mass is not needed at all (for example, other methods will be used), does it mean that something is wrong with SRP? - Oleg Sh
    • @OlegSh: If the mass is not needed at all, then it is not clear why this object is a descendant of the original. - VladD
    • @OlegSh: For your specific example, the vehicle mass is a constant thing, isn't it? So the method that establishes it is not needed. If not, tell me more about your model. - VladD
    • for example, for some descendants it matters, for others it does not. Well, let this method be called differently as you like, it just uses mass. - Oleg Sh