Better design in order to avoid violating Liskov Substitution principle

Viewed 467

I am running into an issue with Liskov Substitution Principle and am not quite sure what would be the best way to go around it.

Code in question

class BaseModel:
    def run(self, base_model_input: BaseModelInput) -> BaseModelOutput:
        """Throws NotImplemented or @abstractmethod"""
        pass

class SpecificModel(BaseModel):
    def run(self, specific_input: SpecificModelInput) -> SpecificModelOutput:
        # do things...

I understand well why is this not a great code, and why it violates the Liskov Substitution Principle. I am wondering how to design my system better to avoid this problem in the first place.

Fundamentally I have a BaseModel class that acts like an interface, providing some methods like run that the extending classes must implement. But extending classes also deal with specific input/output, that are also extensions of the base input/output classes (that is SpecificModelInput inherits from BaseModelInput and adds some fields and functionality, same with output)

What would be a better approach here?

1 Answers

As I stated in the comments:If you can really constrain the inputs and outputs to subtypes of a base-input and base-output, I don't see any problem with this model

If by violation you mean that on restricting the input options, the subclass can no longer b used everywhere the baseclass can, I'd say it is a case where you are taking the Liskov Substitution principle as dogma where it should be very good advice.

See, the Liskov..principle is a good guideline, and a bit simplistic, as it will give you a really good feeling of what inheritance should be.

But in real-world terms, restricting input parameters (and attribute types) is not a concern in all cases. Anyway, having a concrete example will serve us well: think of an abstract "Vehicle" class with a "board" method which allows you to board "Transportables" - and concrete "Transportables" and some os the allowable subclasses of "Transportable" are Persons, Dogs, Grocery Bag, Pianos, and Elephants. If your "Vehicle" class is a "Ship" - it can take all of those. If your vehicle class is a Car, some of those are out.

That seems to be your use case.

So, of course, you are violating the principle here. The wording in most places explaining it is that "if you substitute an instance of the subclass anywhere the superclass appears, the program should not break". So, the most correct thing to do, is to modify the superclass contracts in a way that any input may or may not work, and allow it to raise a runtime exception (or return an object signalizing an error) even if the input is valid.

Everyone calling that method should handle the "did not work" state - with the example above it is easy to see that if I have a method in an unrelated class that calls vehicle.board, it should be responsible to see it is not trying to put an Elephant inside a car. If everywhere the method is called these checks are made, the principle holds!

If engineering this is overkill for whatever task you have, I'd say "...practicality beats purity" in this case, and simply set the annotations to silence the static type checker.

Of course, telling that to the static type checker is another thing - I think the use of Generics as pointed in the answer linked in the comments could do: How do I annotate the type of a parameter of an abstractmethod, when the parameter can have any type derived from a specific base type?

Related