Comments 123
Мысль понятна. Не понятно, как автор к ней пришел, точнее к ситуации, когда появилась потребность анализировать подход настолько поверхностно. Тема не развернута, лично я до конца не понял, что именно автора так задело, т.к. проектирование "от ui" является наиболее логичным, когда создаёшь продукт, т.е. отправной точкой для mvp должно быть удовлетворение конкретных потребностей продукта (сервиса), а все навороты и фичи - уже потом?
При этом сам не раз сталкивался, и, можно сказать, специализируюсь даже, на случаях когда "под капотом все не очевидно", но и тут всегда все отталкивается изначально от покрытия потребностей, а код - всего лишь способ реализовать это покрытие, теми или иными путями.
Здесь речь идет лишь о принципе программирования и программных, а не ui интерфейсах.
Мысль понятна. Не понятно, как автор к ней пришел
"Одно из моих любимых занятий - разбираться во внутреннем устройстве и работе используемых open-source программ и библиотек."
Путем анализа работы других авторов - пришел к выводу, что это наиболее рабочий подход и так как есть положительные результаты - программы и библиотеки с хороших кодом, то этот подход является эффективным и наиболее часто используемым.
Соглашусь с автором, что правильно спроектировать интерфейсы до реализации нереально.
проектирование "от ui" является наиболее логичным
Не все программы и библиотеки имеют "ui"
Но все имеют пользователей. В случае библиотеки - замените UI на «внешние интерфейсы», в случае бэкенда - на API. Суть одна - есть клиент и весь код нужен чтобы на какой-то его запрос давать какие то ответы - то какого типа есть запросы у пользователя и какого типа должны быть на них ответы и есть user interface. А то что пользователь - программист и сам внешние методы вашей либы дергает - это уже детали.
Принцип программирования на уровне интерфейсов ошибочен и приводит к плохой архитектуре -- название противоречит содержанию: автор приходит к мысли, что:
В больших проектах невозможно сразу продумать оптимальные интерфейсы, поэтому не спешите цементировать код. -- кратко вся статья
Программирование на уровне интерфейсов -- огромное преимущество ООП, без этого сегодня ни один большой проект не обойдётся. А проблема заключается в создании оптимальных интерфейсов.
Программирование на уровне интерфейсов -- огромное преимущество ООП, без этого сегодня ни один большой проект не обойдётся
Я бы с вами не согласился, интерфейс понятие более общее и существуют в отдельности от ООП. Как тогда по вашему работают все большие проекты написанные без использования данной парадигмы?
Если совсем кратко, то я бы еще охарактеризовал так:
Подобный принцип программирования заставляет на раннем этапе вводить неоптимальные интерфейсы/абстракции, вокруг которых пишется остальной код, который их использует, что в большинстве случаев приводит к лишним проблемам и постоянным корректировкам этих интерфейсов и сопутствующего кода по мере дальнейшей разработки. Или, что еще хуже, оставления этих первоначальных интерфейсов без изменений.
Добрый день. Мне очень интересна Ваша точка зрения, в основной ее антидогматичностью и, простите, контринтуитивностью. Но все же я не совсем программист наверное мне не понять вашей идее в том виде, как Вы ее представили. Объясните, пожалуйста, на моем примере.
Сейчас я пишу исследовательскую программу. Ее отдельные части - это алгоритмы, которые что-то делают с данными. Я понимаю, что мое представление о том, что конкретно должны выполнять эти алгоритмы и как им быть оптимально реализованными, с ходом времени может меняться, однако форматы их входов и выходов - имеют куда более стабильный вид.
Я заметил, что если все алгоритмы оформлять сначала как абстрактные классы с виртуальными методами иногда даже без внутренних полей, то вносить изменения становится гораздо легче. Когда в моей голове появляется новая реализация, я создаю еще один дочерний класс и передаю его в остальную программу через указатель на базовый. Весь остальной код при этом никак не меняется.
Если по-вашему метод интерфейсов плох, то что вместо него я должен тогда использовать, чтобы большая программа по-прежнему допускала представление, как собранная из конструктора модель?
Автор статьи и не говорил, что нужно отказаться от интерфейсов. Как я понял, автор имел в виду, что интерфейсы можно назвать "нормально работающими", если можно в большую программу добавить новый функционал без перелопачивания всех абстракций. У вас как раз случай "здорового" использования интерфейсов.
Здравствуйте, в первую очередь, я хочу заметить (как уже делал это в другом комментарии), что я нигде не говорил, что интерфейсы, это плохо. Я говорил, что принцип программирования, в котором предполагается проектирование интерфейсов между всеми частями программы до реализации самого функционала, в большинстве случаев ошибочен и приводит к плохой и неоптимальной архитектуре.
Интерфейсы и абстракции, это несомненно важные вещи. Вопрос в том, насколько они удобны и оптимальны, а также, каким способом они достигаются.
Мне тяжело сказать что-то конкретное по поводу вашего примера, так как я не видел ни конкретной задачи и данных, ни вашего кода. Возможно у вас нет никаких проблем.
Я говорил, что принцип программирования, в котором предполагается проектирование интерфейсов между всеми частями программы до реализации самого функционала, в большинстве случаев ошибочен и приводит к плохой и неоптимальной архитектуре.
И как вы тогда предлагаете действовать например в ситуации когда эти самые отдельные части программы пишутся различными людьми или даже командами или даже фирмами?
Это ситуации вынужденного введения интерфейсов. Они есть в каждом проекте, но количество подобных интерфейсов всегда невелико (зависит от размера проекта).
Я не знаю в каких проектах работаете лично вы. Но я бы не сказал что у нас количество подобных интерфейсов невелико :)
И дальше например есть ситуации когда у вас могут быть различные имплементации для каккого-то функционала. Как вы в этой ситуации обходитесь без интерфейсов?
То есть как уже вам здесь написали много раз: если у вас простые проекты, то интерфейсы не особо нужны или даже мешают. Но далеко не у всех проекты простые...
Я признаю, что моя формулировка может трактоваться по разному, но я нигде не писал, что я против интерфейсов или то, что их не нужно использовать, или без них можно вообще обойтись. Моя критика была направленная на конкретный догматический принцип написания кода, который постулирует, что нужно пытаться придумывать/проектировать интерфейсы без предварительной реализации функционала. Это не относится к более-менее большим модулям программы, над которыми могут работать несколько человек или команд. В этом случае вам по любому придется договаривать и вводить интерфейсы между этими модулями, хоть даже временные. Я говорю о применении данного принципа при написании функций, классов и структур данных, когда в преждевременном введении интерфейсов нет никакой необходимости.
Моя критика была направленная на конкретный догматический принцип написания кода, который постулирует, что нужно пытаться придумывать/проектировать интерфейсы без предварительной реализации функционала.
В обоих описанных мною случаях вам скорее всего придётся начинать с интерфейса. И никто нигде не говорит что интерфейс в принципе не может меняться со временем. Особенно на стадии начальной разработки.
Я говорю о применении данного принципа при написании функций, классов и структур данных, когда в преждевременном введении интерфейсов нет никакой необходимости.
Ну вот банально если вы работаете с любой более-менее модульной архитектурой, плагинами, юнит-тестами/моками или тем же dependency injection, то вам от интерфейсов скорее всего никуда не деться. С другой стороны создание и подержка интерфейсов практически "бесплатна" и не занимает время если у вас есть IDE с адекватным функционалом.
Я думаю, что без конкретики мы далеко не уйдем. Так как тут не совсем понятен размер проекта, и что подразумевается под модулем. Возможно если вы работаете над готовым проектом, с уже имеющимися интерфейсами и дополняете ее модулями, то в вашем случае, это удобный вариант. Я не собираюсь навязывать свою точку зрения, я ее озвучиваю и признаю, что в отдельных ситуациях такой подход может работать, но вводить интерфейсы без необходимости при начальной разработке, это плохое решение.
С другой стороны создание и подержка интерфейсов практически "бесплатна" и не занимает время если у вас есть IDE с адекватным функционалом.
Да, и еще, что я заметил на своем опыте, что преждевременное введение интерфейсов и локальных оптимизаций(привет Кнуту), скрывает от тебя более глобальные оптимизации. И если взять и переписать решение "в лоб", без абстракций, то можно увидеть намного более удачные абстракции, чем те, что были прежде. А это IDE вам не поможет обнаружить.
И если взять и переписать решение "в лоб", без абстракций, то можно увидеть намного более удачные абстракции, чем те, что были прежде. А это IDE вам не поможет обнаружить.
Во первых это уже зависит от IDE и тулинга в принципе. Различные анализаторы они тоже развиваются и на месте не стоят. И такие вещи это скорее задачи техлидов/архитектов, а не среднего программиста.
А во вторых чем вам интерфейсы то в данном случае мешают? Вам же никто не запрещает и на конкретную реализацию смотреть. Это же не так что если интерфейс есть, то всё что за ним автоматом становится невидимым....
Во первых это уже зависит от IDE и тулинга в принципе. Различные анализаторы они тоже развиваются и на месте не стоят
Я сомневаюсь, что такие анализаторы существуют, которые могут находить более глобальные оптимизации автоматически. К тому же, понятие оптимизации, зависит от конкретной цели. У всего есть обратная сторона, и в одном случае изменение может считаться оптимизацией, а в другом нет.
А во вторых чем вам интерфейсы то в данном случае мешают? Вам же никто не запрещает и на конкретную реализацию смотреть. Это же не так что если интерфейс есть, то всё что за ним автоматом становится невидимым....
В теории да, но на практике очень сложно держать в голове множество реализаций и каждый раз заглядывая внутрь. Общая картина теряется.
Я сомневаюсь, что такие анализаторы существуют, которые могут находить более глобальные оптимизации автоматически.
Они как минимум могут подсказать вам где стоит поискать.
В теории да, но на практике очень сложно держать в голове множество реализаций и каждый раз заглядывая внутрь. Общая картина теряется.
Это если у вас есть множество реализаций. И если они есть, то вам без интерфейса вообще по хорошему не обойтись. То есть на мой взгляд в таком случае необходимость интерфейса даже обсуждать глупо.
А вот если у вас интерфейс это просто "контракт" и существует только одна реализация, то на мой взгляд вообще нет никакой разницы в контексте оптимизации и нахождения "более удачных абстракций". В куче IDE даже можно настроить что если реализация одна, то "навигация" по дефолту идёт сразу на неё, а не на интерфейс.
Это если у вас есть множество реализаций. И если они есть, то вам без интерфейса вообще по хорошему не обойтись. То есть на мой взгляд в таком случае необходимость интерфейса даже обсуждать глупо.
Еще раз, мною нигде не сказано, что интерфейсы не нужны, я не понимаю, почему вы продолжаете это утверждать. Я говорю, что их преждевременное введение без необходимости, часто создает проблемы по ходу дальнейшей разработки. Интерфейсы это всегда границы, так вот, если в начале разработки, эти границы были определены неправильно (что происходит часто, так как проект большой и сделать это правильно и оптимально до реализации практический невозможно), то хоть это может звучать контр-интуитивно, вам нужно убрать эти границы, посмотреть на код без них и провести новые, более оптимальные.
Я говорю, что их преждевременное введение без необходимости, часто создает проблемы по ходу дальнейшей разработки.
А я не оосбо понимаю что такое "преждевременное введение без необходимости" в вашем понимании.
Вот какая разница есть у вас интерфейс с сигнатурой функции или просто публичная функция с такой же сигнатурой? Какие конкретно недостатки имеет первый вариант по сравнению со вторым?
Ок, я полагаю, что путаница может возникать из за разных трактовок слова интерфейс. Мы вроде бы не трактуем слово интерфейс в узком ООП смысле. Об этом было написано выше.
Я бы с вами не согласился, интерфейс понятие более общее и существуют в отдельности от ООП. Как тогда по вашему работают все большие проекты написанные без использования данной парадигмы?
Под словом интерфейс предполагается совокупность методов и функций и тогда вопрос теряет смысл.
Я всё ещё не особо понимаю в чём проблема. Вот у нас есть вариант:
class Foo
{
public void GetFooData(int dataId)
{
.....
}
}
И вариант:
class Foo :IFoo
{
public void GetFooData(int dataId)
{
.....
}
}
interface IFoo
{
void GetFooData(int dataId);
}
Это "интерфейс в узком ООП смысле"? Или нет?
И какие конкретно недостатки будет иметь вариант с интерфейсом по сравнению с вариантом без оного?
Недостаток имеет вариант, когда вы заранее решили, что вам нужен именно метод GetFooData, и что он должен принимать именно int dataId, хотя возможно вместе с id, во многих местах, вам будет нужен еще и String name и оптимальнее было бы сделать структуру данных содержащую id и name. Но так как вы уже определили такой интерфейс (в глобальном смысле) взаимодействия с Foo, то в остальных местах, где вам нужно еще и name, вы можете добавить дополнительные ненужные и неоптимальные функции, которые получают name. А то, что у этого сразу определен еще и ООП интерфейс уже вторично. Естественно с одним, двумя и тд классов, это не кажется большой проблемой, но мы же говорим о более крупных проектах, где их десятки и сотни, и они взаимодействуют друг с другом.
Недостаток имеет вариант, когда вы заранее решили, что вам нужен именно метод GetFooData, и что он должен принимать именно int dataId, хотя возможно вместе с id, во многих местах, вам будет нужен еще и String name и оптимальнее было бы сделать структуру данных содержащую id и name.
Ну ок, я определил такой интерфейс в случае с интерфейсом. И определил публичную функцию в случае без интерфейса. Но на мой взляд озвученные вами претензии одинаково применимы или не применимы в обоих случаях.
Ну или почему вы считаете что функцию я могу менять как хочу, а интерфейс нет? В чём разница?
Но так как вы уже определили такой интерфейс (в глобальном смысле) взаимодействия с Foo, то в остальных местах, где вам нужно еще и name, вы можете добавить дополнительные ненужные и неоптимальные функции, которые получают name.
Или я просто при необходимости меняю интерфейс. Это же не запрещено.
Ну ок, я определил такой интерфейс в случае с интерфейсом. И определил публичную функцию в случае без интерфейса. Но на мой взляд озвученные вами претензии одинаково применимы или не применимы в обоих случаях.
Ну или почему вы считаете что функцию я могу менять как хочу, а интерфейс нет? В чём разница?
Мне кажется, что мы друг друга не понимаем. В предыдущем комментарии, я как раз и пытался показать, что разницы, как это называть (просто публичная функция или интерфейс) никакой нет. И публичные функции тоже входят в понятие интерфейсов, как подмножество. А главная проблема начинается тогда, когда вы заранее пытаетесь определить и выявить все функции и структуры данных с которыми они работают в вашем приложении.
Но так как вы уже определили такой интерфейс (в глобальном смысле) взаимодействия с Foo
Имелось ввиду вашу публичную функцию, поэтому я написал интерфейс в глобальном смысле слова.
А главная проблема начинается тогда, когда вы заранее пытаетесь определить и выявить все функции и структуры данных с которыми они работают в вашем приложении.
В смысле? Вы сначала пишите отдельны части программы без всяких публичных методов и только потом когда они готовы пытаетесь собрать их в единое целое? Или как?
Тогда я бы сказал что ваша статья не о интерфейсах, а о том что иногда bottom-up подход логичнее чем top-down. Ну ок, да. В отдельных случаях :)
Ок, остановимся на том, что мотивацией статьи (как я указал в обновлении), было представление альтернативной точки зрения, догматическому принципу "всегда программируйте на уровне интерфейсов", который часто можно услышать от разных "гуру" в мире разработки. Ну и услышать мнение других :)
Улавливаете, о чем я? В общем-то, о том же самом — если применять это как догму — ничего путного конечно же не выходит. Но если помнить, что наша цель тут — уменьшение связности (а само уменьшение связности ведет к уменьшению сложности, потому что нужно следить за меньшим числом (или более простых) связей между компонентами), то становится понятнее, для чего принцип собственно нужен.
А уменьшение сложности — это и повышение надежности, а обычно и оптимизация производительности где-то тут зарыта, как следствие, потому что вы начинаете лучше понимать свою систему, где у нее узкие места, и как их расширить.
Если так на это дело смотреть — то не особо то он и догматичный получается, не правда ли?
С вашей формулировкой тяжело спорить :)
Но я попробую ответить так:
Нравятся ли мне цели декларируемые данным принципом? (уменьшение связанности и сложности, увеличение надежности и производительности) - Да, конечно.
Стал бы я использовать данный принцип, если бы он приводил к этим целям? Да, конечно. Зачем же мне отказываться от такого замечательного инструмента?
Приводит ли следование данному принципу к обещанным целям? - По моему опыту и наблюдениям, в большинстве случаев нет.
О причинах этого (в моем представлении), я попытался указать в статье и комментариях выше.
Еще, я бы добавил общий пассаж на счет того, что подобные принципы (SOLID в том числе), на практике мало полезны - так как, все согласны, что бездумно применять принципы нельзя, а для того, чтобы знать где их можно и нужно применять, да и вообще границы применения, нужет опыт, но опытным программистам принципы не нужны, так как у них есть опыт, который намного обширнее и полезнее принципов, а у начинающих программистов опыта нет, и они не знают когда можно, а когда нельзя применять принципы, но когда этот опыт появляется, то потребность в принципах уже отпадает :)
На это можно возразить, что следование принципам начинающими программистами всегда дает лучший результат, чем если бы они писали без них. Но это тоже не всегда так. Я встречал примеры решения задач, по которым можно было бы изучать все паттерны проектирования :) И тогда думаеш - лучше бы они их не знали и писали просто.
Приводит ли следование данному принципу к обещанным целям? — По моему опыту и наблюдениям, в большинстве случаев нет.
Тут я совершенно согласен. Все принципы (а ля SOLID) — это не более чем полезные советы. Прямого результата они как правило не дают — они лишь дают направление движения. В конце концов, взять хоть любой принцип, ну хоть инверсию зависимостей — а представим, что начинающий программист как-то интуитивно сразу написал так, что все верно. Если он зависимости буквально инвертирует — станет же хуже. То есть, надо все-таки понимать, как хорошо, а как плохо, и желательно еще — почему.
>когда этот опыт появляется, то потребность в принципах уже отпадает :)
Ну, я бы сказал, что некоторые вещи просто делаешь на автомате. Потому что уже узнаешь знакомые паттерны, и знаешь точно, как будет хорошо. Или хотя бы — как точно будет плохо. То есть не то что отпадает использование принципов — а просто скорее не думаешь в их терминах «а вот тут надо LSP».
>лучше бы они их не знали и писали просто.
Тоже да. До принципов надо хоть немного, но дозреть. Просто прочитать книгу — недостаточно.
Мне больше нечего добавить.
Вообщем, хотел бы поблагодарить Вас и остальных людей за критические комментарии, так как еще раз размышляя над ними, я более четко прояснил для себя, в чем заключается моя главная неприязнь к различным "принципам программирования", а точнее тому, как они пропагандируются их адептами.
Постараюсь обобщить примером:
От адептов разных методологий, мы слышим советы в стиле "просто используйте принцип Х" и вы получите быстрый, надежный и расширяемый код. Но я по своему опыту знаю, к каким последствиям приводит "просто используйте принцип Х", и эти последствия чаще всего хуже, чем если бы этот принцип вообще не использовался.
Когда я начинаю приводить конкретные примеры таких ситуаций, то в ответ слышу "вы просто неправильно поняли этот принцип", после чего идет ссылка на 100501-ю статью "Что такое X на САМОМ ДЕЛЕ", в которой приводится очередной кастрированный пример, не имеющий ничего общего с реальными ситуациями в проектах.
Или звучит фраза - "Ну естественно он не всегда применим, просто нужно знать, когда его применять".
Стоп, но в вашем принципе нигде не сказано о ситуациях, когда он применим, а когда нет. Я не собирался спорить с тезисом, что применительно к определенным ситуациям, этот принцип помогает. Главная проблема заключается в том, как эти ситуации определить программисту не имеющему достаточного опыта. Вообщем в подобных дискуссиях я видел яркий пример "Истинного шотландца".
То есть полезность принципа никак нельзя опровергнуть, так как любой контрпример парировался подобным образом.
Когда вам дают советы "просто используйте принцип Х", то пытаются продать “серебряную пулю”, но чтобы воспользоваться этой “серебряной пулей”, вам нужно нечто, как минимум равнозначное ей (опыт).
Так никто не говорил, что интерфейсы не нужны. Говорилось о том как лучше их создавать. В начале (когда еще нет никакого практического опыта реализации) или после реализации, когда уже набил шишек и понимаешь какой интерфейс будет правильным.
Спасибо за ответ и уточнение. По моему опыту сами интерфейсы, как и реализация классов, могут быть подвержены изменениям во благо эффективности и логической прозрачности кода. Мне кажется, что интерфейсы и реализация - это что-то вроде микро и макроуровней программирования: каждый из них в случае необходимости должен быть доступен изменениям. Наверное, последняя мысль как-то пересекается с посылом Вашей статьи.
Пожалуйста. Конечно, если нет жестких внешних требований, то все должно подвергаться изменению. В ваших терминах, я бы еще сказал, что чем позже вы начнете выстраивать макроуровень, тем лучше. Так как у вас побудет лучшее понимание задачи и функционирования вашей программы, что даст более подходящую и оптимальную структуру для постоения этого макроуровня.
В больших проектах невозможно сразу продумать оптимальные интерфейсы, поэтому не спешите цементировать код. -- кратко вся статья
Вы согласны?
Скрытое оскорбление
Просто согласитесь, что статья незавершённая, содержание противоречит заголовку. Мнение автора не считается аргументом.
Афтар, почитай книги дяди Боба.
Мне кажется, что посыл автора в целом верный, но не понятно, как он относится к заголовку.
Проблема относится скорее к сложностям развития и изменения требований для программных компонентов.
То есть на любом этапе важная часть проектирования - попытаться выделять сущности, скрывающие детали реализации - так их использование будет проще в дальнейшем.
При изменении деталей реализации может потребоваться изменить интерфейс, абстракции не идеальны. А может и не потребоваться.
Авторы популярных библиотек просто прошли в этом чуть дальше, а их абстракции лучше протестированы и обточены.
То есть на любом этапе важная часть проектирования - попытаться выделять сущности, скрывающие детали реализации - так их использование будет проще в дальнейшем.
Как раз в этом я и вижу проблему, так как правильно и оптимально выделить сущности до конкретной реализации, очень сложно. Так как приходится пытаться учитывать все различные варианты их взаимодействия и использования. Когда у тебя уже есть реализация, то провести границы, вывести из нее абстракции и интерфейсы становится намного проще
Проектирование, если брать его целиком, одним из первых этапов включает этап прототипирования, когда создаётся прототип, предназначенный для определения верности (валидности) подхода, определения сущностей и отношений между ними, чернового определения границ и точек споряжения.
После прохождения этого этапа уже можно проектировать целевой продукт с учётом известных особенностей предметной области. На этом этапе закрытие реализаций интерфейсом необходимо, так позволяет уменьшить когнитивную нагрузку, распределить силы команды по реализации, вовремя понять, что разбиение по сущностям неверно: при конкретных реализациях невыявленные требования часто протекают между частями и потом потом предоставляют кучу боли.
Этапы анализа и прототипирования часто опускают, если предметная область понятна и есть опыт реализации.
Я так понимаю, что вы опытным путём вышли на необходимость прототипирования и попытались донести эту мысль.
Я с вами согласен. Еще мне кажется, что то понятие "прототипа", которое часто используется в разработке программ, не совсем удачное. Так как при написании программ, в отличие от проектирования физических объектов, накладные расходы на внесение изменений значительно меньше, и поэтому выделять отдельный этап прототипирования, результат которого будет в дальнейшем выброшен, не всегда имеет смысл. Прототипироваванием можно заниматься по ходу разработки.
Процесс проектирования и разработки вполне может быть итеративным и иерархичным: как для всего ПО, так и для его частей.
… выделять отдельный этап прототипирования, результат которого будет в дальнейшем выброшен, не всегда имеет смысл. Прототипироваванием можно заниматься по ходу разработки.
Это практически всегда вредно для достаточно большого объёма предметной области:
разработка подразумевает выполнение требований, а прототипирование — выяснение и уточнение требований, валидация соответствия решения требованиям.
Смешение этих процессов приводит к тому, что на прототип тратится слишком много сил, после чего выкинуть его не позволяет бизнес, оставляя ПО с архитектурой, не соответствующей предметной области, скоплением костылей и сомнительных решений. Т. е. возрастают накладные расходы на внесение изменений.
Ещё отдельно отмечу, что результатом прототипирования являются уточнённые требования к реализации: сущности, их отношения и интерфейсы. Этот результат, естественно, никуда не выбрасывается.
Как только ты работаешь с достаточно большим или абсолютно новым проектом, это становиться попросту нереалистично
Какое отношение масштаб проекта имеет к интерфейсу (API), который всегда относится к отдельному классу? То есть, речь об интерфейсах (во множественном числе) не идет никогда. Речь идет об одном интерфейсе в каждый момент времени, и никаких серьезных проблем масштабирования в этом месте не существует.
Но все эти отдельные классы не живут в вакууме, а взаимодействуют друг с другом внутри вашего проекта по средствам этих интерфейсов .
по средствам
Посредством.
Бывают интерфейсы между классами и и интерфейсы между слоями приложения, и интерфейсы между разными модулями и интерфейсы между разными системами. Бывают интерфейсы бибилиотек, и фреймворков.
И как то странно пытаться для всего этого придумать единые правила.
ошибочен и приводит к плохой архитектуре
Что метод приводит к плохой архитектуре — вообще в статье не продемонстрировано. Показано, что автору так неудобно, он что-то не понимает, и т.п. Это вполне бывает, но это разные вещи. Если кто-то не умеет умножать — ну так он не умеет умножать, а не умножение не работает.
Тут неплохо бы пойти дальше и признать, что код надо непрерывно выбрасывать и кодить заново, причём как на уровне отдельных задач, так и на уровне целых систем.
Хотелось бы увидеть полноценную статью, раз пошло такое дело. Наглядно показать, как знание реализации может ускорить работу программы (например, перебор связного списка или списка на основе массива по индексам), как интерфейсы решают проблему (например, использование итератора для перебора, который гарантирует линейное время). Может ли "опускание" интерфейса значительно упростить архитектуру, бороться с оверинжинерингом?
Я считаю, что в начале должна следовать простая реализация, и лишь по прошествию времени, когда вы лучше поймете задачу и взаимодействие ее элементов, вы сами сможете увидеть повторяющиеся структуры и выделить правильные абстракции и интерфейсы.
То есть сначала писать на уровне реализации, а потом добавить интерфейс? И как это внедрять, расширять? Покажите примером.
Далеко не везде так можно сделать. Иногда использование интерфейсов - это единственный способ подставить мок.
Не все упирается в тестирование. Отдельные интерфейсы могут быть нужны:
потому что один класс может имплементировать несколько интерфейсов (например, для соблюдения ISP клиенты зависят от точечных интерфейсов, но имплементация имеет смысл в виде одного класса);
потому что у вас может быть несколько реализаций одного интерфейса (например, Стратегия);
потому что вы хотите разделить слои, и, например, интерфейс репозитория у вас находится в слое бизнес-логики, а его имплементация – в слое данных / инфраструктуры.
А если у вас уже хорошо выделены интерфейсы, то часто и моки оказываются не нужны, потому что проще подставить тестовую имплементацию.
ну абстракции и вообще инкапсуляция - это не только про проектирование систем
Данная точка зрения не претендует на роль абсолютной истины и является лишь результатом моего опыта, чтения, наблюдений и размышлений.
А зря, звучит как отказ от своей позиции. В конце концов, если вы говорите о чем не как о факте, а как о какой-то позиции (пусть даже "своей"), то это не ваша позиция, т.к. вы от нее абстрагировались, отделились
Полагаю, страшно, что запинают за категоричность. Но ее нет, если не выражать фактичность эксплицитно, вроде "все так, как здесь написано и никак иначе". А если будут пинать по фактам, можно вступать в дискуссию. В конце концов, может, вы правда что-то не учли и ошибаетесь. А может, вы совершенно правы. В любом случае, абстрагирование от позиции выглядит как неуверенность
Ну скажем так, в первую очередь защищать практически любую позицию с претензией на абсолютную истину я считаю бесперспективным, так как любой контрпример нивелирует все ваши усилия. К данной статье я и сам могу привести контрпример :) Но применительно к большинству ситуаций, я готов отстаивать свою позицию, так как она основана на личном опыте, а не просто взята из учебника или статьи.
Понимаю мысль автора статьи.
У меня был, однако, следующий опыт. Нам нужно было, чтобы нам в инструмент тестирования добавили функцию. И я ребятам описал, то как я представляю себе использование такой функции. Прямо отправил им кусок кода с примерами вызова еще не существующих функций, со словами "сделайте так, чтобы это заработало".
И, на удивление, они очень быстро это реализовали и оно работало так как нужно было нам.
Это как бы говорит в пользу планирования на уровне интерфейсов.
Вообще при проектировании функций мне помогает вопрос "как ты будешь это использовать". От этого будет зависеть реализация.
Банальный пример. Мы пишем функцию-модуль, которая будет логиниться на странице, мы можем сделать так, чтобы она заканчивалась сразу после нажатия кнопки "войти" тогда следующий модуль всегда должен будет проверять загрузку страницы после логина. Либо мы можем сделать так чтобы она также брала на себя ответственность довести тебя до страницы за логином. Все зависит от того какие функции-модули будут использоваться после нее. А может имеет смысл сразу сделать несколько параметризированых именем и паролем логин-методов с заходом в разделы приложения. Что предпочтительней - зависит от того, как это будет использоваться далее.
А вот если сначала написать последовательность, а потом оборачивать ее в функцию, потому что мы заметили, что код повторяется тут и там, это как я его называю "реактивный" подход.Он не гарантирует нам, что интерфейс родился "из потребности", он родился "из наличия". Тогда появляются уродливые интерфейсы. И не редко в дальнейшем их приходится переделывать.
В проектировании интерфейсов для электроники, это будет походить на то, когда конечный дизайн будет продиктован нам имеющимися в наличии дееталями. И если это к примеру автомобильный радиоприемник, то мы в лучшем случае получим более навороченый автомобильный радиоприемник, а не современную информационно-развлекательную платформу для автомобиля.
Так что я считаю, что в проектировании интерфейсов, полезнее отталкиваться "от спроса", а не "от предложения".
Point VirtualPointToDevicePoint(Point pt)
. Мощь этого решения была невероятной: можно было ничего не переписывая выводить картинку на любое гипотетическое устройство, хоть на параболоид инженера Гагарина! Можно было не думать о каких-то там масштабах и прочих несущественных деталях! Поскольку я действовал не со зла, а по неопытности, мне удалось обмануть не только себя, но и того чувака, который писал растеризатор. С моей-то стороны всё действительно выглядело просто и универсально, а вот с его… Через неделю он пришёл злой, небритый, невыспавшийся и без обиняков заявил, что после того, что по моей милости он делал со своими алгоритмами все выходные, он, как честный человек должен на них жениться. И что он уже близок к тому, чтобы и со мной проделать то же самое. Пришлось, короче, вернуться к старому интерфейсу. Не понимают эти погрязшие в нюансах программисты тонкую ранимую душу архитектора!Если вы действительно сталкиваетесь с ситуацией, когда интерфейс не способен удовлетворить существующую потребность, то да — нужно идти глубже до уровня реализации. Но не для того, чтобы выполнять свою задачу на этом уровне, а для того, чтобы исправить интерфейс или сделать новый интерфейс, исходя из новой более хорошей абстракции. И после этого опять работаете на уровне интерфейса до тех пор, пока в процессе работы не выкристаллизуется более эффективная абстракция.
Неудачные интерфейсы можно и нужно переписывать. Но из этого вовсе не следует, что интерфейсы вообще не нужны.
Просто не надо возводить эвристические принципы в абсолют.
Абсолютно согласен.
Неудачные интерфейсы можно и нужно переписывать. Но из этого вовсе не следует, что интерфейсы вообще не нужны.
Я нигде не утверждал, что интерфейсы не нужны. Я утверждал, что принцип программирования - "интерфейсы, до реализации" в большинстве случаев ошибочен, хотя конечно бывают ситуации, когда он помогает.
Мотивацией к статье было то, что я слишком часто слышу, как люди пропагандируют разные сомнительные принципы разработки (которые на первый взгляд действительно кажутся очень полезными и логичными), как единственно правильные, best practices и тп.
Я утверждал, что принцип программирования — «интерфейсы, до реализации» в большинстве случаев ошибочен
Так, стоп, давайте определимся — какой именно принцип ошибочен? В какой формулировке?
Потому что сама по себе фраза «интерфейсы до реализации» не может быть ошибочной хотя бы по той простой причине, что она не содержит никакой конкретики. В неё нигде не говорится «нужно полностью и детально проработать все интерфейсы до того, как вы вообще приступите к реализации». В такой формулировке принцип действительно вреден. Но тут вы спорите не с самим принципом, а с конкретной его интерпретацией. И делаете вывод, что он по сути неверен.
А какую вы тогда предполагаете альтернативу? Приступать к реализации не имея даже общего представления интерфейса? Ну, удачи ))
Мотивацией к статье было то, что я слишком часто слышу, как люди пропагандируют разные сомнительные принципы разработки (которые на первый взгляд действительно кажутся очень полезными и логичными), как единственно правильные, best practices
Так сами принципы сомнительные? Или всё-таки проблема в тех людях, которые не умеют готовить эти принципы, и при этом считают их серебрянной пулей?
Карго-культ — это заблуждение, но реальные самолёты вполне себе успешно летают по небу.
Почему принцип программирования на уровне интерфейсов ошибочен и приводит к плохой архитектуре
После такого заголовка я ожидаю как минимум пару примеров, что использование принципа привело к плохой архитектуре. Ну и какое-то обобщение, почему так произошло.
Содержание статьи не соответствует заголовку, на мой взгляд
Я не автор, но пример из виденного могу привести.
protocol ProductProtocol {
var cost: Decimal { get }
var lenght: Double { get }
var cardHolderName: String { get }
}
class SaleProduct: Codable, ProductProtocol {
var cost: Decimal = 0 // приходит с сервер
var lenght: Double = 0 // приходит с сервер
var cardHolderName: String { "" } // всегда пустая строка
}
class GiftCard: Codable, ProductProtocol {
var cost: Decimal = 0 // приходит с сервера
var lenght: Double { 0 } // всегда 0
var cardHolderName: String = "" // приходит с сервера
}
//где в коде функция
func showInfo(product: ProductProtocol) {
if product is SaleProduct {
self.showSaleProductInfo(product)
} else if product is GiftCard {
self.showGiftCardInfo(product)
}
}
По примеру:
1 - в интерфейсе были поля из обоих классов и в случае если класс не должен их иметь, они были с заглушкой. Как было написано в одном из комментариев выше: - хвост виляет собакой.
2 - в функции по отображение информации об элементе, была проверка на тип. То есть типы изначально разные и работа с ними ведётся в по разному оформленных окнах.
Когда это на одной странице, это ещё можно быстро понять, а когда такое размазано по коду то это приводит к курьёзам, когда у SaleProductInfoView появляется показ отдельного окна, для редактирования cardHolderName.
У меня так же есть примеры хорошего использования интерфейсов.
Интерефейсы не существуют в вакууме. Они просто контракт регламентирующий "что это", но не регламентирующий "как Это использовать". Это уже процессы и описывается иначе.
Странно ожидать "идеального решения" в постоянно изменщихся бизнес-процессах.
Вот я долистал до самого низа, уже с желанием написать то же самое. Интерфейс - просто один из видов контракта с пользователем функционала. Одно дело, когда ты пишешь пет-проект в стол, и можно его как попало кромсать хоть ежедневно. Другое - когда ты публично зарелизен, и нарушение контракта приведет в лучшем случае к лучам ненависти от пользователей, в худшем - уходу к конкурентам, которые держатся в рамках ранее данного контракта. Интерфейс здесь - просто удобный инструмент закрепления этого контракта, наравне с дженериками, заголовочными файлами.
Разделяю мнение автора. Так же понимаю почему в комментариях люди жалуются что автор не привёл конкретных примеров. Мне кажется что у автора произошла некоторая профессиональная деформация в следствии изучения кода популярных опенсоурсных проектов. Автор знает как именно выглядет достойный код и на уровне интуиции понимает как подобный код писать. Но когда любитель посмаковать опенсоурсные проекты пытается объяснить что такое "достойный код" людям, которые не особо интересуются опенсоурсными решениями, то получается как в анекдоте про чукчу, который пытался объяснить другим чукчам что такое апельсин.
Я на этот вопрос смотрю под другим углом - интерфейсы, как элемент кода, предназначены не для программ, а для людей.
Сделать идеальный интерфейс невозможно, или очень сложно, и в процессе разработки внутренняя структура будет, безусловно, меняться. Но такое изменение может одновременно отрефлексировать только один человек - который его вносит. Все остальные об этом изменении не узнают, и недостаточно просто прочитать код, чтобы его понять. Другие разработчики будут иметь на подкорке другую версию. И для того, чтобы работа была совместной, у людей должны быть стабильные ожидания от системы.
Таким образом, интерфейсы идеологически разделяют систему на части, которые разные люди могут развивать независимо. И такие разделяющие интерфейсы обязаны быть, и они должны быть стабильными, иначе система перестанет нормально развиваться - в небольших системах все на себя перетянет один человек (т.е. dungeon master), а в больших наступит типичный легаси-хаос. И да, такие интерфейсы зачастую приходится проектировать "пальцем в небо", и с ними приходится мириться.
Отсюда и эмпирическое правило, основанное на законе Конвея - бизнес-домены, интерфейсы и структура команд должны повторять друг друга и меняться совместно.
Некоторые амбициозные методологии ставят своей целью построение "системы-бульона", где интерфейсы в системе очень мелкие, а атомарное изменение может затрагивать множество интерфейсов одновременно. Но живые примеры, которые я видел, выглядят предсказуемо - либо единоличный dungeon master, либо система замирает без развития.
Я думаю, такие мысли рождается в голове, когда человек встречается с тем, что он воспринимает как оверинджиниринг. И обычно оверинджинирингом занимаются другие, а я-то д'Артаньян и пишу понятный код (мне же он понятен!). И тут есть два варианта: 1) код коллеги -- действительно оверинджиниринг, учитывающий миллионы гипотетичных кейсов в будущем -- но это проблема коллеги и к интерфейсам отношения не имеет 2) автор таких мыслей не обладает полными знаниями по бизнес-контексту и не понял архитектуры, поэтому всё списал на "какой-то дикий оверинджиниринг" -- очень удобно, т.к. думать не нужно.
Но подобный когнитивный диссонанс при встрече с кодом, имеющим более, чем одну абстракцию, не говорит нам о том, что это нормально писать спагетти-код с большой связностью и заниматься прочим колхозом. Всё-таки в рамках какого-то конкретного релиза/фичи/MVP набор сущностей примерно известен и конечен и не вижу проблем сразу набросать абстракций, актуальных для данного релиза, а не оттягивать до конца, когда ты уже написал тучу кода и понял, что сова не натягивается на глобус и нужно всё переписывать, так как не было должного предварительного проектирования и не был учтён какой-то маленький, но гордый кейс. Главное не упарываться и не пытаться предугадать все возможные сценарии развития в будущем. Но в целом не вижу больших проблем по умолчанию отталкиваться от работы с интерфейсами, чтобы просто хотя бы уменьшить связность компонентов. Единственный минус -- больше boilerplate, но мы люди привыкшие.
Я думаю, это отчасти некий спор о терминах в рамках воображаемой концепции "мы сейчас спроектируем интерфейс, а потом уже и реализация подъедет". Если же переходить к практическим сценариям, то с одного конца спектра будет "waterfall глазами его противников" (т.е. всё проектируем на бумаге, затем пишем), а с другого конца -- "органический рост", где всё как бы "само" растёт. Но даже в этой концепции как только у вас появляются компоненты А и Б, им же приходится как-то взаимодействовать? Значит, вы всё равно садитесь и делаете интерфейс. То есть вопрос в том, что такое "отталкиваться от", и где мы уже не "отталкиваемся".
Мне кажется, что граница между этими картинками психологическая, и возникает она тогда, когда компоненты растут, как и количество связей между ними. Если у вас есть мелкий класс о трёх функциях, который общается с другим таким же классом, то и проблемы нет: вся задача настолько мелка, что обсуждать, от чего мы отталкиваемся, не особо интересно. Проблема в том, что писать в таком стиле не так уж и просто, а меинстримные языки не позволяют легко изолировать компоненты так, чтобы с первого взгляда было ясно, что компонент может создаваться только в подсистеме А и общаться лишь с компонентами Б и В. Я об этом думал на досуге, но как-то не решился опубликовать мысли по причине их непричёсанности. Может, стоит попробовать дискуссии ради.
Мне кажется, я понял, что хотел сказать автор. Во времена моей молодости, когда я только начал работать со Спрингом (core Java уже был в бекграунде), то во многих книгах, на которых я учился было сказано, что каждая имплементация некоторой бизнес логики (спринговый бин другими словами) должна сопровождаться интерфейсом, я так это запомнил. Доводов к такому подходу было много - это и TDD, это и мокирование в юниттестах, это и реализация нескольких имплементаций одного интерфейса, а также проектирование перед реализацией. И это настолько запало в памяти, что я каждому бину присоединял интерфейс, чаще писал интерфейс, затем реализацию, а потом кромсал и то и другое в процессе рефакторинга и изменения требований. А сильно потом понял, что 95% моих классов с бизнес логикой являются внутренними и не нуждаются в интерфейсах. Они имеют одну единственную имплементацию, мокируются в спринге без всяких проблем, я чаще всего не пишу через TDD. Но зато это сильно облегчило рефакторинг. И если уж вдруг встала необходимость иметь интерфейс к имплементации, то моя IDE может выделить его с помощью пары кликов мыши.
Мне кажется что это частный пример "разработка снизу-вверх vs сверху-вниз". А-ля "делали-делали, а api сказал что так не бывает" vs "все куски работали, а собрали чудищо".
Как-то логика ускользает.
Интерфейсы используются для упорядочивания и формализации обмена данными между крупными модулями системы. Если разрабатываемая система разрослась настолько ,что в ней уже появились большие и относительно независимые модули, то интерфейсы им по любому нужны.
Интерфейс это не что-то жестко "отлитое в граните" (Ц), он вполне может изменяться и совершенствоваться, другое дело что для этого все заинтересованные стороны, работающие с ним, как-то должны обговорить порядок этого развития. Но это тоже логично и важно.
Если следовать принципам SOLID и использовать всю мощь OOP то у вас все получится красиво!
Без картинок? ????
Интерфейс позволяет нам создать контракт и абстракцию, благодаря которому мы не должны заботиться о деталях реализации, и зависимостях. Поэтому проблему неправильного использования интерфейсов можно переформулировать как
Не надо создавать интерфейс там, где не предполагается альтернативной реализации функционала.
Если вы ввели интерфейс в подобном месте, то тут можно увидеть "преждевременную оптимизацию". Вероятно эта проблема уходит корнями в обучающие материалы, где helloworldы показывают возможности, но воспринимаются как обязательные элементы.
Это немного не так: мы всегда создаём интерфейс, когда добавляем публичные методы. Интерфейс -- литерали -- это всё, что видит клиент, точка взаимодействия двух сторон.
Многие языки программирования поддерживают интерфейсы на уровне системы типов, но с декларацией interface или без неё, вы создадите их. Декларация тут ещё и формальный ритуал, который призван символизировать важность выставления тех или иных методов в паблик.
И именно продумывание публичного интерфейса, в реальности, улучшает вашу архитектуру.
Следуя принципу "Программируйте на уровне интерфейсов, а не реализаций", вы на самом раннем этапе ставите ненужные препятствия, пытаясь цементировать элементы, когда четкого и полного представления задачи еще не сформировано, а это в большинстве случаев приводит к неоптимальным решениям и плохой архитектуре
Эм... что? Кто сказал, что наличие интерфейсов означает "цементирование элементов"? Если нужно - все рефакторится, в том числе и интерфейсы.
Напомнило недавний разговор из телеграм-канала по PHP, где, внезапно, ООП противопоставляли DDD.
Интерфейсы - это абстракция над реализацией. И в сложной архитектуре это очень помогает.
Следуя принципу "Программируйте на уровне интерфейсов, а не реализаций", вы на самом раннем этапе ставите ненужные препятствия, пытаясь цементировать элементы, когда четкого и полного представления задачи еще не сформировано, а это в большинстве случаев приводит к неоптимальным решениям и плохой архитектуре.
Как вообще можно браться за проект без четкого и полного представления задачи? Написать "на коленке", после подумать и после всё переписать.... как это по нашему... верно?
Автор почему-то думает что интерфейсы как-то препятствуют изменениям в системе, наоборот это самая замечательная вещь при изменениях, знание того что:
1. При сохранение контракта, мы сколь угодно можем переписывать реализацию и у пользователей моего api (интерфейса) ничего не сломается, значительно упрощает жизнь.
2. Знание того что при изменениях интерфейса, все сломается в нужных местах, пока ты не исправишь реализации оно просто не скомпилиться.
Приведу несколько примеров из жизни за последнее время:
1. Переделывал хранение локализации из properties файлов, в бд -> просто подменил реализации, без необходимости исправлять что-то по всему проекту.
2. Для стороннего api захотели сделать фолбэк, т.к. оно часто падало, просто обернули реализацию в прокси, опять же без необходимости что-то менять.
3. Сторонняя команда, реализовала хранилище нужного нам сервиса на другой бд (ускорив тем самым перфоманс), мы просто поменяли у себя зависимость, и все само заработало.
Всегда считала, что этот принцип применим в тех случаях, когда понятно развитие проекта и куда он будет расти в дальнейшем. Понятное дело, что если требования к нему меняются, то что абстракция, что реализация теряют всякий смысл.
Программирования в терминах интерфейсов и позволяет относительно малой кровью эволюционировать архитектуре.
Да идея то в общем простая. Интерфейсы должны быть такими, как удобно подсистемам, использующим интерфейс, а не подсистемам, предоставляющим интерфейс. Потому что (если мы не совсем упоролись в инверсию зависимостей) код, использующий интерфейсы, скорее ближе к бизнес-логике, а код, предоставляющий интерфейсы, скорее ближе к деталям реализации. А именно код, близкий к бизнес-логике, особенно важно поддерживать в состоянии чистоты.
В этом смысле да, мы можем и должны проектировать интерфейсы сервисов независимо от их реализации. Тем самым перекладывая сложность со стороны клиентов на сторону сервисов.
Если ситуация такова, что мы разрабатываем сервисы, ещё не имея понимания, какие интерфейсы будут нужны клиентам, то да, их придётся по 500 раз переделывать, это нормально, ни о каком цементировании речи нет.
Я считаю, что в начале должна следовать простая реализация, и лишь по прошествию времени, когда вы лучше поймете задачу и взаимодействие ее элементов, вы сами сможете увидеть повторяющиеся структуры и выделить правильные абстракции и интерфейсы.
В этом есть доля правды, когда фичу надо сделать быстро, то архитектурный вопрос можно отложить на потом, НО, скорее всего, это выйдет дороже, чем сделать сразу.
Проектирование в UML диаграммах, достаточно бестолковое занятие, так как я не вижу никаких преимуществ перед непосредственным написанием кода, а скорее недостатки, так как код ты можешь запускать и проверять, а работу UML диаграмм можно лишь представлять в голове.
Этот абзац, как и заголовок не отражает истину - надо мыслить абстракциями, тогда интерфейсы придут сами собой, т.е. продумываем какие модули нам нужны, как они будут взаимодействовать друг с другом. Т.е. на диаграммах можно описать абстракции (контракты), на их основании уже будет проверяться рабочая и удобная ли эта схема. Потом уже переходит в реализацию. Т.е. бежать сразу кодить интерфейсы плохо, сначала нужно продумать все реализацию и продумать взаимодействие, после этого все станет на свои места.
Когда я начал изучать скалу, мне было совершенно непонятно, почему там сначала интерфейсы(trait) пишут, а потом уже реализации. После питона было дико неудобно и казалось, что я напрасно трачу время, лучше сразу класс с логикой запилить. А потом я глянул видео Scott Wlaschin — Talk Session: Domain Modeling Made Functional и понял, что интерфейсы должны писаться не только программистами, а именно специалистами по бизнесу, для которого делают проект, потому что они разбираются в теме гораздо лучше программистов. Тогда интерфейсы имеют больше смысла, так как отражают бизнес модель.
"Программируйте на уровне интерфейсов, а не реализаций" - это не про качество абстракций, а про полиморфизм. Абстракции могут быть такими же уродливыми, как и их первые реализации. Зачастую так и происходит. Но не вводя их и улучшать нечего. За качество отвечают уже другие инструменты. Например, ISP, DIP и т.п.
Do not write code easy to extend - write code easy to delete
Статья срезонировала, но я бы чуть расширил мысль.
Нельзя с первого раза написать хороший код.
Фиксировать интерфейсы, это хорошо, и обычно с них начинают, после беглого прикидывания их реализаций. Но всегда по мере развития проекта проверяются гипотезы, какие-то из них оказываются несостоятельны, могут поменяться требования, могут добавиться необходимые функции. И в какой-то момент код из красивой логичной конструкции становится кучей заплаток и костылей. Это нормально, в этот момент нужно переосмыслить весь полученный опыт, без сожаления перепроектировать все аспекты, к которым есть вопросы и начать новую итерацию.
То есть не сами интерфейсы проблема, а отказ от того, чтобы их переработать с полученным новым опытом.
отличная статья, или, скорее, мнение
проблема в том, что очень часто после написания работающего кода никто не заморачивается с рефакторингом, абстракциями и т.п.
Приходилось одно время работать с кодом, где интерфейсов было, наверное, не меньше, чем файлов с имплементацией. Практически каждый класс типа "я тут приму А и верну В" реализовывал три-четыре интерфейса, которые к тому же друг от друга виртуально наследовались и т.д. И на всё это дело был накручен свой мета язык на сишных макросах, который в свою очередь парсился неким самопальным кодогенератором- обфускатором.
Но хотя интерфейсы в основном торчали снаружи, и это было ещё пол дела. А вот "в нутрях" объекты постоянно кастились к разным интерфейсам через dynamic_cast, чтобы понять, кого же нам передали и что с этим делать. В итоге проект разжирел до состояния макаронного монстра, а главный создатель данного чуда уволился. Люди, которым приходилось поддерживать и расширять продукт, старались ничего не менять без десяти митингов и уточнений, а надо ли это делать, или может быть обойдёмся?
В общем, интерфейсы надо тоже уметь правильно готовить. Плохо, когда их слишком мало, но слишком много тоже не лучше. Интерфейсы должны служить для связи между более крупными модулями - тогда от них будет большая польза.
Большой проект невозможен без документированной структуры и протоколов. Соратникам по коду нужна общая ментальная модель. Интерфейс с документацией -- это тот же протокол.
Уродские интерфейсы обычно результат неполного или неправильного понимания автором интерфейса предметной области либо концепции и назначения интерфейсов. Интерфейс разработанный по принципу советского забора (сначала пишем слово, а потом прибиваем к нему доски), конечно, нафиг никому не нужен.
Интерфейсы - обычный код, соответственно их можно менять как обычный код. Если вы имеете в виду API, то это не только интерфейсы. Но только программирование через интерфейсы обеспечит слабую связанность. И только она позволит писать модульные тесты. И еще много плюшек: простой и надежный код, минимальное количество усилий на поддержку и внесение изменений, параллельная работа множества разработчиков или даже множества команд над одним функционалом. Да даже компилятор будет работать быстрее, если вы будете следовать ISP и SRP и ему нужно будет анализировать ваши классы, содержавшие самый минимум самых простых зависимостей :) Можно много перечислять преимуществ. Итог - все в плюсе.
Но только программирование через интерфейсы обеспечит слабую связанность. И только она позволит писать модульные тесты. И еще много плюшек: простой и надежный код, минимальное количество усилий на поддержку и внесение изменений, параллельная работа множества разработчиков или даже множества команд над одним функционалом.
То, что оно обещает все эти преимущества, это я слышал и читал сотни раз, но на практике получение данных преимуществ я видел очень редко.
Подобная позиция уже обсуждалась в https://habr.com/ru/post/581824/comments/#comment_23567168
Да я видел этот комментарий.
> Приводит ли следование данному принципу к обещанным целям? - По моему опыту и наблюдениям, в большинстве случаев нет.
> но опытным программистам принципы не нужны
Я полагаю, что вы опытный и принципы вам не нужны, так откуда у вас опыт их применения? Часто опытным программистам сложнее адаптироваться к изменениям технологий, концепций, принципов. И на мой взгляд, опытные должны больше тратить усилий что бы принять что-то новое и полезное, так как их опыт им мешает. SOLID – это не новое конечно.
> а у начинающих программистов опыта нет, и они не знают когда можно, а когда нельзя применять принципы, но когда этот опыт появляется, то потребность в принципах уже отпадает
SOLID работают хорошо вместе, выборочно работают плохо.
Как вы пишете модульные тесты с зависимостями без интерфейсов?
Я полагаю, что вы опытный и принципы вам не нужны, так откуда у вас опыт их применения? Часто опытным программистам сложнее адаптироваться к изменениям технологий, концепций, принципов. И на мой взгляд, опытные должны больше тратить усилий что бы принять что-то новое и полезное, так как их опыт им мешает. SOLID – это не новое конечно.
Я пытался следовать SOLID и тп. на протяжении многих лет (о чем сейчас жалею).
Я не хочу никого ни в чем переубеждать. Если вы считаете, что данные принципы помогают вам писать лучший код, то я искренне рад. Моя статья скорее рассчитана на людей, которые заметили, что эти принципы часто не приводят к обещанным результатам, но все вокруг говорят, что такого не бывает и отступать от принципов ни в коем случае нельзя.
Как вы пишете модульные тесты с зависимостями без интерфейсов?
Не знаю, как в других языках, а в PHP можно замокать любой класс и передать его вместо изначальной зависимости, для этого не нужны интерфейсы.
Я думаю, мокать классы можно много где, это не зависит от языка. Но как вы поймете какие методы мокать, а какие нет? Или будете мокать все публичные методы класса? И если нет, то не получится что часть модульных тестов будут тестировать и логику других модулей вместе с из приватными методами? Тогда это уже интеграционные тесты. Представите вы вернетесь к этим тестам через полгода, как просто будет разобраться в этом?
Что если это класс содержит много публичных методов, сколько из них нужно мокать?
Что будет с тестами, когда замоканый класс поменяет логику работы и одни публичные методы станут вызывать другие публичные методы (хоть это и не очень правиьно)?
Что если вы используете наследование и логика изменится в одном из родительских классов?
На мой взгляд, при использовании моков на классы, тесты становятся сложными, хрупкими и часто проверяют не то, что нужно. Код становится сложно рефакторить, а поддержка таких тестов становится дорогой. И в итоге эти тесты становятся обузой, мешающей развитию продукта.
А теперь представьте альтернативный подход применительно к модульным тестам:
- Применяя ISP ваши интерфейсы содержат по 1–2 метода, логически связанных друг с другом (неважно реализованы ли они одним или несколькими классами).
- Все зависимости класса — это лишь несколько таких интерфейса и при выполнении LSP вы можете легко заменить их на другие реализации, в том числе для тестирования в изоляции.
- Каждый класс зависит только от того, что реально использует и быстрый взгляд на конструктор даст полное представление.
- Тесты становятся простыми и тестируют только определенный модуль, не ломаются, если что случилось за его пределами.
- Когда меняется класс (что должно быть не очень часто из-за OCP) или происходит его рефакторинг, можно запустить только тесты на этот класс и быть уверенными, что нет регрессии
Вопрос был о том, как писать тесты без интерфейсов, а не о том, зачем нужны интерфейсы. Тесты без интерфейсов пишутся точно так же, как с интерфейсами. Подменяем реальную зависимость на заглушку, и всё. У интерфейсов есть свои преимущества, но это другой вопрос.
Но как вы поймете какие методы мокать, а какие нет? Или будете мокать все публичные методы класса?
Те же, которые мокали бы в случае интерфейса. Мы же как-то определяем, какие методы вынести в интерфейс. Для интерфейса мокаем все, значит и для класса будем мокать все. А если класс содержит больше 1 ответственности, зто уже нарушение SRP.
То есть сначала делаем класс, который имеет методы из DbConnectionInterface и HttpRequestInterface и является единственной их реализацией, а потом зачем-то разделяем интерфейсами? Может просто не надо делать такой класс?
С интерфейсами эта проблема тоже есть. Есть у вас интерфейс с 2 методами, а класс использует только 1, потому что метода с 1 интерфейсом никто не сделал. Вам все равно надо подменять 2 метода.
Каждый класс зависит только от того, что реально использует и быстрый взгляд на конструктор даст полное представление.
И с реализациями точно так же. Зависит класс только от DbConnection, значит использует только DbConnection. Замена DbConnection на DbConnectionInterface тут ничего не меняет.
Все зависимости класса — это лишь несколько таких интерфейса и при выполнении LSP вы можете легко заменить их на другие реализации, в том числе для тестирования в изоляции.
Тесты становятся простыми и тестируют только определенный модуль, не ломаются, если что случилось за его пределами.
Когда меняется класс (что должно быть не очень часто из-за OCP) или происходит его рефакторинг, можно запустить только тесты на этот класс и быть уверенными, что нет регрессии
И с моками точно так же. Берем зависимость и заменяем ее на мок для тестирования в изоляции. Тесты по простоте вообще не отличаются от тестирования интерфейса с таким же количеством методов, что там что там 3 публичных метода подменить. Можно запустить только тесты на этот класс и быть уверенными, что нет регрессии.
Программные интерфейсы (переменных, функций, классов, компонент) имеют очень важное значение. Именно они определяют внутреннюю архитектуру, позволяют удобно разрабатывать, тестировать и масштабировать приложения. Хороший интерфейс практически невозможно спроектировать с первого и даже со второго раза по многим причинам, среди которых уровень разработчика и степень неизвестности. Но это не повод совсем от них отказываться даже в самом начале. Есть неплохая очевидная техника, о которой уже сказали в комментах - сделать первый вариант и, по мере разработки и развития продукта, рефакторить и видоизменять интерфейсы, видоизменять зоны ответственности. Можно набросать интерфейсы и протестировать архитектуру даже без корректной внутренней реализации. Поэтому с заголовком статьи не согласен от слова совсем. Правильным был бы вопрос "Как проектировать хорошие интерфейсы?".
По поводу вышесказанного и несоответствия заголовка, я в каком-то плане могу с вами согласиться. Но, как я уже указывал не раз в комментариях и обновленнии статьи, моим мотивом было не рассказать о том "как правильно" (я и сам не знаю как привильно), а высказать критику принципа, который постулирует о том, что всегда нужно пытаться спроектировать интерфейсы еще до написания реализации. По моему убеждению, необдуманное следования ему, создает гораздо больше проблем, чем пользы.
Проектирование чего угодно - это процесс итерационный. И к интерфейсам это относится в той же мере. "С нуля и правильно", обычно, не бывает - ну разве что у Линуса (и то не факт, сколько раз там Линукс переписывался??)
Если интерфейс получится убогим - надо переделывать его, а не пытаться натянуть ежа на глобус, насилуя имплементации, просто потому что "такая уже архитектура,сорри". Если не получается сделать лучше - лучше тогда вообще избавиться от такого интерфейса и задуматься над альтернативным подходом (а он, как правило, есть всегда). Как то так.
Короче как деды завещали: написать, понять как правильно и переписать. При необходимости - повторить.
Почему принцип программирования на уровне интерфейсов в большинстве случаев ошибочен и приводит к плохой архитектуре