有没有办法在扩展抽象类时将类型提示重新定义为后代类?

时间:2009-12-13 21:22:02

标签: php

我将使用以下示例来说明我的问题:

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

我确实认识到,作为一种解决方法,我可以在具体方法中使类型提示相同,并在方法体本身中进行实例检查,但有没有办法在签名级别简单地强制执行此操作?

3 个答案:

答案 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,表示子类型的对象可以替换超类型的对象。 SimpleFactoryAbstractFactory的子类型,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覆盖更新方法。