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

php
2022-08-30 14:01:35

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

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()

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

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

例如,运算符在以下情况下将返回 true:instanceof

SimpleAttribute instanceof Attribute // => true

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


答案 1

我不会这么认为,因为它可以打破类型暗示合同。假设一个函数采用了一个 AbstractFactory,并被传递给了一个 SimpleFactory。foo

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

foo调用一个 Attribute 并将其传递给工厂,它应该采用该属性,因为方法签名承诺它可以采用 Attribute。砰!SimpleFactory 有一个无法正确处理的对象。updateAbstractFactory::update

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()
    }
}

在契约术语中,后代类必须遵守其祖先的契约,这意味着函数参数可以获得更多的基础/更少指定/提供较弱的协定,返回值可以更派生/更指定/提供更强的协定。埃菲尔铁塔(可以说是最流行的合同设计语言)在“埃菲尔铁塔教程:继承和合同”中描述了这一原理。类型的弱化和强化分别是逆变和协方差的示例。

从更理论的角度来看,这是 LSP 违规的一个示例。不,不是那个LSP;里氏替换原理,该原理指出子类型的对象可以替代超类型的对象。 是 的子类型,并采用 .因此,根据 LSP,应采取 .这样做会导致“调用未定义的方法”致命错误,这意味着已违反 LSP。SimpleFactoryAbstractFactoryfooAbstractFactoryfooSimpleFactory


答案 2

接受的答案是正确的,回答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);
   }

}

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

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

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


推荐