我将使用以下示例来说明我的问题:
class Attribute {}
class SimpleAttribute extends Attribute {}
abstract class AbstractFactory {
abstract public function update(Attribute $attr, $data);
}
class SimpleFactory extends AbstractFactory {
public function update(SimpleAttribute $attr, $data);
}
如果你试图运行它,PHP会抛出一个致命的错误,说Declaration of SimpleFactory::update() must be compatible with that of AbstractFactory::update()
我完全理解这意味着什么:SimpleFactory::update()
方法签名必须与其父抽象类完全匹配。
但是,我的问题是:有没有办法允许具体方法(在这种情况下,SimpleFactory::update()
)将类型提示重新定义为原始提示的有效后代?
一个示例是instanceof
运算符,在以下情况下将返回true:
SimpleAttribute instanceof Attribute // => true
我确实认识到,作为一种解决方法,我可以在具体方法中使类型提示相同,并在方法体本身中进行实例检查,但有没有办法在签名级别简单地强制执行此操作?
答案 0 :(得分:19)
我不希望如此,因为它可以打破类型暗示合同。假设一个函数foo
接受了一个AbstractFactory并传递了一个SimpleFactory。
function foo(AbstractFactory $maker) {
$attr = new Attribute();
$maker->update($attr, 42);
}
...
$packager=new SimpleFactory();
foo($packager);
foo
调用update
并将属性传递给工厂,因为AbstractFactory::update
方法签名承诺它可以获取属性。巴姆! SimpleFactory有一个无法正确处理的类型的对象。
class Attribute {}
class SimpleAttribute extends Attribute {
public function spin() {...}
}
class SimpleFactory extends AbstractFactory {
public function update(SimpleAttribute $attr, $data) {
$attr->spin(); // This will fail when called from foo()
}
}
在合同术语中,后代类必须遵守其祖先的合同,这意味着功能参数可以更加基础/更少指定/提供更弱的合同,并且返回值可以更多地得出/更明确/提供更强的合同。在“An Eiffel Tutorial: Inheritance and Contracts”中描述了艾菲尔(可以说是最受欢迎的合同设计语言)的原则。类型的弱化和强化分别是contravariance and covariance的例子。
从理论上讲,这是LSP违规的一个例子。不,不是 LSP; Liskov Substitution Principle,表示子类型的对象可以替换超类型的对象。 SimpleFactory
是AbstractFactory
的子类型,foo
需要AbstractFactory
。因此,根据LSP,foo
应该采用SimpleFactory
。这样做会导致“调用未定义的方法”致命错误,这意味着LSP已被违反。
答案 1 :(得分:4)
在回答OP时,接受的答案是正确的,即他试图做的事违反了Liskov替代原则。 OP应该使用新的接口并使用组合而不是继承来解决他的功能签名问题。 OP示例问题的变化相当小。
class Attribute {}
class SimpleAttribute extends Attribute {}
abstract class AbstractFactory {
abstract public function update(Attribute $attr, $data);
}
interface ISimpleFactory {
function update(SimpleAttribute $attr, $data);
}
class SimpleFactory implements ISimpleFactory {
private $factory;
public function __construct(AbstractFactory $factory)
{
$this->factory = $factory;
}
public function update(SimpleAttribute $attr, $data)
{
$this->factory->update($attr, $data);
}
}
上面的代码示例做了两件事:1)创建ISimpleFactory接口,所有依赖于SimpleAttributes工厂的代码都将对其进行编码2)使用通用工厂实现SimpleFactory将需要SimpleFactory通过构造函数获取AbstractFactory的实例它将在SimpleFactory中的ISimpleFactory接口方法覆盖的更新函数中使用的派生类。
这允许从依赖于ISimpleFactory的任何代码封装对泛型工厂的任何依赖,但允许SimpleFactory替换从AbstractFactory(Satisfying LSP)派生的任何工厂,而不必更改其任何代码(调用代码将提供依赖)。 ISimpleFactory的新派生类可能决定不使用任何AbstractFactory派生实例来实现自身,并且所有调用代码都将被屏蔽掉。
继承可能具有很大的价值,但是,有时候组合被忽略了,这是我减少紧耦合的首选方法。
答案 2 :(得分:1)
自PHP 7.4起,允许通过使用接口在子类中使用更具体的类型提示
interface OtherAttributeCompatibleInterface{};
interface SimpleAttributeCompatibleInterface{};
interface AllAttributesInterface extends OtherAttributeCompatibleInterface, SimpleAttributeCompatibleInterface{};
class Attribute implements AllAttributesInterface{};
class SimpleAttribute extends Attribute implements SimpleAttributeCompatibleInterface{};
abstract class AbstractFactory {
abstract public function update(AllAttributesInterface $n);
}
class SimpleFactory extends AbstractFactory {
public function update (SimpleAttributeCompatibleInterface $n){
}
}
由于AllAttributesInterface继承了SimpleAttributeCompatibleInterface,允许SimpleFactory从AbstractFactory覆盖更新方法。