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

Yoskaldyr

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

@ksnk такое уже давно придумано и об этом я и писал выше:
И есть еще 3-й вариант для совсем альтернативно одаренных, полный треш - поиск замена по исходникам с генерацией файлов кеша.
поддерживать этот треш вообще не возможно и невозможно от слова совсем - проще руками править код (welcome to opencart world mazafaka! ).

Нет, конечно. Мне просто стало интересно, получится ли как-нибудь извернуться с yield в конструкторе и эмуляцией parent подменой $this а-ля JS. Получилось
Вот мне реально зашло! Понятно что это не для основного кода, но закостылить что-то или где-то для сверхвнутренного использования - самое то. Появилось пара идей как и где подобное применить (не инъекцию зависимостей, а такой хитрый проброс через yield).

Чтобы было понятно почему у меня такие странные идеи, основная моя работа была лет 8 была пилить модули для xenforo и magento (т.к. системы расширения похожие). Да системы не фонтан, но вот с точки зрения пользовательского опыта - есть хорошие идеи. И все это время пробовал все различные идеи как обойти/изменить/заменить встроенную систему расширения (учитывая что по умолчанию там не все можно расширять). Пробовал все перечисленное в данной теме (и аоп и ранкит) - и с точки зрения соотношения плюсов и минусов - такое вот динамическое наследование было лучше всего (дебаг, поддержка стормом и т.д.). Кое что было неплохо, например использовать прокси полностью на магии __call + __get, но время выполнения увеличивалось значительно в некоторых случаях и это как раз та часть которую никак нельзя контролировать.
Но последние 2 года уже этим не занимаюсь, а сейчас появилось немного свободного времени и решил заняться исследованиями, как можно было бы сделать то же самое в плане расширяемости но лучше или без такого наследования.
 

whirlwind

TDD infected, paranoid
Понудю немножко

1) А можно пример метода расширения - что он делает
2) Такое наследование вовсе ни разу не лучше, чем глобальный сервис локатор/god object/big ball of mud/etc ибо single-responsibility
3) DI решает проблемы класса, но автоматически усложняет жизнь потребителю класса. Упростить жизнь потребителю можно не только за счет service locator, но фабрик и билдеров
4) Если композицией избавляться от наследования, то надо всего два варианта (в зависимости от п1): композитный декоратор или chain-декоратор.
5) Если бы вы тесты писали, то бежали от подобного наследования как можно дальше
 

Yoskaldyr

"Спамер"
Партнер клуба
1) А можно пример метода расширения - что он делает
Пример с потолка, т.к. поднимать все старые разработки надо из архива. Есть пост на форуме. Клиент захотел расширить, добавить возможность автору поста проставлять какую либо из предопределенных меток для поста, в зависимости от метки, посты в теме могут по другому сортироваться, по другому отображаться и т.д. Т.е. модификация затрагивает почти все связанное с обычным отображением постов - от роутинга (тут да мидлварью легко), экшена контроллера (тут тоже мидлвари достаточно будет), сервисы что дергаются, вью модель, репозиторий, райт модель, ентити, вьюха (например. метка языкозависимая) и шаблон (это не в счет, а только для примера). Везде нужны или небольшие или значительные изменения/добавления. О полностью отдельном функционале типа само добавление, редактирование и т.д. - это понятно что отдельный код.
И вот таких расширений для поста может быть 100500 от разных разработчиков.
Фактически это не любимое наследование и является мидлварь подобной реализацией. Т.к. в 99% случаев все методы наследников состоят из собственного кода + вызов parent. Это ничем не отличается от мидлварьного next().

3) DI решает проблемы класса, но автоматически усложняет жизнь потребителю класса. Упростить жизнь потребителю можно не только за счет service locator, но фабрик и билдеров
Чем усложняет?
Фабрики и билдеры и так используются (тоже можно передать через зависимость)

4) Если композицией избавляться от наследования, то надо всего два варианта (в зависимости от п1): композитный декоратор или chain-декоратор.
Декоратор не всегда подходит. Класс с десятком свойств + 10 методов (все взаимосвязанные) и поведение каждого из этих методов в классе может быть изменено модулем/дополнением, а свойства добавлены/изменены. И этих дополнений может быть довольно большое количество.

2) Такое наследование вовсе ни разу не лучше, чем глобальный сервис локатор/god object/big ball of mud/etc ибо single-responsibility
да, все через контейнер, но:
- если все делать как сейчас в xenforo, то полностью согласен - типичный вариант сервис локатора,
- если делать как в нормальных инжекторах, то чтобы шаловливые руки разработчиков модулей не лезли на каждый чих в сервис локатор, пусть лучше явно пропишут зависимость в конструкторе (как раз по этому и создал тему). Да он может обойти все это, но это дополнительные телодвижения и если он это сделает - сам себе злобный буратино и это элементарно ищется статическими анализаторами кода.

5) Если бы вы тесты писали, то бежали от подобного наследования как можно дальше
В том то и дело что на свой код, который не зависит от стороннего - пишу, на бизнес логику в частности. Но тот же xenforo не совсем подходит для тестов во многих местах (жирные контроллеры и куча других кривых мест), но это проблема как раз не наследования, а кривой архитектуры самого движка. Наследование нужно только для дополнений и все. Сам форум работает без него.
Я бы даже сказал что использование такого наследования ничем не отличается от использования обычного контейнера. Но конечно тесты идут в сад когда используется чей-то чужой код - там тестов нет никогда и поведение предсказуемо другое.

Но реализовать тест 1 аддона + движок по умолчанию - вообще без проблем. А совместимость 2-х дополнений по определению никто не может гарантировать. Может заработать а может и нет.
 

Yoskaldyr

"Спамер"
Партнер клуба
о и еще вишенка на торте - дополнение которое расширяет дополнение (тоже стандартная задача)
и желательно чтобы не надо было обновлять дополнение когда только немного поменялась сигнатура одного из неиспользуемых методов используемого в аддоне класса.
 

Yoskaldyr

"Спамер"
Партнер клуба
@whirlwind
чем плох такой вариант:
PHP:
class Addon extends Main {
    public $dep3;

    public function __construct(Dep3 $dep3, ...$deps) {
        $this->dep3 = $dep3;
        parent::__construct( ...$deps);
    }
}
когда правильность и порядок зависимостей разруливаются контейнером
чем принципиально это отличается от стандартного использование DIC, когда тоже все разруливается контейнером?
PHP:
class Addon {
    public $dep3;

    public function __construct(Dep3 $dep3, ...$deps) {
        $this->dep3 = $dep3;
    }
}
 

Yoskaldyr

"Спамер"
Партнер клуба
@whirlwind И тогда вопрос в догонку. Может есть на примете какой-либо готовый проект на гитхабе который легко можно расширить независимыми модулями? как пример.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Yoskaldyr ощущение, что ты тихо сам с собою разговариваешь.
Мы твоего кода не видим.

>заранее неизвестны зависимости родителя
такого не бывает по определению, если ты расширяешь класс - значит, класс объявлен, его интерфейс известен, иначе получишь Fatal Error

> полностью не решены, для крупных блоков приложения да, но когда речь о небольших изменениях, вот тут появляются нюансы.
опровергается легко: если бы проблема была - про нее писали бы статьи, обсуждали, предлагали, и решали, а этого нет потому что нет такой проблемы

дальше в тексте еще много подобных ложных утверждений
 

Yoskaldyr

"Спамер"
Партнер клуба
при написании модуля не известны. Известен только базовый класс, а дальше цепочка строится в рантайме. Т.е. в момент выполнения конкретный родитель уже известен.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
> класс Proxy_AddonMain вычисляется динамически через class_alias
не бывает динамических вычислений классов,
классы статические - это базовый принцип Java, создать динамически именно класс можно только через eval, можно считать это запрещенным,
создать и изменить можно объект, вычисления бывают только у объектов или статических методов

или говори как есть, или это не код, а фантазии
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
при написании модуля не известны. Известен только базовый класс, а дальше цепочка строится в рантайме. Т.е. в момент выполнения конкретный родитель уже известен.
приведи простой пример задачи, когда при написании модуля может быть неизвестен родительский класс, такого даже в wordpress не встречается - а расширений там сотни тысяч
 

WMix

герр M:)ller
Партнер клуба
Сочуствую, @Yoskaldyr. Думаю при такой работе выбора нет, приходится только дебажить код. Я очень редко это делаю.. но чтоб ответить на вопрос. Если бы форум/смs былаб написано на хорошем массовом фреймворке, на той же симфони к примеру, то разработка плагина ничем не отличалась бы от обычного программирования.. а так это «как бы впихнуть невпихуемое»
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Допустим, ты вводишь соглашение: такое-то имя класса FOO объявляется служебным.
Потом объявляешь множество userland-классов BAR1{protected $baz;}, BAR2{protected $lambda}.
И делаешь финт ушами - объявляешь алиас для одного класса с именем служебного: `class_alias(BAR2::class,'FOO');`
У тебя есть класс ZOO extends FOO, - служебный алиас, так ты можешь на лету задать ему предка..? точнее, scope.
Так можно получить доступ к protected-членам userland-класса, от котогого ты унаследовался. Profit?

Идем дальше по коду. Работаем мы не с классами, а с объектами. Следующий шаг - создается объект по служебному классу `$x = new ZOO`;
И тут, внезапно, в документации описано с примером как делать monkeypatching правильно
PHP:
class A{private $foo = 5;}

$userlandClass = 'A';

$A = new $userlandClass;

$x = function (){foreach ($this as $k=>$v) echo $k,' => ',$v,"\n";};
$x = Closure::bind($x,$A,$userlandClass);

$x();
вывод, как обычно, rtfm
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
а DI для new $userlandClass; делают, соответственно, все фреймвоки мира
 

whirlwind

TDD infected, paranoid
Клиент захотел расширить, добавить возможность автору поста проставлять какую либо из предопределенных меток для поста, в зависимости от метки, посты в теме могут по другому сортироваться, по другому отображаться и т.д. Т.е. модификация затрагивает почти все связанное с обычным отображением постов - от роутинга (тут да мидлварью легко), экшена контроллера (тут тоже мидлвари достаточно будет), сервисы что дергаются, вью модель, репозиторий, райт модель, ентити, вьюха (например. метка языкозависимая) и шаблон (это не в счет, а только для примера). Везде нужны или небольшие или значительные изменения/добавления. О полностью отдельном функционале типа само добавление, редактирование и т.д. - это понятно что отдельный код.
И вот таких расширений для поста может быть 100500 от разных разработчиков.
Трудно не видя кода. Мне кажется, здесь ключевое непонимание, вызванное "абстрактным" определением инкапсуляции. Не любое поведение нужно рассматривать как часть класса. Возьмите любой класс и посмотрите сколько мест в коде, где он инстанцируется и какое совокупное количество объектов создается в целом при работе. Чем количество выше, тем оправданнее написание класса. С другой стороны, если экземпляр класса создается единственный раз в единственном месте и при этом он еще и такой сложный, как в вашем примере, стоит крепко задуматься: не делаю ли я так, что вместо класса User с атрибутом name я делаю UserVasya, UserPetya, etc???

Если ради изменения поведения вы постоянно меняете/расширяете код, а не определения или правила (аргументы, параметры) работы существующей реализации, это явно проблемы дизайна. И тесты давно бы научили вас как переписать так, что бы не изменять поведение, путем расширения "почти все связанное с обычным отображением постов", а создавать правила отображения постов и в последствии например иметь возможность инстанцировать два и более экземпляра контроллера неизменного и ни разу не отнаследованного класса, но с разнымы аргументами сетапа, вместо инжекта новых зависимостей и множество новых наследников.

Почему? Потому что IoC и за ним все тянется. Вот вы спросили, почему жизнь потребителя усложняется при DI? Все потому же. Потому что IoC построена на функциях ЯП (интерфейсы, контракты, полиморфизм). А конструктор не является частью ценного production интерфейса и он не полиморфен. Как фабрика облегчает ситуацию? Фабрика производит полиморфные объекты, скрывая неполиморфный конструктор от потребителя. Потребитель может инстанцировать 100500 экземпляров разных классов, имплементирующих один и тот же интерфейс не зная требований конструктора и вообще реальной сложности этих объектов. Все что интересует потребителя объектов - это доступ к желаемому интерфейсу. Фабрика в один вызов дает потребителю именно это и не более. Это ближе TDA. А заморачиваться конструкторами это программирование по-соглашению, не контрактное, неполиморфное, сильно связанное с реализациями -> проблемное.

Но повторюсь, не видя кода говорить - как лечить по телефону. При наличии толстого service layer этот способ подойдет. А если там размазано 80% по контроллеру, то нет.
 

Yoskaldyr

"Спамер"
Партнер клуба
> класс Proxy_AddonMain вычисляется динамически через class_alias
не бывает динамических вычислений классов,
небольшое разъяснение если не было понятно:
для примера есть базовый класс Main, он расширяется 2 дополнениями AddOn1 и AddOn2
PHP:
//при вызове из контейнера класса Main
$instance = $container->get(Main::class); //$instance - это объект класса AddOn2_Main

//----------------------------------------------
//контейнер видя из конфига что у класса есть расширение 2 дополнениями делает:

//алиас
class_alias('Main', 'AddOn1_Main_VirtualProxy');
 
// загрузку класса первого дополнения:
class AddOn1_Main extends AddOn1_Main_VirtualProxy {
         //.......
}

//алиас
class_alias('AddOn1_Main', 'AddOn2_Main_VirtualProxy');

// загрузку класса второго дополнения:
class AddOn2_Main extends AddOn2_Main_VirtualProxy {
         //.......
}

//инстанс нужного класса
return new AddOn2_Main();
 

Yoskaldyr

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

по функционалу кто и что расширяет (не коду) достаточно просто зайти в магазин приложений форума или глянуть список модулей мадженты и посмотреть дополнения которые расширяют другие дополнения.
 

Yoskaldyr

"Спамер"
Партнер клуба
Но повторюсь, не видя кода говорить - как лечить по телефону. При наличии толстого service layer этот способ подойдет. А если там размазано 80% по контроллеру, то нет.
проблема в том что даже с толстым сервислеером иногда надо менять какой-то один из методов сервиса (если конечно сервис состоит больше чем из одного класса) и менять один и тот же метод может несколько дополнений одновременно. Жирным контроллерам ничего не поможет, наследование в том числе
 

whirlwind

TDD infected, paranoid
Тогда мне ничего иного не остается, как выдать прописную истину: универсальность и производительность лежат на разных чашах весов. (Условно) либо универсально ORM и замедление, либо неуниверсально без джойнов расширять таблицы, но зато работает быстро. Для коробочных продуктов ИМХО выбор очевиден.
 

Yoskaldyr

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

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