Constructor Dependency Injection и извращения

Yoskaldyr

"Спамер"
Партнер клуба
Навеяло обсуждением в другой теме.
Сразу хочу сказать что все описанное ниже из разряда извращений и только для коробочных продуктов где важно расширение любого функционала сторонними модулями и в данный момент самое жизнеспособное решение это через цепочку наследования (пример xenforo/magento).

но с таким подходом нормальное явное DI в конструкторе не сделать:
PHP:
//базовый
class Main {
    public $dep1;
    __construct(Dep1 $dep1) {
        $this->dep1 = $dep1;
    }
}
//пример цепочки наследования для первого модуля
//здесь проблем нет, т.к. 100% известен родитель
class AddonMain extends Proxy_AddonMain {
    public $dep2;
    __construct(Dep1 $dep1, Dep2 $dep2) {
        parent::__construct($dep1);
        $this->dep2 = $dep2;
    }
}
// класс Proxy_AddonMain вычисляется динамически через class_alias или через генерацию прокси прокладки:
class Proxy_AddonMain extends Main {}

//вот когда модулей больше 2-х и их порядок при написании кода не может быть известен то нормальный явный конструктор уже не сделать:
class Addon2Main extends Proxy_Addon2Main {
    public $dep3;
    //заранее не известны зависимости родителя потому правильную сигнатуру не сделать
    // splat оператор и магия через рефлексию - все очень не явно
    __construct(Dep1 $dep1, Dep2 $dep3) { // это ломает все
        parent::__construct($dep1);
        $this->dep3 = $dep3;
    }
}
Setter/Property injection - фигово тем что или описание зависимостей находится полностью отдельно от класса, в конфиге контейнера и к тому же есть возможность инстанцировать полностью невалидный класс.

Ну и любимый мной SL - одна супер зависимость - контейнер, которая рулит всем (как раз так и делают везде)

Как еще можно сделать инъекцию зависимостей чтобы было более менее явно, описано не в контейнере, а в самом классе и чтобы можно было так наследовать? Ну или как еще расширять коробочный продукт, независимыми модулями (вариант событий/хуков в определенных точках не позволяет сделать что угодно)?

P.S. Понимаю что задача изначально - извращение, но коробочные продукты они такие :(
 

Yoskaldyr

"Спамер"
Партнер клуба
composition over inheritance
да :( только как композицией в пхп решить это:
только для коробочных продуктов где важно расширение любого функционала сторонними модулями
Т.е. чтобы было понятно - всю кучу минусов цепочки наследования я знаю, но как по другому сделать (а aop еще более неявно и собирает вообще все грабли какие только можно) - я хз, т.к. такая расширяемость - это одно из главных бизнес условий
 

Yoskaldyr

"Спамер"
Партнер клуба
добавлю еще
использовать Trait injection тоже не совсем вариант, т.к. это считай подвид Setter/Property injection, но все равно класс может быть невалидным + магия на стороне контейнера более жесткая, хотя явность зависимостей выше
 

Yoskaldyr

"Спамер"
Партнер клуба
Описанный вариант расширения имеет кучу недостатков, но он решает свою основную задачу - расширять как угодно и включать выключать модули почти на лету (только нюансы в асинхронных и фреймворках по типу roadrunner-а).

Можно конечно пойти по пути go-aop и подобных либ - полный парсинг php кода и генерация результирующих классов, но даже в этом случае проблему constructor injecton нормально не решить.

Также можно делать все на аннотациях, но программирование на аннотациях еще большее зло и тоже никак не решает проблемы невалидных объектов
 

флоппик

promotor fidei
Команда форума
Партнер клуба
ну вот как например, композицией решены во всех фреймворках миддлвары и прочие части сторонних пакетов: описываешь универсальную точку входа через интерфейс, или используешь события.
 

Yoskaldyr

"Спамер"
Партнер клуба
ну вот как например, композицией решены во всех фреймворках миддлвары и прочие части сторонних пакетов: описываешь универсальную точку входа через интерфейс, или используешь события.
извини, но полностью не решены, для крупных блоков приложения да, но когда речь о небольших изменениях, вот тут появляются нюансы.
А события тоже немного мимо, ну не получится везде ставить точку входа, т.к. чтобы можно было изменить любую часть (еще раз повторюсь такого в обычном приложении никогда не бывает нужно) надо чтобы точка входа по типу мидлвари или события должна быть перед каждым методом, а это бред.
Во всех фреймворках все заточено под конкретное приложение которое контролирует тот кто пишет код, и обычно под расширением подразумевается или единократная подмена реализации (DI) или добавление обработчиков в ограниченном количестве мест (события/мидлвари). В обычном приложении не стоит извращенной задачи заменить оригинальный код, без фактической его замены - зачем извращаться когда можно просто отредактировать код как надо (например сделать свою реализацию интерфейса)
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Ну вот те примеры, что ты приводишь - это просто хуевая архитектура. Тут ты явно пишешь "я хочу сломать SOLID, особенно буквы О и L". Если у тебя возникают проблемы уровня "как напихать новых депенденсов в конструктор" значит, твоя абстракция говно и развалилась, и нет тут никаких "обычных" и "необычных" приложений.
А если тебе нужно 100% функционала написать "по-другому" ну, тут никакие goaop не помогут.
 
  • Like
Реакции: AmdY

Yoskaldyr

"Спамер"
Партнер клуба
это просто хуевая архитектура.
А где я говорил что это хорошая архитектура? :))))) Да, гавно. Но по другому никак :)

Если бы пхп был более динамический - по типу того же js, то вообще таких вопросов не стояло-бы - расширяй, подменяй любой метод в любое время в рантайме.

И если насчет O почти согласен, то насчет L - тут явно мимо, т.к. вся эта хреновая магия как раз на базе подклассов и вся отнаследованная многоуровнениая хрень обязана работать вместо базовых классов.

Клиенту плевать на архитектуру. Он хочет купить продукт и в магазине приложений накупить модулей - все. И реализовать это красиво в пхп совсем не просто. Сейчас в реальных продуктах используется только 2 варианта - или на базе событий (но это ведет и к 100500 событий и к постоянному копипасту с постоянной несовместимостью между модулями) или на базе такого динамического наследования. И есть еще 3-й вариант для совсем альтернативно одаренных, полный треш - поиск замена по исходникам с генерацией файлов кеша. По соотношению плюсов и минусов - динамическое наследование лучший вариант из существующих. И как по мне хоть это и гавно, но значительно меньшее чем god object like сервис локатор, который есть почти во всех проектах на гитхабе (даже здесь на форуме народ часто любит сервис локаторы, судя по примерам кода).
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Клиенту плевать на архитектуру. Он хочет купить продукт и в магазине приложений накупить модулей - все. И реализовать это красиво в пхп совсем не просто.
Хуевую архитектуру нельзя сделать "красиво". "Просто" тоже не бывает ничего.
У тебя проблема не с языком или архитектурой, а с тем, что ты хочешь писать "нормально", хотя в бизнесе находишься в рынке для говнопродуктов. Твоя проблема решается в другой плоскости.
 

fixxxer

К.О.
Партнер клуба
Я, конечно, не призываю ТАК делать, но в качестве забавного извращения - вот тебе PoC:

PHP:
class Dep1 {}
class Dep2 {}
class Dep3 {}

class Main {
    public $dep1;
    public $dep2;

    public function __construct(Dep1 $dep1, Dep2 $dep2) {
        $this->dep1 = $dep1;
        $this->dep2 = $dep2;
    }
}

class Addon extends Main {
    public $dep3;

    public function __construct(Dep3 $dep3) {
        $this->dep3 = $dep3;
        yield $this;
    }
}

$dep1 = new Dep1();
$dep2 = new Dep2();
$dep3 = new Dep3();

$addonClass = new ReflectionClass(Addon::class);
$addon = $addonClass->newInstanceWithoutConstructor();
foreach ($addon->__construct($dep3) as $instance) {
    $parentCtor = new ReflectionMethod(get_parent_class($instance), '__construct');
    $parentCtor->invoke($instance, $dep1, $dep2);
}

assert($addon->dep1 === $dep1);
assert($addon->dep2 === $dep2);
assert($addon->dep3 === $dep3);
Сделать из этого DIC оставляю в качестве самостоятельного упражнения. :)
 
Последнее редактирование:

Yoskaldyr

"Спамер"
Партнер клуба
@fixxxer Я думал о подобном, но только через ... оператор .
PHP:
class Dep1 {}
class Dep2 {}
class Dep3 {}

class Main {
    public $dep1;
    public $dep2;

    public function __construct(Dep1 $dep1, Dep2 $dep2) {
        $this->dep1 = $dep1;
        $this->dep2 = $dep2;
    }
}

class Addon extends Main {
    public $dep3;

    public function __construct(Dep3 $dep3, ...$deps) {
        $this->dep3 = $dep3;
        parent::__construct( ...$deps);
    }
}
осталось только в контейнере через рефлексию выстроить правильный порядок параметров (который можно кешировать чтобы не делать повторно для того же класса) и контейнером создавать
PHP:
new Addon(...$deps);
т.е. получается обычный autowire через рефлексию, разве что с поиском в глубину родительских классов + небольшая конвенция по конструкторам. Все остальное получалось более громоздкое и извращенное
 

ksnk

прохожий
@fixxxer , А если в "цепочке наследований" больше двух ну очень самобытных конструктора? Их все перечислять в цикле ? Ну и использовать yeld в конструкторе у меня как-то "глаз режет" :) Может отдельный гет-конструктор, все таки...
В таком случае, imho, будет более управляемым - передавать ассоциативный массив параметров, чтобы каждый конструктор разбирал нужный ему депенденс в нужное ему место. Ну и в каждом классе обязан быть конструктор дефолтного критического для класса параметра, для случая, когда юзер не хочет его явно инициировать снаружи.
 

Adelf

Administrator
Команда форума
Хватит извращаться. Какой смысл обсуждать то, что надеюсь не появится у вас в гите никогда)
А как ты те самые классы то так красиво и невероятно гибко расширяешь? Ты ведь наверняка просто максимум переопределяешь какие-то протектед или паблик методы и заменяешь реализацию? Других адекватных изменений я что-то не вижу.
И какая разница если это будет один большой интерфейс, где будут собраны все эти методы и протектед и паблик и одна реализация его основная(которая дефолтная). Остальные просто тоже самое делают, но с помощью шаблона Декоратор. Причем имея нормальные конструкторы и такую же расширябельность, но такую архитектуру еще и не стыдно будет показать кому-то. Собирать эти цепочки декораторов можно из конфига как-нибудь. не суть.
 

Yoskaldyr

"Спамер"
Партнер клуба
@ksnk А чем ассоциативный массив параметров отличается от передачи просто контейнера? В таком случае тупо передавать контейнер, как делают много где, но тогда это будет сервис локатор (правда только в конструкторе, но все равно сервис локатор)
 

Yoskaldyr

"Спамер"
Партнер клуба
@Adelf ну расширь 10 декораторами один класс и успехов в дебаге. Не все можно сделать декоратором нормально. Особенно когда этих декораторов для одного класса дофига и больше. Это как раз та часть которую разработчик дополнения не может контролировать.
 

Yoskaldyr

"Спамер"
Партнер клуба
И да, тема же довольно точно называется - ИЗВРАЩЕНИЯ! Какие претензии?
 

Adelf

Administrator
Команда форума
ну дебаг, да. аргумент. Хотя я думаю с цепочкой наследования из 10 классов тоже есть проблемы. и они намного хуже:)
 

ksnk

прохожий
Насколько я понял, задача навеяна вот этим - https://m.habr.com/ru/post/247351/ . Можно переформулировать задачу для простоты и ощущения полезности.
Есть класс ядра, для определенности - Main, с конструктором и одним методом, которые могут поменяться разработчиками в разные моменты жизни системы. Есть плагины для системы Addon1,... AddonX / которые желали бы добавить своего кода в одну-обе точки класса Main. Желательно, сделать разработку аддонов и ядреного класса максимально независимыми друг от друга. Обязательно, код конечного пользователя должен работать независимо от наличия-отсутствия плагинов системы (в разумном смысле слова "независимо")
Требуется разработать дисциплину оформления плагинов в системе приемлемой сложности и удобства для жизни (отладка - эффективность)

Очевидно, что интерфейс менять нельзя, ибо конечный пользователь системы может расчитывать в том числе и на работу в "голой системе", без плагинов.
Код конечного пользователя - что-то вроде
PHP:
$record=$app->getinstance('Main')->GetRecord();
Из разряда извращений - патчинг исходного кода. В код добавляются специальными комментариями куски, которые тупо и цинично вставляются в заготовку. К примеру,
PHP:
class Main {
//!patch point class_main_description

function __construct(){
//!patch point class_main_construct_before
...
//!patch point class_main_construct_after
}
}

class Addon1 {
//!patch insert class_main_description
var $dep1;
//patch!

function fake_function(){
//!patch insert class_main_construct_after
$this->dep1=new Dep1();
//patch!
}
}
}
Парсятся исходники в момент, когда инсталлируется-обновляется очередной плагин, "ядреный" класс дополняется всеми необходимыми кусочками кода и помешается в рабочий каталог системы. Все "исходники" ядра и плагинов находятся в нерабочем разделе системы.
Очевидно, что таким образом можно навернуть много чего, в том числе и неслабо навернуться... вот только жить и отлаживать в таком коде будет непросто. Из плюсов - практическая эффективность решения. Расходов на инициализацию системы плагинов - минимум.
 

fixxxer

К.О.
Партнер клуба
Хватит извращаться. Какой смысл обсуждать то, что надеюсь не появится у вас в гите никогда)
Нет, конечно. Мне просто стало интересно, получится ли как-нибудь извернуться с yield в конструкторе и эмуляцией parent подменой $this а-ля JS. Получилось :)

Если серьезно:
А где я говорил что это хорошая архитектура? :))))) Да, гавно. Но по другому никак :)
А зачем тогда пытаться вообще привносить какие-то хорошие практики? SOLID работает только целиком, убрал одну буковку и все рассыпалось. Если уж делать говно - так не стесняться.

Твоя проблема решается эмуляцией прототипного наследования через __call и прокси. А конструкторы вообще без параметров обойдутся, нафигачить фасадов а-ля Laravel и отлично. Для писателей плагинов к CMS-кам самое то, они твои старания не оценят все равно, уж поверь.
 
Последнее редактирование:
Сверху