更好的设计以避免违反里氏替换原则

mks*_*rge 6 python design-patterns liskov-substitution-principle python-3.x python-typing

我遇到了里氏替换原则的问题,并且不太确定解决它的最佳方法是什么。

有问题的代码

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...
Run Code Online (Sandbox Code Playgroud)

我很清楚为什么这不是一个很好的代码,以及为什么它违反了里氏替换原则。我想知道如何更好地设计我的系统以避免这个问题。

从根本上讲,我有一个BaseModel类似于接口的类,提供一些run扩展类必须实现的方法。但是扩展类还处理特定的输入/输出,它们也是基本输入/输出类的扩展(继承SpecificModelInputBaseModelInput添加一些字段和功能,与输出相同)

这里更好的方法是什么?

jsb*_*eno 1

正如我在评论中所述:如果您确实可以将输入和输出限制为基本输入和基本输出的子类型,那么我认为该模型没有任何问题

如果违反你的意思是在限制输入选项时,子类不能再在基类可以使用的任何地方使用,我想说这是你将里氏替换原则作为教条的情况,这应该是非常 好的 建议

看,里氏原则是一个很好的指导方针,而且有点简单化,因为它会让你对继承应该是什么有一个很好的感觉。

但在现实世界中,限制输入参数(和属性类型)并不是所有情况下都需要考虑的问题。不管怎样,有一个具体的例子对我们很有帮助:想象一个带有“board”方法的抽象“Vehicle”类,它允许你登上“Transportables”——具体的“Transportables”和一些操作系统“Transportable”允许的子类是人、狗、杂货袋、钢琴和大象。如果您的“车辆”类别是“船舶” - 它可以容纳所有这些。如果您的车辆类别是汽车,则其中一些已不再适用。

这似乎是你的用例。

所以,当然,你在这里违反了原则。大多数地方解释它的措辞是“如果在超类出现的任何地方替换子类的实例,程序不应该中断”。因此,最正确的做法是修改超类契约,使任何输入都可能有效或无效,并允许它引发运行时异常(或返回一个表示错误的对象),即使输入有效。

每个调用该方法的人都应该处理“不起作用”状态 - 通过上面的示例,很容易看出,如果我在一个不相关的类中有一个调用的方法vehicle.board,它应该负责看到它没有试图放置大象车内。如果在任何地方调用该方法都进行这些检查,则原则成立!

如果工程设计对于您所执行的任何任务来说都太过分了,那么在这种情况下我会说“......实用性胜过纯粹性”,并且只需设置注释即可使静态类型检查器保持沉默。

当然,告诉静态类型检查器是另一回事 - 我认为使用注释中链接的答案中指出的泛型可以做到:当参数可以有时,如何注释抽象方法的参数的类型从特定基类型派生的任何类型?