Интерфейсы, именование, равнозначность с абстрактными классами

Adelf

Administrator
Команда форума
В ларавеле такое поведение по умолчанию, и оно скорее вредное: бывает, подсунет автокомплит вместо интерфейса конкретную реализацию, не заметишь, потом удивляешься.
В том числе и поэтому я сознательно нарушаю PSR и интерфейсы называю ISomething как принято в сишарпах.
 

Вурдалак

Продвинутый новичок
В том числе и поэтому я сознательно нарушаю PSR и интерфейсы называю ISomething как принято в сишарпах.
А зачем вообще как-то выделять интерфейсы? Почему не просто Something?
Туплю, почитал о чём речь. Ну, если тип называется просто Something, то интерфейса-то может быть и нет? А если есть, то реализация должна как-то по-другому называться: MysqlSomething, RedisSomething. То есть, ты просишь передать в конструктор какой-то сервис, а интерфейс это или класс — это не важно.

Я после этой статьи перестал использовать различные маркеры интерфейса: http://verraes.net/2013/09/sensible-interfaces/
 

Adelf

Administrator
Команда форума
Ну у меня это скорее привычка с сишарпа. Там не так многословно как в яве.
class Something : BaseSomething, ISomething.
Ни extends ни implements. Т.е. чтобы в этой записи различать что где удобен префикс I.
В яве привычки другие. interface Something. SomethingImpl implements Something. Или какой-нибудь FooSomething implements Something.
Вероятно, если приучить себя и весь проект вести с такими привычками, то будет неплохо.
 

Вурдалак

Продвинутый новичок
Ну у меня это скорее привычка с сишарпа.
Ну, просто «привычка» — это такой, хиленький ответ. :) Тебе же зачем-то нужно понимать, что перед тобой интерфейс или класс, чтобы использовать сервис и не очень ясно зачем. Это важно, если изучаешь реализацию самого сервиса, но тут и так язык подскажет что перед тобой с помощью ключевых слов (interface, class).

Наличие подобных соглашений ведет к бедному неймингу, когда человек называет интерфейс IFoo/FooInterface и класс таким же образом: Foo. Тут либо тогда не нужен интерфейс, либо нужно указать в имени чем же так специфична реализация Foo. Ведь предполагается, что она может быть не одна.

Ни extends ни implements. Т.е. чтобы в этой записи различать что где удобен префикс I.
Мне нравится эта языковая особенность тем, что можно сначала указать интерфейс, а потом поменять на абстрактный класс, если требуется, без нарушения BC с высокой долей вероятности (т.е. нужно чтобы consumer не наследовал класс от какого-то другого). Uncle Bob писал о чём-то подобном: http://blog.cleancoder.com/uncle-bob/2015/01/08/InterfaceConsideredHarmful.html
 

fixxxer

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

Adelf

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

Наличие подобных соглашений ведет к бедному неймингу, когда человек называет интерфейс IFoo/FooInterface и класс таким же образом: Foo. Тут либо тогда не нужен интерфейс, либо нужно указать в имени чем же так специфична реализация Foo. Ведь предполагается, что она может быть не одна.
Реализации часто бывают как минимум две - реальная и мок для тестов.
 

Вурдалак

Продвинутый новичок
Ну для меня идеальный мир там, где все зависимости только на интерфейсы
Совсем недавно обсуждали бессмысленность, например, интерфейсов 1-в-1 к value objects и сущностям — здесь не предполагается других реализаций. Часто это касается также бизнес-сервисов, где интерфейс лишь вводит двусмысленность: якобы, у нас может существовать две и более ветвей бизнес-логики, которые определяются где-то на уровне DI. Я считаю вредным вводить в таких случаях интерфейсы и, соответственно, никак не могу принять это за «идеал»: там, где нужно устранение любых двусмысленностей, я ввожу зависимость от вполне конкретного класса. Если некие «ветви» бизнес-логики и существуют, то они должны быть явно отражены в самом сервисе, а не в DI-конфигурации.

Также я могу вспомнить случаи, когда мне казалось удобным поменять интерфейс на final/abstract класс (и наоборот), и отсутствие суффикса приводило к тому, что для клиентов код никак не менялся, дифф был минимальным.

Я советую почитать пост Uncle Bob'а, я хоть и не полностью с ним согласен, но я с ним солидарен, что явное разделение интерфейсов и классов имеет свои негативные моменты.

В данном случае можно не добавлять префиксов-суффиксов в интерфейсы.
Я не совсем понял взаимосвязь с «везде интерфейсы». Как именно тебе мешает отсутствие маркера в ситуации, когда ты точно не знаешь что ты используешь? Тебе нужен репозиторий с пользователями, ты инжектишь «UserRepository», какая тебе разница класс там или интерфейс?

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

Вурдалак

Продвинутый новичок
У меня временами так получается, что это различие уходит в неймспейс. Наверное, имеет смысл дублировать и в классе, для большей ясности.
Использование алиасов и неймпейсов для разрешения данной проблемы — это как перепрыгивать забор вместо поиска калитки :)

Название реализации можно дать условно любое, хоть MyAwesome*. Это ж только в DI должно фигурировать, должно быть пофиг.
Но если реализация и прям выглядит как единственно верная, то возникает вопрос: нужен ли интерфейс тут вообще?
 

Adelf

Administrator
Команда форума
Совсем недавно обсуждали бессмысленность, например, интерфейсов 1-в-1 к value objects и сущностям
Я хотел это отметить, что мое утверждение не касается этих классов, но подумал что это очевидно ) Там же IoC упомянут )
Тебе нужен репозиторий с пользователями, ты инжектишь «UserRepository», какая тебе разница класс там или интерфейс?
Для меня важно, чтобы класс со всем остальным миром был связан тонкими связями(кроме конечно системных и Entity/VO классов). Для меня это означает, что я могу взять его отдельно и препарировать как душе угодно в тестах. Чтобы это были действительно юнит-тесты, а не "вынужденно-интеграционные". Другой сильной причины сходу назвать не могу. Но она наверняка есть.
 

Вурдалак

Продвинутый новичок
Я хотел это отметить, что мое утверждение не касается этих классов, но подумал что это очевидно ) Там же IoC упомянут
Ну OK, я там же привёл другой пример — бизнес-сервисы.

Для меня важно, чтобы класс со всем остальным миром был связан тонкими связями(кроме конечно системных и Entity/VO классов).
Ну так и в чём проблема? Если ты всё это ведёшь к тому, что если там будет класс, и он будет final, то придётся обойтись без мока, то это, на мой взгляд, лишь некоторое упущение PHP, мне кажется было бы разумным с помощью рефлексии обходить это ограничение (так сделано в Hack, например). В конце концов, лучше уж final снять (можно заменить на phpdoc @final), чем добавить какой-то бессмысленный интерфейс для unit-тестов.
 

Adelf

Administrator
Команда форума
Мокать реальный класс... это не по феншую. Но да, тесты это как-то неубедительно. Может завтра придумаю что-то более сильное.
 

Вурдалак

Продвинутый новичок
Мокать реальный класс... это не по феншую.
Откуда у тебя подобное убеждение?

Я раньше (и частично сейчас) тоже придерживался подобного мнения, при возможности я стараюсь просто создавать конкретный инстанс. Но бывают ситуации, когда я вижу бессмысленность отдельного интерфейса, но мокать хочется, поскольку с практической точки зрения создавать обычный инстанс нерационально и/или там получается большая цепочка конкретных классов, которая (о боже) может закончиться «внешним миром»: база данных (прямо вот SQL-запросы), запросы к внешнему API, etc., где мокать уже нельзя.

Но если мы это будем считать, что мы мокаем контракт (нечто обобщающее понятия class/interface), то всё меняется. И правда, если придерживаться мнения, что мы зависим от контрактов, а не от классов/интерфейсов, то какая нам разница что именно мы мокаем? Главное — соблюдать этот контракт. А сам контракт может видоизменяться: становится интерфейсом или наоборот.
 

Вурдалак

Продвинутый новичок
Кстати, напомню про ситуацию с интерфейсами в Java, где ввели понятие default method. У меня складывается впечатление, что здесь изначально была некоторая ошибка в проектировании языка, о которой уже упоминалось в статье выше.

Основная суть такого разделения — это решение проблемы множественного наследования. Есть и чисто технические проблемы (diamond problem), так и проблема восприятия (достаточно трудно читать такой код). Но если бы 1) лексически extends/implements никак не различались как в C# 2) был запрет на наследование двух и более классов с состоянием (читай: с переменными класса), то не пришлось бы вводить default method: абсолютно любой интерфейс можно было бы сконвертировать в абстрактный класс без потери обратной совместимости. А ведь именно этим и является интерфейс с default method — это abstract class без состояния.

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

Adelf

Administrator
Команда форума
Ну с default method интерфейсы превратились в абстрактные классы, которые можно множественно наследовать. В C# возможность добавлять такие методы в интерфейсы была наверно с самого начала. Правда там это делается извне(this параметры). И никакой катастрофы с множественным наследованием, которую пророчили некоторые, не происходит.
Для меня тоже вполне естественно это, некое слияние абстракт-классов и интерфейсов. И зависеть от абстрактного класса по мне тоже нормально. Вот с реальными... у меня какая-то внутренняя проблема. Вероятно это связано с использованием много лет назад слабых мок-библиотек, у которых проблемы с реальными классами.
И все-таки, завися от реального класса(тем более финального), между этими двумя классами теперь большая связь. Вижу в этом нарушение Open Closed принципа.

Использование алиасов и неймпейсов для разрешения данной проблемы — это как перепрыгивать забор вместо поиска калитки :)
В ларавеле это бесит постоянно. Описываешь зависимость, и потом приходится выбирать из какого нэймспейса оно. Кстати "Respect the contract" из статьи тоже про ларавель )
 

Adelf

Administrator
Команда форума
И все-таки, завися от реального класса(тем более финального), между этими двумя классами теперь большая связь. Вижу в этом нарушение Open Closed принципа.
Хотя, если можно поменять на интерфейс, то по большому счету никакого нарушения нет. если мы не композер-пакет конечно делаем.
 

Вурдалак

Продвинутый новичок
И все-таки, завися от реального класса(тем более финального), между этими двумя классами теперь большая связь. Вижу в этом нарушение Open Closed принципа.
Во-первых, OCP — это про изменение на регулярной основе. Класс должен стремиться к точке «равновесия»: когда изменения вносить не придётся, но по пути к этой точке он может и должен меняться. Если понадобится там интерфейс вместо final class — поменяешь. Из утверждения, что зависимость от final class — нарушение OCP, следует, что эта зависимость именно от интерфейса обязательно понадобится. Это заведомо ошибочная предпосылка, я уже приводил примеры, когда интерфейс будет вреден и вводить его никто не будет.

Во-вторых, напомню про coupling vs cohesion. Ты под «зависимостью», судя по всему, обязательно понимаешь coupling. В то время как два класса могут быть специально жестко взаимосвязаны для повышения так называемой связности. Это как в том давнем примере с расчётом расстояния, где был захардкожен радиус Земли прямо в сущности. Помнится, ты возмущался, что это жёсткая зависимость. Да, это жёсткая зависимость, потому что мы делали модель расчёта конкретно для земных условий, а не абстрактную модель в вакууме для любой планеты. Там было что-то связано с путешествиями. Вот будут у компании Acme туры по Марсу, можно будет подумать об изменении модели. Хотя даже в таком случае, вероятно, проще сделать две отдельные модели, потому что они могут сильно отличаться.

В-третьих, возвращаюсь снова и снова к одной и той же мысли: как раз если у тебя нет никаких маркеров:
PHP:
public function __construct(UserRepository $userRepository)
, то ты не знаешь final class перед тобой или нет. В этом и смысл: ты можешь заменить реализацию контракта и consumer про это даже не узнает. А если ты вводишь маркер, то ты сам вводишь дополнительную зависимость: ты говоришь, что вот это интерфейс/класс и прозрачно поменять это уже не получится.
 

Adelf

Administrator
Команда форума
Я уже намекал, но повторюсь: мне нравится идея безмаркерного именования интерфейса. Но, например тот же репозиторий, обычно у него интерфейс лежит в домене, а реализация в Infrastructure и это разные неймспейсы. Безболезненно поменять не получится. Но это наверно пример неудачный. (upd: тут я немного спутал понятия, и говорю о замене на класс и обратно)
По поводу high cohesion зависимостей, я так подозреваю ты имеешь ввиду классы, которые лежат обычно в одном неймспейсе и кто-то зависит от другого. Такие вещи я зависимостью не считаю. Такие зависимости не должны попадать в контракт класса(в параметры конструктора, etc, можно либо static-методы юзать, либо создавать экземпляр когда нужно через new) и о них стороннему коду можно даже не знать. Нет смысла называть это зависимостью.
В других случаях, когда зависимость идет на другой совсем класс... мне почему-то претит идея мокать реальный класс или подсовывать его в тесты и вообще сама идея просить реальный класс(да, идея с подменой его на интерфейс когда нужно, хорошая и для всех кастомеров это будет незаметно, но я чую какую-то подставу тут). Но, похоже, это моя личная проблема и приводить это как аргумент смысла нет :)
 

Adelf

Administrator
Команда форума
Кстати вспомнил, что разок делал приложение с помощью TDD и было крайне удобно все зависимости через интерфейс, поскольку реализации нужного интерфейса часто пока еще не было, но это не мешало писать классы зависящие от него и тесты к ним.
Плюс погуглил тут специально, в C# библиотеке для моков Moq, чтобы мокать методы реального класса, нужно чтобы они были virtual. В PHP понятное дело такой проблемы нет, у нас все виртуальное. Но там это мешает. Поэтому у меня и образуются такие привычки, переходящие в другие языки.
 
Последнее редактирование:

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Adelf, я не могу согласиться, что классы в одном неймспейсе зависимостью не считаются. Считаются. Если есть модели Image и Item, Item включает в себя коллекцию Image, то это - зависимость. Изменение контракта Image сломает методы Item.
 
Сверху