Для иллюстрации моего вопроса я буду использовать следующий пример:
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()
s должна точно совпадать с сигнатурой его родительского абстрактного класса.
Однако мой вопрос: есть ли способ разрешить конкретный метод (в данном случае SimpleFactory::update()
) переопределить подсказку типа действительному потомку исходного подсказки?
Примером может служить оператор instanceof
, который вернет true в следующем случае:
SimpleAttribute instanceof Attribute // => true
Я действительно понимаю, что, работая, я мог бы сделать тип намека таким же в конкретном методе, и сделать экземпляр проверки в самом тесте метода, но есть ли способ просто обеспечить это на уровне подписи?
Я бы не ожидал этого, так как он может разорвать типы намекающих контрактов. Предположим, что функция 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() } }
В терминологии контракта классы потомков должны соблюдать контракты своих предков, что означает, что функциональные параметры могут быть более базальными / менее определенными / предлагать более слабый контракт, а возвращаемые значения могут быть более выведены / уточнены / предлагают более сильный контракт. Принцип описан для Eiffel (возможно, самый популярный язык дизайна по контракту) в « Эйфелевом учебнике: наследование и контракты ». Ослабление и усиление типов являются примерами контравариантности и ковариации , соответственно.
В более теоретических терминах это пример нарушения LSP. Нет, не то , что LSP ; Принцип замещения Лискова , в котором говорится, что объекты подтипа могут быть заменены объектами супертипа. SimpleFactory
является подтипом AbstractFactory
, а foo
принимает AbstractFactory
. Таким образом, согласно LSP, foo
должен принимать SimpleFactory
. Это приводит к фатальной ошибке «Call to undefined method», что означает, что LSP был нарушен.
Принятый ответ правилен в ответе на ОП, что то, что он пытался сделать, нарушает Принцип замещения Лискова. 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, который он затем будет использоваться в функции обновления из переопределения метода интерфейса ISimpleFactory в SimpleFactory.
Это позволяет любой зависимости от родового завода быть инкапсулированным из любого кода, который зависит от ISimpleFactory, но позволил SimpleFactory заменить любой завод, полученный из AbstractFactory (удовлетворительный LSP), без необходимости изменять какой-либо его код (вызывающий код обеспечивал бы зависимость) , Новый производный класс ISimpleFactory может решить не использовать какие-либо экземпляры, созданные для AbstractFactory, для реализации самого себя, и весь код вызова будет защищен от этой детали.
Наследование может иметь большое значение, однако иногда композиция упускается из виду и ее предпочтительный способ уменьшения жесткой связи.