Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

Коваріантність в PHP

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Шановні форумчани, 100-відсотково впевнений, що є люди, які працюють з php. Можливо хтось підкине якусь хорошу ідею. Є наступний код:

interface IPayload {
    public function toArray(): array;
}



interface IPayloadFactory {
    public function create(): IPayload;
}


class Payload implements IPayload {
    public function toArray(): array {
        return ['hello' => 'world'];
    }
}


class ChildPayload extends Payload {
    public function setVar(string $var)  {
        $this->var = $var;
    }
}


interface IChildPayloadFactory extends IPayloadFactory {
    public function create(): ChildPayload;
}


class ChildPAyloadFactory implements IChildPayloadFactory {
    public function create(): ChildPayload  {
        return new ChildPayload();
    }
}


$factory = new ChildPAyloadFactory();

var_dump($factory->create());

як результат отримуємо:

Fatal error: Declaration of IChildPayloadFactory::create(): ChildPayload must be compatible with IPayloadFactory::create(): IPayload in /implode.io/script.php on line 34

Наскільки я розумію, це вроді із серії про коваріантність, якої в php, наразі, не має. Може комусь приходилось обходити цю проблему і хтось може поділитись досвідом. Буду вдячний.

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

К сожалению, RFC не позволяет ковариантность в возвращаемых типах при наследовании. Вот выдержка из этого самого RFC:

The enforcement of the declared return type during inheritance is invariant; this means that when a sub-type overrides a parent method then the return type of the child must exactly match the parent and may not be omitted. If the parent does not declare a return type then the child is allowed to declare one.
А вот ссылка на сам RFC: wiki.php.net/rfc/return_types
Но они предлагают интересный вариант решения:
interface A {  
    static function make(): A;  
}
class B implements A {
    static function make(): A {
        return new B();
    }
}
Т.е. в самом методе возвращаемый тип ты указываешь как в интерфейсе, а вот возвращаешь «расширенный» тип.

Тут проблема в тому, що якщо Ти в виконаєш метод make() класу B, який повертає інтерфейс А, то далі згідно контракту, ти зможеш з ним працювати виключно, як з обєктом А, а решту функціоналу — це буде як чорний ящик, і використання методів класу В — це вже порушення контракту, що не фантан...

Не совсем так. Объект, который вернется из класса В будет удовлетворять контракту интерфейса А, но при этом будет иметь расширенные возможности класса В. Что и есть полиморфизм.
В твоем случае я не совсем понимаю какой вообще смысл в interface IPayloadFactory если ты его вообще не используешь и если ты не хочешь использовать полиморфизм, то зачем вся эта архитектура с наследованием?

IPayloadFactory закидаєтся як ключ для dependency injection a PayloadFactory, що реалізує цей інтерфейс, буде значенням в DI...

Ну тогда тот вариант что ты выбрал (убрав возвращаемый тип в IPayloadFactory.create()) смотрится как самый логичный, учитывая особенности PHP. Хотя, я не вижу особой разницы если использовать подход, предлагаемый в RFC (обозначить тип как Payload и возвращать нужный ChildPayload). Все равно ведь при твоем подходе ты будешь инжектить переменную с типом Payload и соответственно никакого выигрыша от IChildPayload я не вижу.

Так это проблема только если у вас AnemicDomainModel и если вы используете «процедуры завёрнутые в обьекты» вместо ООП

поки-що, для себе вибрав такий варіант:

interface IPayload { 
    public function toArray(): array;
}

interface IPayloadFactory {
    public function create();
}

class Payload implements IPayload {
    public function toArray(): array {
        return ['hello' => 'world'];
    }
}

class ChildPayload extends Payload {
    public function setVar(string $var)  {
        $this->var = $var;
    }
}

interface IChildPayloadFactory extends IPayloadFactory {
    public function create(): ChildPayload;
}

class ChildPayloadFactory implements IChildPayloadFactory {
    public function create(): ChildPayload  {
        return new ChildPayload();
    }
}

$factory = new ChildPAyloadFactory();

var_dump($factory->create());

В IPayloadFactory прибрав тип, який повинен повертати метод create(), а в IChildPayloadFactory вже вказую той тип, який потрібно...

А що не так? Розумієш, є таке поняття «сигнатура метода». Тобто
function create(): ChildPayload;
function create(): IPayload;
це різні речі

Звикни до того, що Інтерфейс — це в багатьох випадках качина типізація. Якщо тобі сказали «потрібна качка», то ти повинен принести качку, а не Anas platyrhynchos f. domestica, доки ти не назвеш це качкою — воно не качка. І якщо саму качку калькулятор із легкістю типізує, то он «функція що повертає качку» має повертати саме качку, бо сама фраз «повертає качку» забита як сигнатура метода.

На етапі компіляції то ще може бути вирішено. Але PHP ще не в тій стадії, щоб робити компіляцію в файл готового коду (хоча я не все знаю, може в кеш вже й навчилися то робити). А для транслятора вважається складною процедурою (жертиме ресурс) робити складні неявні перетворення щоразу, коли хтось запустив один малесенький скриптик.

Якщо коротко: теоретично то можливо зробити, і колись PHP тому навчать. Але ти практик, тобі потрібно «тут і зараз», а це означає вивчи як воно працює зараз та роби як воно хоче.
В твоєму випадку створи обидва методи, якщо не знадобиться — потім приб′єш.

Тут не питання PHP, тут питання філософії.. якщо мені потрібно повернути качку, то хіба буде погано, якщо я поверну качку з яблуками, скажімо... від того качка меньшою качкою не буде.. хоча так тут я погоджуюсь, тут більше проблема в обмеженнях самого PHP...

Тут не питання PHP, тут питання філософії.. якщо мені потрібно повернути качку, то хіба буде погано, якщо я поверну качку з яблуками, скажімо

В большом проекте сели архитекты разных подсистем, набросали дизайны, согласовали интерфейсы — и понеслась. Работают команды, пишут реализации...
А потом оказывается, что у одной команды, вместо уток содаются и возвращаются «серые утки» (или гуси). А по интерфейсу должны быть утки, которых ожидают на входе в другую подсистему — т.к. об этом уже договорились ранее.
Такая вот философия.

Сіра качка, це є та сама качка, просто ще сірого кольору, вона по суті не може бути гускою..

П.С. Да и вообще. Если уж о философии — нахера тебе наследование? Делай через композицию и будет тебе счастие.
А наследование — это зло. Тем более, в неумелых руках.

Я сначала название прочитал

Коварність PHP

В цьому випадку, напевне це не є далеким від істини.. :), те що в інших мовах програмування, напр С#, це стандарна практика, в PHP, поки не реалізовано.. а шкода.. дуже спомічна штука була б, і архітектурф кода була б лаконічніша.. Так що момент підступно-коварності тут все ж таки є.. :)

Это вопрос о совместимости сигнатур методов. У тебя в интерфейсе IPayloadFactory метод create должен возвращать объект который реализует интерфейс IPayload. А в интерфейсе IChildPayloadFactory метод create уже должен возвращать экземпляр класса ChildPayload. Да, классChildPayload реализует интерфейс IPayload, но методы все равно несовместимы. потому что ты ограничиваешь возможные возвращаемые значения по отношению к базовому интерфейсуIPayloadFactory. Ведь исходя из этого интерфейса ты можешь вернуть экземпляр любого класса, который реализует IPayload, таких классов может быть куча. А в дочернем ты уже возвращаемое значение ограничил только классом ChildPayload.
Уже ночь и голова не варит, сомневаюсь что я сумел донести свою мысль, но все же)

Не зовсім погоджуюсь.. ChildPayload я не обмежую, а розширюю інтерфейс IPayload. Відповідно, якщо я повертаю ChaildPayload, я автоматично реалізую повертаю і інтерфейс IPayload... Ваше твердження було б справедливим, якби IPayloadFactory повертав би IChildPayload, a IChildPayloadFactory повертав би IPayload, то в такій ситуації дійсно, IChildPayloadFactory повертав би дійсно інтерфейс, який би обмежував батьківську фабрику, тому-що повертав би інтерфейс, який по своєму функціоналу був би обмеженим по відношенню до ChildPayload. А так, на мою думку, контракт(інтерфейс, на відміну від класа) він, по ідеї, повинен бути контрактом, без привязки до назви класу, в цьому і є суть коваріантності, на скільки я розумію...

контракт(інтерфейс, на відміну від класа) він, по ідеї, повинен бути контрактом, без привязки до назви класу

Контракт на то и контракт, чтобы его выполняли все потомки интерфейса и их реализации (классы). Это основа полиморфизма и ОО-декомпозиции.

А когда в контракте написано:
function create(): IPayload;

а у потомка резко оказывается, что:
function create(): ChildPayload;

— это нарушение контракта. И полиморфизм так работать не будет.

та ну.
на вход такое работает отлично: ожидать IPayload, но работать по сути с ChildPayload — ради такой абстракции как раз интерфейсы и нужны.

Ну да. Получаешь IPayload (т.к. контракт) — потом в реализации кастишь в ChildPayload (а лучше в IChildPayload) и работаешь с этим.

явное приведение — симптом проблемы в проектировании.
да, иногда альтернатива еще больнее.
но редко.

Общий интерфейс и приведение к специализирванному интерфейсу в реализации — основа полиморфизма. По другому это не работает.

полиморфизм — это возможность абстрагироваться. явное приведение — ломает абстракцию.

т.е. работать с фруктами, укладывая их в ящики, игнорируя отличие в цвете яблок и даже не задумываясь об отличии яблок и груш. это абстрагирование.
а не так, шо у нас логика декларирует работу с любыми фруктами, а внутри внезапно «а теперь возьмите яблоко и ...». а чё, если это груша?а если манго? ананас?

є rfc по цій темі. Шкода, що поки в драфті, а майбутнє його туманне як альбіон..
wiki.php.net/...​-contravariant-parameters

[del] надо было собственный совет проверить в песочнице; удалил

Такий варіант не підходить, тому-що, childPayload-ів є велика кількість (тобто пайлоадів, які реалізують інтерфейс IPayload), кожен пайлоад це типу як DTO-обєкт з get-ерами та set-ерами і кожен, повинен мати свій унікальний набір гетерів і сеттерів, для класу, який буде використовувати цей пайлоад. Тому GenericPayload розширяти від всіх, пайлоадів трохи не серйозно...

скорее наоборот, GenericPayload, от которого наследуются все, и create возвращает его экземпляр, посмотрел — по другому видимо никак, если синтаксис использовать 7.1+. Сейчас задумался, что в принципе эти возможности не использовал, поскольку yii2 база 5.6, чтобы не наводить разноообразий

Именно. Делайте свои пейлоады со статическим create методом, внутри которого заполняйте его данными и методом toArray. Вынесите create и toArray в интерфейс и имплементируйте его. В итоге у вас будет только один интерфейс для пейлоада, и фабрики будут возвращать объект, его имплементирующий. И все — вам не нужны сеттеры и геттеры в интерфейсе.

interface IPayload {
    public static function create(array $data): self;
}

interface IPayloadFactory {
    public function create(): IPayload;
}
Тут на вигляд, якась надлишковість по функціоналу. Не зрозуміло, навіщо у фабриці декларувати метод create, і потім при реалізації цієї фабрики ми просто делегуємо виконання методу з сутності, яка породжується цією фабрикою.. тобто і там, і там у нас, фактично одне і те саме.
По друге, якщо ми реалізовуємо create метод у класі самої сутності, то що робити, якщо потрібно якось по інакшому створити IPayload і нап потрібна інша фабрика, скажімо дані вигрібаються вже не з бази, а з файлу?? Тоді потрібно розширяти Payload, перевизначати в цьому класі метод create(), і тоді вже в dependecy Injection контейнері, якось переприсвоювати інтерфейсу IPayload нову фабрику, яка поверне розширену сутність, зі зміненим методом create()... як на мене дуже багато не потрібних рухів, якщо ж в нас фабрика окремо від сутності, яку вона породжує, тоді просто потрібно створити нову фабрику і все..

якщо ChildPayloadFactory повертає iPayload, то це значить, що по контракту, toArray() нам вже не доступний... можна звичайно цей метод використати, в надії, що ми гіпотетично працюємо з класом, який реалізує мотод toArray(), але як на мене це зовсім не Right Way...

странно выглядит, почему в первом случае возврат фабрики описан интерфейсом, а во втором — уже классом? переписать на интерфейс тоже, который наследуется от IPayload не вариант?

Воно так і є.. у мене все описано через інтерйфейси, ту не описував, щоб зробити код компактнішим, бо по суті це нічого не змінить..

Підписатись на коментарі