Я думал, что понимаю LSP, но, похоже, я совершенно не прав. У меня есть следующие классы:
class PrimitiveValue {
}
class StringValue extends PrimitiveValue {
}
class A {
public function foo(StringValue $value) {
}
}
class B extends A {
public function foo(PrimitiveValue $value) {
}
}
Обратите внимание, что базовый класс A принимает подкласс StringValue в качестве типа параметра, а подкласс B принимает родительский класс PrimitiveValue. Я говорю это потому, что похожий вопрос, только с обращенными типами, задают везде.
Согласно моему пониманию, метод foo () в классе B принимает все, что принимает родительский метод, плюс еще, так как он принимает базовый тип PrimitiveValue. Таким образом, любой вызывающий объект, который просто видит класс A, передает значения, которые могут быть обработаны B, не нарушая LSP. Вызывающие абоненты, которые знают, что это B, могут делать более сильные предположения и могут свободно передавать другие подтипы PrimitiveValue.
Тем не менее, когда я выполняю код, я получаю ошибку:
Строгое (2048): объявление B :: foo () должно быть совместимо с A :: foo (StringValue $ value) [APP / Controller / TestLocalController.php, строка 17]
Что я делаю не так? Я думаю, что наиболее полезным будет пример того, как может быть передано значение, которое нарушает предположения, которые код делает относительно этого значения. Предполагая, что понятно, чего я пытаюсь достичь, пожалуйста, также добавьте код о том, как это сделать правильно.
Что я делаю не так?
Я думаю, что наиболее полезным будет пример того, как может быть передано значение, которое нарушает предположения, которые код делает относительно этого значения.
Вы не неправильно поняли LSP, допуская больший тип в подклассе, не нарушает принцип; на самом деле то, что вы описали, известно как типы аргументов контравариантного метода, что-то, что не поддерживается в PHP ни конструкцией, ни трудностями реализации.
Лично моя единственная оговорка об этом подходе заключается в том, что обработка более слабого типа в вашем подклассе скрыта суперклассом.
Предполагая, что понятно, чего я пытаюсь достичь, пожалуйста, также добавьте код о том, как это сделать правильно.
Типы аргументов метода в PHP являются инвариантными, то есть тип для каждого аргумента (если указан в родительском) должен точно совпадать при переопределении метода. Хотя это поведение обсуждалось в прошлое и снова воспитан относительно недавно с введением тип подсказки, он не гарантированно будет доступен даже в следующей основной версии.
Чтобы преодолеть это, вы в настоящее время вынуждены заставить и примитивные, и производные типы реализовывать один и тот же интерфейс, а затем использовать его в родительских и дочерних классах, как описано в этот ответ.
Это не так, как работает PHP ООП. Вы можете сделать это с помощью интерфейсов, как таковых:
<?php
interface CommonInterface {
}
class PrimitiveValue implements CommonInterface {
}
class StringValue extends PrimitiveValue implements CommonInterface {
}
class A {
public function foo(CommonInterface $value) {
}
}
class B extends A {
public function foo(CommonInterface $value) {
}
}
Да, учитывая фрагменты, которые вы показали, нет ситуации, в которой вы могли бы создать ошибку, связанную с несовместимыми типами. Однако, кто сказал, что это будет окончательная структура класса? Вы можете развестись StringValue
от PrimitiveValue
в будущем. Сигнатура метода должна быть совместима «сама по себе», чтобы избежать такого будущего сбоя, если вы измените, казалось бы, не связанный код. Просто глядя на сами подписи, они очевидно несовместимый. Только с дополнительными знаниями двух других классов могут подписи считаться совместимы. Это слишком много перекрестных связей; это «совместимость по доверенности». Типы более слабо связаны, чем это. Имя типа в подсказке типа не подразумевает реализацию. Ваша «совместимость» зависит только от текущей реализации ваших типов, но не от самих сигнатур типов.
Ваши подписи разные, поэтому несовместим. Вы не можете дать никаких гарантий относительно текущей или будущей совместимости этих подписей. Нигде официально не указано, что StringValue
а также PrimitiveValue
иметь какие-либо совместимые отношения. Ты можешь иметь ток реализация это говорит о том, что подпись типа не может этого знать или полагаться на это.
В качестве практического примера это работает:
require 'my_types.php'; // imports StringValue
(new B)->foo(new StringValue);
Это не:
require 'my_alternative_type_implementations.php'; // imports StringValue
(new B)->foo(new StringValue); // fatal error: expected PrimitiveValue
Ничего не изменилось в типе подписей в B
Тем не менее, он все еще сломался во время казни.