Расширение класса внешними методами и переменными

whirlwind

TDD infected, paranoid
https://github.com/adelf/acwa_book_ru/blob/master/manuscript/2-di.md#наследование. Вот здесь я попытался придумать проблему. Вполне вероятную кстати.
Пример нерелевантный и демонстрирует как раз случай непонимания проблемы. Наприши юнит тест. Не интеграционный, который использует реализацию и обращается к диску, а использует абстракцию. Юнит тест на класс, использующий твой final ImageUplloader. Теперь расширь иерархию на несколько уровней. Представь, что некий сервис юзает твой final ImageUploader и в свою очередь является final Service. Напиши тест на пользователя такого сервиса и ужаснись объему работы. Ты декорировал для логгирования, это супер! Теперь сделай то же самое для классического примера AOP логгирование. Ну и т.д.
 

fixxxer

К.О.
Партнер клуба
А в чем проблема? Если у меня final ImageUploader, то он либо extends abstract, либо implement, и "некий сервис" зависит от абстракции.
Если у меня в том "некоем сервисе" явная зависимость от final ImageUploader, я явно написал говно и нарушил DIP.
 

whirlwind

TDD infected, paranoid
Если бы в языке по умолчанию был final и требовалось бы писать open к тем классам, где наследование разрешено (Kotlin, например), то такой формулировки («где его использование будет оправдано»), наверное, бы не было. Я ищу не места, где нужен final, а где его можно убрать.

Я могу привести в пример, допустим, любой value object. Или класс с одним методом, который реализует интерфейс. Там наследование при всём желании не имеет смысла, там нечего будет наследовать, даже притянуть его за уши не получится.
То есть ты за протокол вместо инструмента. Иначе говоря, если бы ты был депутатом, то ратовал за основной принцип, при котором все что не разрешено, запрещено. А любое действие требует наличие закона, регламентирующего эту деятельность? С чего ты взял, что это уместно/приемлемо/эффективно/допустимо для всех? Для тебя лично это может быть вполне допустимо. Если у вас 100% всего кода - это ваш собственный код, то это вполне допустимо. Как только вы начинаете юзать чужой код или кто то юзает ваш, требование протокола на каждый чих это мягко выражаясь - вы много на себя берете.
 

whirlwind

TDD infected, paranoid
А в чем проблема? Если у меня final ImageUploader, то он либо extends abstract, либо implement, и "некий сервис" зависит от абстракции.
Если у меня в том "некоем сервисе" явная зависимость от final ImageUploader, я явно написал говно и нарушил DIP.
У него там финалы не implement. Я и говорю, что заставить всех implement вы не в состоянии. И спорить с тем, что бОльшая часть стороннего кода не implement это спорить с реальностью. Единственный способ прикинуться абстракцией это сделать наследника (автоматически или явно). Если у вас final, вы обрекаете этот класс на единичное использование. Вы не сможете пропихнуть замену этого класса никуда. Плохой код без implement? Ну поспорьте с гитхабом по поводу java-diff-utils и прочими апачами. Мало кто имплемент и это мрачная реальность.
 

fixxxer

К.О.
Партнер клуба
Сторонний код инфраструктурного уровня (за редким исключением, когда он реализует стандартные интерфейсы типа PSR-овских) один фиг у меня будет изолирован через какой-нибудь adapter/wrapper. На уровне приложения, конечно, там подстраиваешься под фреймворк, но это просто обвязка, там вообще пофиг.
 

Вурдалак

Продвинутый новичок
С чего ты взял, что это уместно/приемлемо/эффективно/допустимо для всех?
Потому что это мой API и если я что-то допускаю в нём, то я тем самым сужаю возможности для самого себя для рефакторинга с учётом обратной совместимости.
Да и потом, я напротив помогаю тем, что пытается переопределить какой-то метод и не знает в какой момент это может сломаться из-за того, что его сигнатуру решили изменить в родителе: я сто раз видел, как во фреймворках плюют на обратную совместимость в таких местах, если переопределение конкретного метода им кажется маловероятным. Ну так почему нельзя было сразу final поставить, если им так кажется? А у кого-то в таких местах ломается код.

Если ты начинаешь приводить всякие веселые аналогии с политикой, то у этого есть и обратная сторона. Такие как ты просто боятся ответственности и не ставят final, но при этом как бы и не открывают для наследования, ведь они просто ничего не ставили, а если что-то у кого-то сломалось, так и сами виноваты.
 

whirlwind

TDD infected, paranoid
Сторонний код инфраструктурного уровня (за редким исключением, когда он реализует стандартные интерфейсы типа PSR-овских) один фиг у меня будет изолирован через какой-нибудь adapter/wrapper. На уровне приложения, конечно, там подстраиваешься под фреймворк, но это просто обвязка, там вообще пофиг.
В некоторых случаях иначе и не получается. Тонко там где рвется. Где тестами не покрыто, там и порвется. На этой спайке, где wrapper/adapter невозможно было протестировать.
 

whirlwind

TDD infected, paranoid
Такие как ты просто боятся ответственности и не ставят final, но при этом как бы и не открывают для наследования, ведь они просто ничего не ставили, а если что-то у кого-то сломалось, так и сами виноваты.
Я тебе еще раз говорю - выйди из своего уютного мирка и окунись в жестокий мир реальности разработки enterprise applications.
Конкретный пример spring framework пакет context (IoC). Оно не может проксю на final метод. С каким тезисом ты будешь спорить дальше:
1) spring это хороший фреймворк на передовой enterprise разработки
2) IoC хороший подход
 

fixxxer

К.О.
Партнер клуба
А как юнит-тестами протестировать инфраструктурщину, тем более если это банальная обвязка вокруг стороннего кода? Да никак, тут на behavior tests вылезет.
 

whirlwind

TDD infected, paranoid
А как юнит-тестами протестировать инфраструктурщину, тем более если это банальная обвязка вокруг стороннего кода? Да никак, тут на behavior tests вылезет.
Моки стабы. С моками понятно, а вот стабы как раз хорошо идут extends.
 

fixxxer

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

whirlwind

TDD infected, paranoid
Писать юнит-тесты на сторонний инфраструктурный код - это, как по мне, излишне. Сложно (или вообще невозможно в зависимости от архитектуры этого кода), трудоемко, и в конечном итоге бессмысленно - все равно behavior tests словят проблемы.
Нет. Вот здесь в основном непонимание по поводу которого спор. Ты в юнит тесте используешь абстракцию стороннего кода. Не важно какого эьто уровня абстракция. Это может быть фасад, за которым нечто сложное. Если есть интерфейс, то ты его мокаешь. Если интерфейса нет, ты мокаешь класс и переопределяешь методы, с которыми взаимодействует Class Under Test. Тут важно что интерфейс класса это тоже интерфейс и контракт. Он и нужен. Но если класс final, то кроме behaviour теста никак не протестировать. А вот тут уже справедливо все, что ты написал - сложно или вообще невозможно. Более правильно - неоправданно дорого.
 

fixxxer

К.О.
Партнер клуба
Если это тупая обертка, то нафиг такие юнит-тесты вообще. Я когда-то упарывался и писал моки-стабы на все подряд, но в случаях таких оберток это ни разу ничего не словило (да негде там банально), только создавало кучу оверхеда на суппорт этого всего на пустом месте. А если не тупая, ну чо, значит, надо отдекаплить, чтобы стала тупая, предоставляющая свой интерфейс, и не тупая, его использующая.

Впрочем, final, который не extends abstract и не implements, конечно, в "библиотечных" штуках неуместен, я тут и не спорю (а вот для, скажем, domain entity/VO final по определению нужен, просто по самой сути). Но правило "abstract or final" отлично работает. Я как-то ради интереса посмотрел по пачке своих проектов, где я наследуюсь от не абстрактных классов - такого было очень мало и везде это были хаки и костыли вида "тут, конечно, надо композицию, но фиг с ним, срежу углы".
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Оно не может проксю на final метод.
Успокойся ты со своими проксями — я приводил вон пример с value objects и классами, у которых есть интерфейсы.

Что до прокси в целом — да, такая проблема есть, но и она по идее могла бы решаться, если бы в языке была бы возможность сделать наследника как-то напрямую через рефлексию (правда тут будет теряться возможности для некоторых оптимизаций байт-кода для всяких final-методов, но таким для PHP можно было бы и пожертвовать, наверное).

Но это не проблема final как такового. Можно его заменить на «мягкий» вариант с аннотацией `@final` как в Symfony, где они детектят нежелательное наследование на уровне debug class autoloader: и волки сыты и овцы целы.
 

whirlwind

TDD infected, paranoid
Сильно зависит от языка и mock фрейморка. Если фреймворк или язык позволяет делать контроль соответствия прототипов наследника с оригиналом, то тесты сразу выявят проблему, если контракт сторонней либы поменялся. В плюсАх и java есть спецификатор override и оно просто не скомпилит. В пыхе не помню, но там была раньше разница мокать интерфейс или класс.

По поводу для кого надо, для кого нет. Тут надо с азов начинать. Тесты подразделяются на behavior test и state test. Сделать assertEquals(VO, VO) - это state test. Это нормально для DTO/VO ибо их суть представлять данные. Но например assertEquals(expectedService, mailService) смысла мало. В этом случае больше важно насколько правильно ваш CUT взаимодействует с mailService. И это уже тест поведения. Сделать его можно через моки, путем определения правильной последовательности взаимодействия с интерфейсом сервиса. Для интерфейсов (включая интерфейсы классов), которые в основном представляют собой данные, обычно используется сравнение ожидаемого состояния с полученным в результате работы. Для интерфесов, за которыми скрывается сложная бизнес логика используют тестирование поведения. Если вы применяете несоответствующие подходы, то ваше тестирование превращается в ад, а тесты начинают становиться проблемой. Все что я писал выше про final и прочие прелести параноидальной инкапсуляции, все это мешает как раз тестированию случаев сложной бизнес логики. Почему тесты так важны? Потому что любой тест это первый пользователь вашего класса. Если вам сложно написать тест, то ваш класс - плохой кандидат на reuse.

PS. Что бы тема опять не вернулась к примерам первого уровня типа ImageUploader. Речь идет не о дизайне CUT, а дизайне объектов, с которыми взаимодействует CUT. Протестировать ImageUploader легко. Протестируй того, кто юзает ImageUploader, что бы понять, насколько дизайн хорош.
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Протестируй того, кто юзает ImageUploader, что бы понять, насколько дизайн хорош.
Если тот, кто юзает ImageUploaderImpl, завязан на ImageUploaderImpl а не на абстракцию, и это не адаптер-враппер к сторонней либе, то дизайн уже явно не то чтобы хорош.

final class S3ImageUploader extends AbstractImageUploader - нормально.
final class S3ImageUploader implements ImageUploader - нормально.
final class S3ImageUploader { - да, проблемно. (но для domain entities - как раз именно так и надо).

class S3ImageUploader, class MyS3ImageUploader extends S3ImageUploader - явно фигня какая-то, типичное наследование там где нужна композиция.

(Да, конечно, в ряде случаев, связанных с тестами, workaround в виде замены final на аннотацию @final - вполне себе вариант. Но это не то чтобы прямо частый случай.)
 

whirlwind

TDD infected, paranoid
Все еще не согласен.

Давайте вспомним, что такое интерфейс. В Java интерфейсы появились с 1.0 (это было до (очепятка) после первых плюсов), но если на них посмотреть повнимательнее сегодня, они обратно превращаются в классы. В C++ до сих пор нет интерфейса как отдельного языкового понятия. В C++ интерфейсы это pure virtual классы, где есть только хидер с объявленным набором прототипов методов. В PHP интерфейс полностью соответствует классу, где все методы abstract и без тела. То, что в более высокоуровневых языках есть interface keyword и то, что тело функции/метода неотделимо от декларации в рамках класса это сайд эффект и не меняет того факта, что интерфейс - это набор публичных методов и атрибутов класса. Сегрегированный interface это способ жесткого требования контракта. И это полезно в отдельных (но не во всех) случаях. Для одного единственного кейса implements скорее вредно, чем полезно. Когда вы сидите и думаете, ну вот например тут могла бы быть такая имплементация или такая - это overengineering (что само по себе противоречит KISS). С другой стороны, если класс не implements, это не значит, что у него нет интерфейса. Сами interface как декларация контракта это следствие развития классов и ООП, которые появились сильно после первых ООП-специализированных ЯП. Если класс представляет свой интерфейс, то final class почти как private interface, что является полной бессмыслицей. Это не только нарушает OCP, но и косвенно LSP и DI (а еще бритву Оккама). Это все спорные вопросы, но extends abstract прямо тыкает носом что без наследования можно, но сложно.
 
Последнее редактирование:

Adelf

Administrator
Команда форума
Пример нерелевантный и демонстрирует как раз случай непонимания проблемы. Наприши юнит тест. Не интеграционный, который использует реализацию и обращается к диску, а использует абстракцию. Юнит тест на класс, использующий твой final ImageUplloader. Теперь расширь иерархию на несколько уровней. Представь, что некий сервис юзает твой final ImageUploader и в свою очередь является final Service. Напиши тест на пользователя такого сервиса и ужаснись объему работы. Ты декорировал для логгирования, это супер! Теперь сделай то же самое для классического примера AOP логгирование. Ну и т.д.
Че вы к ImageUploader привязались. в секции про Наследование говорится про ReqisQueue и т.д.
 

Вурдалак

Продвинутый новичок
Я как-то тоже не вполне понимаю. Говоришь ему про value objects и случаи, когда есть интерфейс — ему такие примеры видимо не нравятся.

Уже сто раз говорили, что если реализация реально не предполагается другая и нужно мокнуть — ну можно убрать final или заменить на аннотацию, но человек присосался к примеру, потому что только он, по всей видимости, как-то подтверждает его точку зрения, а остальные он замечать не намерен.
 
Сверху