Question

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?

Était-ce utile?

La solution

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.

Autres conseils

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.

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.

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.

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

}
Licencié sous: CC-BY-SA avec attribution
scroll top