0

I have a abstract class named MotorizedVehicle that contains an implemented gas- and brake-function. I want to make a Truck class that extends this class and uses gas exactly in the same way as MotorizedVehicle, but with the limitation that it can't move if it's ramp is lowered.

My thought is that I am then restricting the behaviour of gas, which seems to me to go against the Liskov substitution principle? That is to say, is this bad OOP design?

Possible solutions (in my head) would be to delegate this behaviour to maybe an engine class and make Truck and MotorizedVehicle implement a common interface instead?

TLDR; Is it acceptable to override a method with an additional limiting factor if following the Liskov substitution principle?

1
  • It might be useful to add some information about the context. What sort of application or library are the classes part of? What will people use it for? And what would these particular classes be used for?
    – bdsl
    Nov 28 '20 at 11:56
3

Depends on the (conceptual) contract of the MotorizedVehicle.gas().

If you define gas() as "will accelerate the vehicle", then you would be violating this contract by not accelerating.

You can however just define gas() as "will accelerate the vehicle, unless the vehicle is in some state that prevents this, in which case it will throw an exception" (or something like that). In this case you don't limit anything, it is already defined that way.

So in general no, you must always offer at least the functionality defined by your superclass. However if you control both ends, sidestepping LSP is sometimes as easy as reformulating the superclass' guarantees.

2
  • 1
    And then you get contracts like "May be callable, and might do something when called. Specifics depend on the quantum state of the universe." ;-) Nov 27 '20 at 17:14
  • @Deduplicator Hm.. sounds like some of the Java Enterprise APIs I'm forced to use at work. :) "This will work, if both the caller and callee are correctly annotated, you configured some xml somewhere correctly and you're making the call from the right context. Otherwise it will just do nothing and there will be no errors anywhere." :) Nov 27 '20 at 19:32
3

If you just have a MotorizedVehicle pointer and don't know if it's a Truck or not, and you call gas and it doesn't work, is that going to be a problem? Or is it only not a problem if you know you have a Truck? If you have to know it's a Truck in order to use it properly, it's not an appropriate use of inheritance.

In other words, how the caller is expected to handle failures is part of the contract. If that's not the same for every MotorizedVehicle instance, the function doesn't belong there.

1

Acceptable is subjective.  Here, it depends on expectations set by the abstract class and its methods, but you haven't offered any.

Can hitting the gas cause an other than expected situation, as it can in real life, e.g. what if the vehicle's engine is off?

LSP tells us that existing expectations made by the rest of the code in a working program should continue to be met when introducing a new subclass and substituting an instance of that for instances of classes that are already existing.  In your case this would depend on whether the rest of program assumes gas function accelerates, and while that is not realistic of real life, we can't say about your program.

It would be hard to be in violation of LSP if there is yet no program having behavioral expectations of the abstract class.  If you write the program already knowing that there are limitations on using the gas, then there's no violation.

The question might better be how are the limitations manifest: throwing an exception, vs. returning an error code, vs. requiring the consumer to ask the instance what its (new) state is.  A violation of LSP would be throwing an exception or quitting the program when the pre-existing program wasn't expecting that due to documentation of the abstract class's gas method not calling that out.

2
  • 1
    "substituting an instance of that for instances of classes that are already existing" is a pretty strict standard. Even substituting ArrayList for LinkedList could break things. Is this rule actually useful?
    – user253751
    Nov 27 '20 at 17:20
  • @user253751, I suppose that I think it is reasonable in the context of an already working program, which implies that there is a program that has taken documented behavioral expectations into account.
    – Erik Eidt
    Nov 27 '20 at 23:56
1

It is hard to tell with such a simplified example, but I believe you may be violating the History Constraint, which is arguably the most important of the constraints imposed by the Liskov Substitution Principle, since it is the one that is actually novel (the other stuff about co- and contravariance, pre- and posconditions, and invariants was already known before hand, but the History Constraint is the one that is new, and is the one that makes the LSP applicable to languages with shared mutable state and aliasing, instead of only purely functional languages).

The History Constraint says that you should not be able to construct a history of the object using the interface of the subtype that you could not also construct using the interface of the super type.

However, using your types, I can construct a history using the Truck type where the ramp is down and the vehicle cannot start, which is a history that cannot be constructed using the interface of MotorizedVehicle.

1

Just switch to delegation:

let vehicle = MotorizedVehicle()
let truck = Truck(delegate: vehicle)
truck.gas()

final class Truck {
  let delegate: Vehicle

  init(delegate: Vehicle) {
    self.delegate = delegate
  }

  func gas() {
    if ramp.isLowered() {
      delegate.gas()
    } else {
      throwError()
    }
  }

}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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