我对 Liskov 替换原则有什么误解

What am I misunderstanding about the Liskov Substitution Principle

我以为我了解 LSP,但看来我完全错了。我有以下 classes:

class PrimitiveValue {
}

class StringValue extends PrimitiveValue {
}

class A {
    public function foo(StringValue $value) {
    }
}

class B extends A {
    public function foo(PrimitiveValue $value) {
    }
}

注意基classA接受subclassStringValue作为参数类型,subclassB接受父级 class 原始值。我这么说是因为到处都有人问类似的问题,只是类型相反。

根据我的理解,class B 中的方法 foo() 接受父方法接受的任何内容,再加上更多,因为它接受基类型 PrimitiveValue。因此,任何只看到 class A 的调用者都会传递 B 可以处理的值,而不违反 LSP。知道它是 B 的调用者可以做出更强的假设,并且可以自由传递其他 PrimitiveValue 子类型。

然而,当我执行代码时,出现错误:

严格 (2048):B::foo() 的声明应与 A::foo(StringValue $value) [APP/Controller/TestLocalController.php,第 17 行兼容]

我哪里错了?我认为最有帮助的是一个示例,说明如何传递违反代码对该值所做假设的值。假设很清楚我要实现的目标,还请添加有关如何正确执行此操作的代码。

这不是 PHP OOP 的工作方式。您可以使用接口来完成,例如:

<?php

interface CommonInterface {
}

class PrimitiveValue implements CommonInterface {
}

class StringValue extends PrimitiveValue implements CommonInterface {
}

class A {
    public function foo(CommonInterface $value) {
    }
}

class B extends A {
    public function foo(CommonInterface $value) {
    }
}

是的,考虑到您所展示的片段,在任何情况下您都不会因类型不兼容而产生错误。但是,谁说这将成为最终的 class 结构?你以后可能会和PrimitiveValue离婚StringValue。方法签名必须兼容 "by itself",以避免将来更改看似无关的代码时出现此类故障。只看签名本身,它们 显然不兼容。 只有了解其他两个 classes 的额外知识才能将签名 视为 兼容。交叉耦合太多了;那是 "compatibility by proxy"。类型比这更松散。类型提示中的类型名称不推断实现。您的 "compatibility" 仅源于您的类型的当前实现,而非类型签名本身。

您的签名不同,因此不兼容。您不能对这些签名当前或未来的兼容性做出任何保证。没有任何地方正式规定 StringValuePrimitiveValue 有任何兼容关系。您可能 a 当前 实现 是这样说的,但是类型签名无法知道或依赖它。

作为一个实际的例子,这是可行的:

require 'my_types.php'; // imports StringValue

(new B)->foo(new StringValue);

这不是:

require 'my_alternative_type_implementations.php'; // imports StringValue

(new B)->foo(new StringValue);  // fatal error: expected PrimitiveValue

B 中的类型签名没有任何变化,但它在执行过程中仍然崩溃。

What am I getting wrong? I think most helpful would be an example of how a value can be passed that violates the assumptions the code makes about that value.

你没有误解LSP,在子类方法中允许更大的类型并不违反原则;事实上,您所描述的是 contravariant method argument types,由于设计或实现困难,PHP 不支持它。

就个人而言,我对这种方法的唯一保留意见是子类中较弱类型的处理被超类隐藏了。

Assuming that it is clear what I'm trying to achieve, please also add code on how to do it correctly.

PHP 中的方法参数类型是不变的,即每个参数的类型(如果在父级中提供)在覆盖方法时必须完全匹配。虽然此行为已在 past and brought up again recently with the introduction of return type hinting 中讨论,但不能保证即使在下一个主要版本中也可用。

为了克服这个问题,您目前被迫让原始类型和派生类型都实现相同的接口,然后在父类和子类中使用该接口 类,如 所述。