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

jas*_*son 29 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);

}
Run Code Online (Sandbox Code Playgroud)

如果你试图运行它,PHP会抛出一个致命的错误,说是 Declaration of SimpleFactory::update() must be compatible with that of AbstractFactory::update()

我完全理解这意味着什么:SimpleFactory::update()s方法签名必须与其父抽象类完全匹配.

但是,我的问题是:有没有办法允许具体方法(在这种情况下SimpleFactory::update())将类型提示重新定义为原始提示的有效后代?

一个例子是instanceof运算符,它将在以下情况下返回true:

SimpleAttribute instanceof Attribute // => true
Run Code Online (Sandbox Code Playgroud)

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

out*_*tis 20

我不希望如此,因为它可以打破类型暗示合同.假设一个函数foo接受了一个AbstractFactory并传递了一个SimpleFactory.

function foo(AbstractFactory $maker) {
    $attr = new Attribute();
    $maker->update($attr, 42);
}
...
$packager=new SimpleFactory();
foo($packager);
Run Code Online (Sandbox Code Playgroud)

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

在合同术语中,后代类必须遵守其祖先的合同,这意味着功能参数可以更加基础/更少指定/提供更弱的合同,并且返回值可以更多地得出/更明确/提供更强的合同.在" 艾菲尔铁塔教程:继承与契约 " 中,埃菲尔(可以说是最受欢迎的合同设计语言)描述了这一原则.类型的弱化和强化分别是逆变和协方差的例子.

从理论上讲,这是LSP违规的一个例子.不,不是 LSP ; 在里氏替换原则,其中指出一个子类型的对象可以被取代的超类型的对象.SimpleFactory是一个子类型AbstractFactory,并foo采取AbstractFactory.因此,根据LSP,foo应该采取SimpleFactory.这样做会导致"调用未定义的方法"致命错误,这意味着LSP已被违反.


The*_*het 5

接受的答案正确地回答了OP,他试图做的事情违反了里氏替换原则。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);
   }

}
Run Code Online (Sandbox Code Playgroud)

上面的代码示例做了两件事: 1) 创建 ISimpleFactory 接口,所有依赖于 SimpleAttributes 工厂的代码都将针对该接口进行编码 2) 使用通用工厂实现 SimpleFactory 需要 SimpleFactory 通过构造函数获取 AbstractFactory 派生类的实例然后,它将在 SimpleFactory 中的 ISimpleFactory 接口方法重写的更新函数中使用。

这允许从依赖 ISimpleFactory 的任何代码中封装对通用工厂的任何依赖项,但允许 SimpleFactory 替换从 AbstractFactory 派生的任何工厂(满足 LSP),而无需更改其任何代码(调用代码将提供依赖项) 。ISimpleFactory 的新派生类可能决定不使用任何 AbstractFactory 派生实例来实现自身,并且所有调用代码都将免受该细节的影响。

继承可能具有很大的价值,但是,有时组合会被忽视,这是我减少紧密耦合的首选方式。