Что я неправильно понимаю в принципе замены Лискова
Я думал, что понимаю 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]
Что я делаю не так? Я думаю, что наиболее полезным будет пример того, как может быть передано значение, которое нарушает предположения, которые код делает относительно этого значения. Предполагая, что понятно, чего я пытаюсь достичь, пожалуйста, также добавьте код о том, как это сделать правильно.
3 ответа
Что я делаю не так? Я думаю, что наиболее полезным будет пример того, как может быть передано значение, которое нарушает предположения, которые код делает относительно этого значения.
Вы не неправильно поняли LSP, допуская больший тип в подклассе, не нарушает принцип; на самом деле то, что вы описали, называется контравариантными типами аргументов методов, что не поддерживается в PHP ни сложностями проектирования, ни сложностями реализации.
Лично моя единственная оговорка об этом подходе состоит в том, что обработка более слабого типа в вашем подклассе скрыта суперклассом.
Предполагая, что понятно, чего я пытаюсь достичь, пожалуйста, также добавьте код о том, как это сделать правильно.
Типы аргументов метода в PHP являются инвариантными, то есть тип для каждого аргумента (если указан в родительском) должен точно совпадать при переопределении метода. Хотя это поведение обсуждалось в прошлом и вновь обсуждалось недавно с введением хинтинга возвращаемого типа, оно не гарантировано будет доступно даже в следующей основной версии.
Чтобы преодолеть это, вы в настоящее время вынуждены заставить и примитивный, и производный типы реализовывать один и тот же интерфейс, а затем использовать его в родительских и дочерних классах, как описано в этом ответе.
Вы должны соблюдать Правило подписи, которое гласит, что подтип должен иметь все методы супертипа, а сигнатуры подтипа должны быть совместимы с сигнатурами переопределенных методов. Итак, вы объявляете класс B, чей метод foo() несовместим с foo() класса A, поскольку вы используете PrimitiveValue, который является супертипом StringValue. Это делает невозможным гарантировать, что каждый экземпляр класса 'A' будет заменен экземпляром класса B
Это не так, как работает 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
Тем не менее он все еще сломался во время казни.