3

I've been digging into some of the SOLID design principles lately, and some information I got from one source originally made sense to me, but based on the strict definitions I've been able to find for thr LSP, it seems like that info might not be correct. The info was specifically that:

1) Not calling back into super() on an overridden method violates LSP (or at least opens you up to violation) because the base class's behavior may change at some point, and your subclass could be missing that behavior in a way that causes the subclass to no longer be substitutable for the parent. That seems to make sense to me, but it would be great if someone could elaborate on that/give some info on when it would ever be appropriate to not call back into super.

2) Subclasses should not be less restrictive than the parent. The example was: if you have a parent class that takes only positive integers, then you make a subclass that accepts both positive and negative ints. So the child should work fine in place of the parent, but the child can't delegate back to super in this case.

I thought that made sense, but the info on LSP seems to say the opposite: a precondition can't be strengthened by the child. Both seem to make sense to me, but Liskov only states that the precondition can't be strengthened and the postcondition can't be weakened. Can someone help enlighten me on this?

Dave Schweisguth
  • 36,475
  • 10
  • 98
  • 121
ryoaska
  • 307
  • 5
  • 17

2 Answers2

1

1) Case, when it would ever be appropriate to not call back into super

Usually (but not always) such case means something is wrong with the class hierarchy design.

Not calling the super class implementation doesn't violate the LSP if the child method accepts correct input types and returns correct output type. It only indicates the possibility of the problem.

Here is an absolutely valid example when you don't call the super method:

class Animal

   void eat(Food food)
       // Eat the food

class Cat extends Animal

   void meow()
       // Say meow


class AnimalOwner

   Animal findAPet() 
       return new Animal()

class CatOwner

   // we can return the subclass of Animal here
   Cat findAPet() 
       return new Cat() // We don't need to use the parent implementation

Here the CatOwner::findAPet() returns a Cat (an Animal subclass), this is valid in terms of LSP and we don't call the parent implementation.

Take into account that calling the parent implementation does not guarantee that you can not fall into the same problem as when you don't call it.

Consider this example:

 class Child

    Output doSomething()
        parent = super::doSomething()
        if parent->isNotGood():
           return new OutputSubclass()  // We called super, but we return 
                                        // something different, might be not safe
        else:
           return parent                // We called super and return 
                                        // the same value, safe

2) "Preconditions cannot be strengthened in a subtype". This also means that pre-conditions (expectations related to input parameters) can stay the same or be weakened. In the example you mention, the pre-conditions are actually weakened, so there is no conflict:

Parent::doSomething(PositiveInteger value)  // Only positive integers

Child::doSomething(Integer value)           // Positive or negative integers, 
                                            // pre-condition is weaker 
                                            // (covers wider area of values)

What is not exactly correct is the first sentence: "Subclasses should not be less restrictive than the parent". Subclasses can be less restrictive when we talk about pre-conditions (input parameters) and this is what shown in the example.

Borys Serebrov
  • 15,636
  • 2
  • 38
  • 54
  • But then this overridden method in child class can't delegate back to the parent using super() (to avoid removing parent behavior which is a violation of LSP right?), because the parent doesn't accept negative values. Your point that if you're not calling super it's a sign of something wrong with hierarchy design is what I was thinking, but wouldn't being less restrictive on preconditions stop us from being able to do that in this case? – ryoaska Jan 30 '16 at 09:37
  • @ryoaska Well these things seems to be related. If you return a different type from your child class or accept different parameters, you may need or may not need to call the parent implementation. But even if you don't call super(), it doesn't violate LSP. Actually my example for the 1st question was not good, because I was talking about side effects (creating an archive and backup), but LSP only cares about types on input / output parameters. I replaced the answer to (1) with better example, please check if it is clearer now. – Borys Serebrov Jan 30 '16 at 14:03
  • Does it only govern input/output? I thought it held that any class invariants of the parent should hold when the child is used too. For example, if the AnimalOwner s keep track of an internal value of how many pets he's found that is incremented each time findAPet is called. If CatOwner doesn't delegate back, his counter wouldn't be incremented and the invariant is broken. (and if we don't own AnimalOwner, that functionality could be something that doesn't exist now, but comes in the future). Isn't this why it guards against "removing parent class behavior" – ryoaska Jan 30 '16 at 21:59
  • I do get what you're saying about input/output now though. If someone is trying to use the parent class, and they misuse it by passing values outside of the contract, then an invalid call to super in the case where you substituted the child class is no different than your original invalid call to the parent. This makes sense to me now, thanks! I guess I'm now just stuck on this idea that not calling super() is always at last a potential violation of LSP because it guards you from losing parent behavior now or in the future. – ryoaska Jan 30 '16 at 22:09
  • @ryoaska I mean that if output / input parameters do not follow LSP rules, then subclass can not be used as super class, for sure, 100% violation. And when we talk about the internal implementation, like calling super() or maintaining some invariants - we can't be sure, there may be or may not be a violation. So if you see that subclass doesn't call super(), it's like a hint to check if everything is alright and this is desired or this is a hierarchy design error. Also in some cases even you call super() you can still break the compatibility with parent (see the example in the answer). – Borys Serebrov Jan 30 '16 at 22:22
  • I see what you're saying. So it may be a code smell, or may be perfectly fine, but doesn't violate LSP. Thanks for taking the time to answer all my questions, much appreciated! – ryoaska Jan 30 '16 at 22:29
0

The Liskov substitution principle requires that you can use a subtype when the base type is expected. To do this you must adhere to the contract of the base type. For a base-class B with a method f, precondition Pre, postcondition Post and invariant I, this means that

  • The caller code only guarantees that Pre holds when potentially using your object. If Pre holds, your precondition must also hold. Logically Pre implies the pre-condition of your derived method. This means that you can widen the pre-condition because Pre -> (Pre and condition), as long as there is no contradiction
  • The caller code expects that Post holds when the method is finished. This means that you can add more guarantees, but you shall not weaken the Postcondition.
  • The invariant of your sub-class must be compatible with the base-class' invariant.

I would consider explicitly calling base-class implementation a code smell (except for constructors). It is much better to use the te,plate method pattern(or non-virtual interface in C++) to enforce the base-class contract. In Python, this looks like:

class Base:
   def publicMethod(self, x):
       // do something
       self.templateMethod(x)
       // do something else

   def templateMethod(self, x):
       // has to be overriden by sub-classes
       // it provides extension hooks, but
       // publicMethod ensures that the overall
       // functionality is correct
Jens
  • 9,058
  • 2
  • 26
  • 43