Вредный Кейворд «Interface»

http://blog.cleancoder.com/uncle-bob/2015/01/08/InterfaceConsideredHarmful.html
  • Перевод

Перевод ироничного поста из блога Боба Мартина в котором он рассуждает о том, насколько неудачным является использование слова interface в современных языках программирования, и какую путаницу и проблемы оно несёт разработчикам.


— Что ты думаешь об интерфейсах?


Имеешь в виду интерфейсы в Java или C#?


— Да. Классная фича этих языков?


Просто великолепная!


— Правда? А что такое интерфейс? Это то же самое что и класс?


Ну… Не совсем!


— В каком плане?


Ни один из его методов не должен иметь реализации.


— Значит это интерфейс?


public abstract class MyInterface {
  public abstract void f();
}

Нет, это абстрактный класс.


— Так, а в чём разница?


Абстрактный класс может иметь реализованные методы.


— Да, но этот класс их не имеет. Тогда почему его нельзя назвать интерфейсом?


Абстрактный класс может иметь нестатические поля, а интерфейс не может.


— У моего класса их тоже нет, почему он не интерфейс?


Потому что!


— Такой себе ответ… В чем реальное отличие от интерфейса? Что такого можно делать с интерфейсом, чего нельзя делать с этим классом?


Класс, который наследуется от другого, не может унаследоваться от твоего.


— Почему?


Потому что в Java нельзя наследоваться от нескольких классов.


— А почему?


Компилятор тебе не позволит.


— Очень странно. Тогда почему я могу реализовать(implements), а не отнаследоваться(extend) от него?


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


— Интересно, зачем такие ограничения?


Потому что наследование от множества классов опасно.


— Вот так новости! И чем же?


Смертельным Бриллиантом Смерти(Deadly Diamond of Death)!


— Звучит пугающе! Но что это значит?


Это когда класс наследует два других класса, оба и которых наследуют третий.


— Ты имеешь ввиду что-то типо этого?


class B {}
class D1 extends B {}   
class D2 extends B {}   
class M extends D1, D2 {}

Верно, это очень плохо!


— Почему?


Потому что класс B может содержать переменные!


— Вот так?


class B {private int i;}

Да! И как много переменных i будет в экземпляре класса M?


— Понятно. Т.е раз D1 и D2 содержат переменную i, a M наследуется от D1 и D2 то ты ожидаешь, что экземпляр класса M должен иметь две разных переменные i?


Да! Но т.к M наследуется от B у которого только одна переменная i, то ты ожидаешь что в M у тебя тоже будет всего одна i.


— Вот так неоднозначность.


Да!


— Получается что Java(и C#) не могут во множественное наследование классов, потому что кто-то может создать "Смертельный Бриллиант Смерти"?


Не просто может создать. Каждый априори создавал бы их т.к все объекты неявно наследуют Object.


— Ясно. А авторы компилятора не могли пометить Object как частный случай?


Ну… не пометили.


— Интересно почему. А как решается эта проблема в других компиляторах?


Компилятор C++ позволяет делать это


— Я думаю Eiffel тоже.


Черт, даже в Ruby смогли решить эту проблему!


— Ладно, получается что "Смертельный Бриллиант Смерти" это проблема, которую решили еще в прошлом веке, и она не фатальна и даже не ведёт к смерти.


Вынужден согласиться.


— Давай вернемся к первоначальному вопросу. Почему это не интерфейс?


public abstract class MyInterface {
      public abstract void f();
}

Потому что он использует кейворд class и язык не позволит унаследоваться от множества классов.


— Верно. Получается, что кейворд interface был изобретен для предотвращения множественного наследования классов?


Да, наверное.


— Так почему бы разработчикам Java (да и C#) не воспользоваться любым из решений проблемы множественного наследования?


Откуда я знаю?


— Кажется я знаю.


??


— Лень!


Лень?


— Да, им было лень разбираться с проблемой. Вот они и создали новую фичу, которая позволила им не решать её. Этой фичей стал interface.


Т.е ты хочешь сказать, что разработчики Java ввели понятие интерфейса чтобы избежать лишней работы?


— У меня нет другого объяснения!


Звучит грубовато. Да и в любом случае, круто что у нас есть интерфейсы. Они тебе чем нибудь мешают?


— Ответь себе на вопрос: Почему класс должен знать что он реализует именно интерфейс? Разве это не должно быть скрыто от него?


Имеешь в виду, что производный тип должен знать что именно он делает — наследует или реализует(extends or implements)?


— Абсолютно! И если ты поменяешь класс на интерфейс, то в код скольки наследников придется вносить изменения?


Во всех. В Java во всяком случае. В C# разобрались хотя бы с этой проблемой.


— Да уж. Ключевые слова implements и extends излишни и опасны. Было бы лучше если бы Java использовала решения C# или C++.


Ладно, ладно. Но когда тебе реально нужно было множественное наследование?


— Я бы хотел делать так:


public class Subject {
    private List<Observer> observers = new ArrayList<>();
    private void register(Observer o) {
        observers.add(o);
    }
    private void notify() {
        for (Observer o : observers)
            o.update();
    }
}

public class MyWidget {...}

public class MyObservableWidget extends MyWidget, Subject {
    ...
}

Это же паттерн "Наблюдатель"!


— Да. Это правильный "Наблюдатель".


Но он не скомпилируется, т.к ты пытаешься отнаследоваться от двух классов сразу.


— Да, в этом и трагедия.


Трагедия? Какого… Ты же можешь просто унаследовать MyWidget от Subject!


— Но я не хочу, чтобы MyWidget знал что за ним наблюдают. Мне бы хотелось разделять ответственности(separation of concerns). Быть наблюдаемым и быть виджетом, это две совершенно разные ответственности.


Тогда просто реализуй функции регистраци и уведомления в MyObservableWidget.


— Что? Дублировать код для каждого наблюдаемого класса? Нет спасибо!


Тогда пусть твой MyObservableWidget содержит ссылку на Subject и делегирует ему все что нужно.


— И дублировать код делегирования? Это какая-то фигня.


Но тебе все равно придется выбрать что-то из предложенного


— Знаю. Но я ненавижу это.


У тебя нет выхода. Либо нарушай разделение ответственностей, либо дублируй код.


— Да. Это язык сам толкает меня в это дерьмо.


Да, это очень грустно.


— И?


Могу лишь сказать, что кейворд interface — вреден и губителен!


Буду признателен за ошибки и замечания в ЛС.

Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну, и что?
Реклама
Комментарии 389
  • –38
    Пора уже перейти на Rust и выкинуть всю эту допотопную дичь. А со временем в Ruste всё допилят, если там чего-то еще не хватает.
    • +17
      — Пора уже перейти на C и выкинуть всю эту допотопную asm дичь.
      — Пора уже перейти на C++ и выкинуть всю эту допотопную C дичь.
      — Пора уже перейти на Java/.Net и выкинуть всю эту допотопную дичь.
      — Пора уже перейти на Go и выкинуть всю эту допотопную дичь.
      Пора уже перейти на Rust и выкинуть всю эту допотопную дичь.
      © Hedgar2018 — Full-stack Golang & JS developer

      Никогда такого не было — и вот опять! Когда на haskell переходить будем с этой допотопной дичи?
      • –4
        До Rust еще далеко… Да и не факт что к тому времени что то другое не изобретут… А вот в 2018 году на Go вполне пора переходить :)
        • +20
          На Go вообще никогда не стоит переходить
          • –4
            Наверняка такому категорическому заявлению есть и железная аргументация...?
            • +14
              Сразу после того, как вы аргументируете, что на Go надо переходить в 2018 =)
              • –8
                1. Быстрый
                2. Стабильный
                3. Развивается
                4. Структурирован (Сокращает количество ошибок)
                5. Мультиплатформенный
                6. Общего назначения

                Asm — переросли, С — переросли, С++ — переросли, Java/С# медленно и перерастаем… Go отличный кандидат на следующий уровень.

                P.S. Да, я на всем перечисленном писал. :)
                • +8
                  Ровно то же самое можно сказать про кучу других языков. Как-то слабовато для аргументов. Но раз вы хотите, то я напишу его недостатки в стиле вашего комментария

                  1. Непродуманный и устаревший синтаксис языка, провоцирующий быдлокод
                  2. Непоследовательная и плохая стандартная библиотека
                  3. Плохой менеджер пакетов
                  • 0
                    Возможно я чего-то не знаю…
                    Что провоцирует на «быдлокод»?
                    Что в ней плохого и непоследовательного?
                    Менеджер пакетов (наверное имеется ввиду менеджер зависимостей) штатный — просто очень удобный по сравнению с Java/C#. Да, есть и лучше, но ни кто не ограничивает их использование.
                    Хотелось бы пример языка общего назначения с идеальным менеджером (штатным) и лучшей стандартной библиотекой.
                    • 0
                      Менеджер пакетов (наверное имеется ввиду менеджер зависимостей) штатный — просто очень удобный по сравнению с Java/C#.

                      По пунктам можно?

                      • +10
                        Простите, но на ваши пространственные плюсы, которые подходят большинству языков — я выдал свои пространственные минусы.

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

                        Тем не менее я вам отвечу. Отсутствие Дженериков приводит к необходимости копипастить, использовать грязные хаки, делать кодогенерацию, которая ухудшает поддержку. Тот же парсинг JSON из-за этого выглядит как сущая магия. Пример непоследовательности библиотеки — это парсинг JSON и пакет flag, которые заточены на похожую задачу, но выполняют ее настолько кардинально разными способами, насколько можно было придумать. Ну вот что мешало сделать парсинг флагов через теги, как уже сделан парсинг JSON? Только понты.

                        Из-за странного запрета циклической зависимости многие люди просто пишут весь код приложения в одном пакете. Да, иногда это плохая практика, а иногда — необходимость. Отвратительная работа с ошибками, которая стала уже мемом.

                        Прекрасный пример ущербности языка Go — это теги. Как можно было придумать такое неюзабельное говно в 21-м веке я вообще не представляю.

                        Менеджер пакетов недавно обсуждали, пришли к выводу, что стандартный настолько плох, что им никто не пользуется.
                        • 0
                          Если нужны холивары веские аргументы и интересные факты, рекомендую пригласить здесь участников этой дискуссии (точнее, тех, кто считают, что в Go концептуально нельзя писать плохой код, в частности благодаря тому, что в нём не поддерживается тернарный оператор).
                          • +2
                            концептуально нельзя писать плохой код, в частности благодаря тому, что в нём не поддерживается тернарный оператор

                            Что? :-| Какая вообще связь-то?..

                    • +1
                      С — переросли

                      Теперь скажите это Линусу. Или любителям микроконтроллеров. Пока что это всё ещё язык по умолчанию, который есть везде.


                      С++ — переросли

                      Вот когда всё плюсовое наследие превратится в фортрановое наследие, а в индустрии будет доминировать условный Rust — тогда поговорим. Потенциал есть, интересные языки есть, но пока и близко к цели не приблизились. Условный D революцию 10 лет обещал. От некогда доминирующих всё и вся плюсов отвалилась куча ниш, да, но и у плюсов осталось достаточно.


                      Java/С# медленно и перерастаем

                      Не наблюдаю. Весь кровавый интерпрайз там, и особых претензий к платформам JVM и .NET нет. Более того, если посмотреть на статистику опроса Stack Overflow, то .NET Core куда-то там вырывается. Не понимаю, почему, но факт.

                      • +2
                        Весь кровавый интерпрайз там
                        А кроме энтерпрайза сейчас 95% геймдева — это один из трех языков: C++, C# или JS.
                        • 0
                          Я и не говорил что языки отмерли. Я просто обозначал вектор развития.
                          Сам не так давно вынужден был на asm под ARM кодить. Но это не значит что это тренд…
                          • 0

                            А что такое "тренд"? В любом Мухосранске можно найти работу по любому из "нетрендовых" языков, а вот найдёте ли по "трендовым" — ещё большой вопрос. И эти языки пользуются спросом на протяжении десятилетий, пока "тренды" с условными руби приходят и уходят. Что-то из "трендов" абсорбируется в "нетренд", и "нетренд" опять побеждает.

                            • 0
                              Не все и не всегда работают чисто ради денег.
                              У некоторых есть основные проекты «нетренд» и дополнительные, в которых инструментарий не лимитируется.
                              А вообще по ответам, к сожалению, тенденция всех форумов прослеживается и тут. Только в 1 ответе было показано с примерами что человеку нравится С++. Остальные просто ругают то что не пробовали или вообще, просто, ругают, при этом не говорят где лучше.
                              Грустно все это, ну да ладно. У каждого свой путь самовыражения.
                              • 0
                                Не обязательно сильно много ковыряться в каком-то языке и досконально его пробовать, чтобы понять, интересен ли он вам или нет. И если выбирать, на что потратить своё время, скажем, на Go или на какой-нибудь Idris, особенно если при этом не нужно задумываться о деньгах, то выбор-то довольно простой. Для меня, по крайней мере.
                      • +2
                        1. На С++ можно писать не медленнее.
                        2. С++ очень следит за сохранением совместимости с имеющимся кодом, иногда даже излишне.
                        3. С++ развивается. Вон, в 20-й версии наверняка будут корутины, концепты и компилтайм-рефлексия, и почти наверняка адекватные модули, например. Если повезёт, успеют прикрутить к корутинам всё необходимое для выражения через них произвольных линейных монад.
                        4. Особенно их сокращает паттерн проверки на ошибки, наверное.
                        5. Как и куча других языков.
                        6. Как и куча других языков.
                        • 0

                          C++ — это круто, и в современном языке можно выделить разумное красивое подмножество. Проблема в том, что подмножество у всех своё, стадия эволюции у всех своя, компиляторы у всех свои, и вообще чёрт ногу сломит.


                          Вот сделают модули. Может быть. Через пару лет. Ещё через несколько лет они будут поддерживаться компиляторами. Казалось бы, вот оно счастье. Но как быстро я смогу не знать про инклюды?


                          И вот так с каждой фичей.

                          • 0
                            К счастью, сегодня не начало двухтысячных с VC6 и gcc 2.95. Modules TS уже сделаны в clang, например, можно начинать играться. Да и вообще опыт показывает, что новые фичи в большинстве своём реализуются в clang и gcc примерно в районе выхода стандарта, если не раньше. А учитывая, что я таки надеюсь, что модули не примут в виде их текущей TS, а смержат с Another take on modules, получается вполне неплохо.

                            И никаких пары лет.
                            • 0
                              А если примут, то что? Стреляться?
                              • 0
                                То грустно. Писать пропозалы для следующего стандарта и участвовать во встречах комитета, чтобы убеждать не принимать те вещи, которые поломают совместимость с будущей адекватной модульной системой.
                    • +3

                      Здесь же уже постили недавно:



                      Go позиционируется как простой язык, который может за вечер изучить любой школьник. Это как взять Java и довести многословность до абсурда под лозунгом "так проще". Я бы не строил карьеру вокруг этой идеи. Когда вокруг "простых языков" начинают строить сложную индустрию, получаются франкенштейны.

                      • 0
                        Нескромный вопрос, а вы это пробовали делать, или не видел, но осуждаю?
                        • +4
                          Я пробовал делать и немало. Отвратительный язык. Хотя с `go something()` придумали неплохо, но это не перевешивает его недостатки.
                          • +10

                            В конечном счёте это ИМХО, конечно.


                            Чтобы понять, что мне не подходит Go, мне не надо много программировать на Go. Мне достаточно увидеть, какие решения предлагаются на замену исключениям, дженерикам и прочему, посмотреть на разнообразные исходники, чтобы понять, что я так писать не хочу. Всё.


                            Точно так же я не хочу писать обфусцированный код на CoffeeScript, вермишель из колбэков на JavaScript или ещё что-то подобное.


                            У меня есть выбор. У меня есть определённые критерии к эстетическим свойствам кода, который я пишу, и краткость, выразительность и наглядность далеко не на последнем месте. Мне не надо писать много кода на языке, чтобы понять, что он меня не устраивает.


                            Прежде, чем писать на JavaScript вермишель из колбэков, я лучше спокойно посижу в сторонке и подожду, когда в язык добавят async/await и классы. Что, уже добавили? Ну, теперь язык для меня подходит.


                            И даже если я не люблю какой-то синтаксис (LISP), я могу понять эстетику и мощь, которые скрываются за бесконечными скобочками. У лиспов есть внутренняя красота, в них вложен инженерный гений, даже если язык мне не нравится и я не собираюсь на нём писать.


                            Что красивого в Go — я не понимаю. Может быть, вы объясните?

                            • –8
                              Проблема в том что всех приучили что ООП это классы и только классы. Это не правда.
                              Дженерик — а оно правда необходимо?
                              1. Это тормозит во всех языках. Не тормозит только при условии кодогенерации. Но именно такая возможность заложена в Go. Просто ее надо вызывать более осмысленно.
                              2. Парадигма Go — простота. Использование дженериков ведет к усложнению кода. Потенциально есть возможность использовать нечто очень похожее используя интерфейсы и контейнеры, но, опять же, это усложнение.

                              Async/await — Классический костыль для синхронизации.
                              В Go используются горутины и общаются они через каналы. Этот костыль просто не нужен при правильном построении программы.

                              Вермишели из коллбеков вроде тоже нет…

                              Вообще, на Go как и на многих языках, нельзя писать «как я раньше писал на @@@» (@@@ замените на ваш любимый язык). Надо понять принципы, идеологию, и все станет сильно проще.

                              А что для вас язык с идеальным синтаксисом?
                              • +3
                                Дженерик — а оно правда необходимо?
                                1. Это тормозит во всех языках. Не тормозит только при условии кодогенерации.
                                В .NET — кодогенерация. Шаблоны в c++ — кодогенерация. Дженерики сделаны для скорости, ну и для проверки типов во время разработки.
                                • +10
                                  Парадигма Go — простота. Использование дженериков ведет к усложнению кода


                                  То есть
                                  ```go
                                  var identity types.Identity
                                  var err = json.NewDecoder(json).Decode(&identity)
                                  ```
                                  Это быстро и просто?

                                  ```cs
                                  var identity = JsonConvert.DeserializeObject(json);
                                  ```

                                  А это — медленно и сложно?

                                  Использование дженериков, очевидно, ведет к упрощению кода, а не усложнению. Хватит повторять библию Гошников. Есть куча примеров, где отсутствие дженериков ведет или к отсутствию статики или к копипасту.
                                  • –4
                                    Вот ведь, век живи, век учись. :)
                                    То что я писал выше — основывается на моем скромном опыте Go. Про «библию» был не в курсе.
                                    Конкретных примеров как в одну сторону так и в другую можно найти массу.
                                    Например я с содроганием вспоминаю как писал в одном из проектов template of template с лямбдами внутри на C++. И там это было обосновано. А потом, через год примерно, отлаживал это.

                                    Так что каждый ищет проблемы соразмерные своей пятой точке в независимости от используемого языка :)
                                    • +5
                                      template != generic

                                      Дженерики уже до предела упрощены.
                                      • –6
                                        Я в курсе. Это просто пример приключений для пятой точки.
                                        • +3
                                          Вы знаете, я видел по телевизу, как человек убивал младенцев. Я в курсе, что Гоу тут (наверное) ни при чем, но это просто пример приключений для пятой точки, потому, на всякий случай, Гоу лучше не использовать
                                        • +1
                                          Полиморфные методы в ML-подобных языках тоже предельно просты и вполне мономорфизируются при компиляции.
                                          • 0
                                            Так ведь они больше похожи на generic чем на template же. Или я ошибаюсь?
                                            • 0
                                              У меня недостаточно знаний о дженериках, чтобы однозначно сказать. Они к ним почти наверняка ближе, чем к темплейтам, но насколько близко — я не знаю.

                                              Вы должны явно описать принадлежность тайпклассу в точке определения вашего типа.
                                              У тайпклассов могут быть реализации по умолчанию, и могут быть реализации методов через другие методы, и можно указать набор минимально необходимых множеств методов (см. какой-нибудь Foldable).
                                              Компилятор может вывести реализацию тайпкласса за вас.
                                              Тайпклассы могут включать в себя несколько типов (раз, два). Могут включать в себя и значения (готового примера не приведу, но можно наваять при желании).

                                              Но вообще я это всё скорее в подтверждение ваших слов.
                                              • 0
                                                Хм, посмотрел я по вашим ссылкам… Похоже, все-таки успели испортить хорошие языки шаблонами. Кошмар начинается с появлением Type families. Даже не представляю как может работать вывод типов в таких условиях.
                                                • 0
                                                  Ну, на практике семейства типов не сильно-то его и ломают, особенно инъективные.

                                                  Что действительно ломает — rank-N polymorphism, и иногда бывает вообще магия.

                                                  А в Idris вывода типов функций, считайте, толком и нет (за парой исключений), но это в основном потому, что в присутствии зависимых типов вывод типов неразрешим в смысле Тьюринга.
                                        • +3
                                          Конкретных примеров как в одну сторону так и в другую можно найти массу
                                          Но это ведь вы категорично заявили, без дополнительных «если»:
                                          Использование дженериков ведет к усложнению кода
                                      • +2

                                        Неиспользование дженериков, кроме как из базовых типов, приводит к еще большему усложнению кода.
                                        Про генераторы не надо… Доводилось это "добро" писать и поддерживать.

                                        • +4
                                          На этих самых ваших каналах простейшая задача вида «отменить асинхронную операцию» превращается в такую кашу…
                                          • +2
                                            На этих самых ваших каналах простейшая задача вида «отменить асинхронную операцию» превращается в такую кашу…

                                            потому что в Go нет примитива синхронизации со встроенным NACK…
                                            Это в огород гошников, что у них самый лучший язык в мире, а там только базовые ченелы завезли, остальное надо руками делать

                                          • 0
                                            Async/await — Классический костыль для синхронизации.
                                            В Go используются горутины и общаются они через каналы.

                                            И чем goroutine от async/await принципиально отличается? Те же яйца, только в профиль: потроха разные, диапазон функций разный, синтаксис разный, но суть одна — написание асинхронного кода как синхронного. Что у авторов Go была возможность внедрить асинхронность в язык с самого начала — это хорошо, но async/await в остальных языках — тоже нормальное решение, причём ещё и более гибкое.


                                            А что для вас язык с идеальным синтаксисом?

                                            Из универсального мейнстрима — C# вне конкуренции по сахару. Из того, что я видел, но на чём сам не писал — Haskell. Из того, что теоретически идеально, но я использовать не буду — LISP.

                                            • +1
                                              И чем goroutine от async/await принципиально отличается?

                                              Принципиальное отличие: async/await — синтаксический сахар над колбэками (stackless реализация), а горутины — над волокнами (stackfull userspace multithreading).


                                              То есть горутины не только выглядят как синхронные, они и внутри тоже синхронные. Просто планировщик из пространства ядра перенесён в юзерспейс, что значительно сокращает накладные расходы на переключение контекста.


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

                                              • 0
                                                > Принципиальное отличие: async/await — синтаксический сахар над колбэками

                                                Не обязательно
                                              • 0
                                                >> Из того, что теоретически идеально, но я использовать не буду — LISP.

                                                Почему?
                                                • +1
                                                  Так типов ж нет!
                                                  • 0
                                                    Вообще-то есть. Там строгая динамическая типизация.
                                                    • 0
                                                      Прошу прощенья, имел ввиду выразительную строгую статическую.
                                                      • 0
                                                        Неприятность в том, что у нас больше нет привычного молотка. Но плюс в том, что больше не нужно забивать гвозди
                                                        • +1
                                                          Тайпчекер — друг, товарищ и брат.

                                                          И средство доказательства всяких полезных свойств программ.
                                                          • 0
                                                            Там есть много других друзей, товарищей и братьев. Они тоже могут быть настолько полезными, что после не понимаешь, как можно без них обходиться.

                                                            Кроме того, там есть возможность расширения, которая позволяет добавить все что нужно самому (привет тем, кто ждет годами новой версии стандарта или новых фич).
                                                            • 0
                                                              Там есть много других друзей, товарищей и братьев. Они тоже могут быть настолько полезными, что после не понимаешь, как можно без них обходиться.

                                                              Например?
                                                              И что из них заменяет выразительную систему типов?

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

                                                              Папиры на, в частности, (gradually) typed racket я читал. Это всё забавно, конечно, но как-то лучше пользоваться языками, где это из коробки.
                                                              • 0
                                                                И что из них заменяет выразительную систему типов?


                                                                На мой взгляд (я не претендую на правоту) макросы лиспа — это совершенно иной подход к выразительности, по мощности не уступающий системе типов.

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


                                                                Не сказал бы. За возможность запилить без титанических усилий async/await или горутины или еще какие штуки любители этих штук отдали бы многое. Полагаю, как минимум из-за любви к тому, что выдается в коробке они сидят на игле своих языков. Как насчет получить в коробке кухню и попробовать самому готовить? В коробке там тоже много чего есть, если что…
                                                                • 0
                                                                  На мой взгляд (я не претендую на правоту) макросы лиспа — это совершенно иной подход к выразительности, по мощности не уступающий системе типов.

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

                                                                  За возможность запилить без титанических усилий async/await или горутины или еще какие штуки любители этих штук отдали бы многое.

                                                                  Так хоть корутины (да, без г), хоть строковую интерполяцию, хоть STM.

                                                                  Хаскелевский TH — мощнейшая штука, равно как и идрисовский элаборатор/рефлектор (но с ним я пока ещё не игрался).
                                                                  • 0
                                                                    Я не уверен, что макросы это могут заменить хоть в каком-то виде.


                                                                    А они это не заменяют. Они работают над выразительностью совершенно другим путем.

                                                                    Чем выразительнее система типов, тем больше всяких полезных инвариантов и свойств вы можете гарантировать статически.


                                                                    Это верно. Но интересно то, что мы не обязаны сосредотачиваться исключительно на задаче получения статических гарантий инвариантов. Есть и другие подходы, чтобы сделать что-то интересное и полезное

                                                                    Хаскелевский TH — мощнейшая штука, равно как и идрисовский элаборатор/рефлектор (но с ним я пока ещё не игрался).


                                                                    Я обязательно изучу и это тоже. Мне нравится рассматривать разные подходы и разные задачи
                                                                    • 0
                                                                      Но интересно то, что мы не обязаны сосредотачиваться исключительно на задаче получения статических гарантий инвариантов. Есть и другие подходы, чтобы сделать что-то интересное и полезное

                                                                      Вопрос создания интересного и полезного — это совершенно ортогональная верифицируемости штука.

                                                                      А вообще, это уже звучит как hujak-hujak-production-driven development.
                                                                      • 0
                                                                        Вопрос создания интересного и полезного — это совершенно ортогональная верифицируемости штука.


                                                                        Верификация возможна и без статической системы типов (да, это так, можно верифицировать например control-flow).

                                                                        А вообще, это уже звучит как hujak-hujak-production-driven development.


                                                                        Верификация, как и другие подходы, нужна для того чтобы делать что-то интересное и полезное. Не вижу причин называть плохими словами все неизвестное
                                                    • –2
                                                      Ну где нет, а где — даже зависимые подвезли
                                                      blog.racket-lang.org/2017/11/adding-refinement-types.html

                                                      > И что из них заменяет выразительную систему типов?

                                                      Система типов — костыль для языков, в которых нет мощной макросистемы.

                                                      > Я не уверен, что макросы это могут заменить хоть в каком-то виде.

                                                      Типы по ссылке выше написаны именно на макросах. Более того — там есть макробиблиотека, которая позволяет легко писать свои системы типов и на ходу их расширять (добавлять свои семантические правила). Например, захотели добавить type families — а это просто обычная языковая библиотека, которую может написать кто угодно и которая подключается как любая другая библиотека в конкретный модуль (в разных модулях, с-но, можно иметь разные системы типов). Разве не замечательно?

                                                      > Хаскелевский TH — мощнейшая штука

                                                      Хаскелевский TH лучше убить не вспоминать. Лучше вообще не иметь макросистемы, чем иметь плохую (то же, кстати, для типов верно — лучше вообще системы типов не иметь, чем иметь плохую).
                                                      • +1
                                                        Система типов — костыль для языков, в которых нет мощной макросистемы.

                                                        Наверное вы имели ввиду "встроенные системы типов"?


                                                        Типы по ссылке выше написаны именно на макросах. Более того — там есть макробиблиотека, которая позволяет легко писать свои системы

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

                                                        • 0
                                                          См. выше: здесь обсуждается то, что «теоретически идеально».

                                                          Как по мне, LISP — это такой ассемблер, у которого в cons-парах может лежать что угодно, как в регистре EAX в x86-ассемблере — хоть число, хоть объект любого типа (указатель). Другое сходство с асмом — что любой кусок памяти можно интерпретировать как исполняемый код и запустить. В LISP-е с этим так же хорошо.

                                                          Когда я думаю, какую бы идеальную аппаратную архитектуру я бы хотел иметь, то мне видится именно LISP-машина в минимальном варианте, без CLOS, макросов и типов. А высокоуровневые языки вводили бы свои фичи поверх этого и компилировались бы в LISP-код.

                                                          Писать на ассемблере очень круто, особенно с макросами (помню, в конце 90-х в ассемблерах чуть ли не if-while-switch делали макросами, и писали, утверждая, что это по простоте кодирования ничуть не уступает другим языкам). Но со временем хочется чего-то другого.
                                                          • 0
                                                            > Наверное вы имели ввиду «встроенные системы типов»?

                                                            Я полагал, это понятно из контекста. Как видно — верно полагал.

                                                            > А вот если это дать в руки среднестатистическому кодеру

                                                            Этому вашему «среднестатистическому кодеру» сложные системы типов в руки в принципе давать нельзя.

                                                            > настанет хаос и вам и мне придется бороться с уродцами-порождениями этого хаоса.

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

                                                              На хаскелле порог вхождения и задачи отличаются от таковых для жабы, ИМХО

                                                              • 0
                                                                Ну вот в хаскеле есть зоопарк расширений и ничего, никто не борется.

                                                                Так это не зоопарк в привычном смысле, они все либо ортогональны, либо являются подмножествами друг друга (ну как PolyKinds и TypeInType, например) и в подавляющем большинстве случаев предназначены для решения разных задач. Кое-как перекрываются только фундепы и семейства типов, например.
                                                                • –2
                                                                  > Так это не зоопарк в привычном смысле, они все либо ортогональны, либо являются подмножествами друг друга (ну как PolyKinds и TypeInType, например) и в подавляющем большинстве случаев предназначены для решения разных задач.

                                                                  Ну так и?

                                                                  > Да, это хорошо, но зачем делать из Racket Idris, если можно сразу взять Idris?

                                                                  Racket выразительнее и удобнее. Алсо, там зависимые типы пытаются делать «практико-ориентированные», а не как в идрисе.

                                                                  > Я не понимаю, как макросистема сама по себе заменяет систему типов.

                                                                  Она дает возможность цеплять к термам некие данные, и проверять эти данные в термах, подтермами которых являются данные термы. Вообще говоря, при наличии макросистемы для получения статических гарантий вам типы и не нужны. Вы можете реализовать их «напрямую» и проверять любые требуемые инварианты. При этом на этапе проверки у вас есть вся мощь хост-языка -вам не приходится изворачиваться ужом и кодировать эти инварианты в кривом, не предназначенном для подобных задач type-level языке.

                                                                  > Вот как раз систему типов вообще и тайпчекер в частности (по сравнению с интерполяцией строк какой-нибудь) лучше иметь в ядре языка

                                                                  Это будет криво и нерасширяемо. Как мы и имеем на практике.

                                                                  > А я вас ещё с прошлой дискуссии за хаскель помню, я тогда ещё решил, что после вашего утверждения, что хаскель не поддерживает

                                                                  Но ведь и вправду не поддерживает. Точно в той же мере, в которой, например, js не поддерживает упомянутые зависимые типы. Вы, например, _можете_ использовать js с зависимыми типами (точно также как и в случае определений в хаскеле), язык это формально позволяет. Но делает все, чтобы вы этого не делали.
                                                                  • 0
                                                                    Ну так и?

                                                                    Ну так и не зоопарк несовместимого. Часть из них вообще предлагают смержить в следующий стандарт (Haskel2020, что ли).

                                                                    Алсо, там зависимые типы пытаются делать «практико-ориентированные», а не как в идрисе.

                                                                    Можете пояснить примером?

                                                                    При этом на этапе проверки у вас есть вся мощь хост-языка -вам не приходится изворачиваться ужом и кодировать эти инварианты в кривом, не предназначенном для подобных задач type-level языке.

                                                                    В языке с изначальной поддержкой завтипов (тот же идрис, агда) у вас нет какого-то отдельного type-level-языка. Это не хаскель как он есть сегодня, где приходится изворачиваться с синглтонами и всякой такой ерундой.

                                                                    Это будет криво и нерасширяемо. Как мы и имеем на практике.

                                                                    В чём кривость-то? И в чём нерасширяемость при наличии, например, идрисовского рефлекшона?

                                                                    Но ведь и вправду не поддерживает.

                                                                    Написал
                                                                    foo, bar, baz :: a -> a
                                                                    foo = undefined
                                                                    bar = undefined
                                                                    baz = undefined
                                                                    
                                                                    meh :: a ->
                                                                           b ->
                                                                           b
                                                                    meh = const $
                                                                            foo .
                                                                            bar .
                                                                            baz
                                                                    

                                                                    Что там не поддерживается?

                                                                    язык это формально позволяет

                                                                    Как?
                                                                    • –2
                                                                      > Ну так и не зоопарк несовместимого.

                                                                      И? Я не понимаю, к чему вы ведете. Можете полностью как-то цепочку логическую изложить, и указать, чем она должна закончиться? Я из намеков понять не могу, слишком туманно.

                                                                      > В языке с изначальной поддержкой завтипов (тот же идрис, агда) у вас нет какого-то отдельного type-level-языка.

                                                                      Вообще-то есть. Еще раз — вы не можете в идрисе написать что-то вроде: «вот этот терм ассоциирован с данными Х, этот с данными Y, а конструкция R добавляет данные Z к терму T в том случае, если X и Y удовлетворяют P»

                                                                      > Можете пояснить примером?

                                                                      Там же по ссылке есть пример. Можно просто взять, и написать vector-ref, который проверяет, что индекс не выходит за границы массива. Просто обычным предикатом.

                                                                      > И в чём нерасширяемость при наличии, например, идрисовского рефлекшона?

                                                                      Вы не можете в идрисе добавить свои семантические правила.

                                                                      > Как?

                                                                      В точности так же, как в вашем примере выше хаскель позволяет писать на отдельных строках. Добавляете ваши типы в комментариях и проверяете руками. Неудобно и вырвиглазно? Именно!
                                                                      • 0
                                                                        Можете полностью как-то цепочку логическую изложить, и указать, чем она должна закончиться?

                                                                        Возможность (и в известном смысле необходимость) реализовывать тайпчекер руками ведёт к набору различных и несовместимых (не в последнюю очередь и семантически) систем типов, что обычно называется зоопарком. ghc'шные экстеншоны ghc'шной же системы типов к такому зоопарку не ведут. Вели бы, если бы ghc был не де-факто единственным компилятором, или если бы хотя бы были другие компиляторы с другими расширениями, но это не так, и в обозримом будущем предпосылок для изменения этого не видно. Тем более, добрую часть неэкспериментальных экстеншонов сольют в Haskell2020, а там и необозримое будущее наступит.

                                                                        Еще раз — вы не можете в идрисе написать что-то вроде: «вот этот терм ассоциирован с данными Х, этот с данными Y, а конструкция R добавляет данные Z к терму T в том случае, если X и Y удовлетворяют P»

                                                                        А ещё в идрисе, похоже, тоже нет импредикативного полиморфизма, и это печалит меня куда больше.

                                                                        Если серьёзно, я не понимаю это высказывание, и тут нужно больше формализма. Что значит ассоциирован, что вы называете данными, и зачем это нужно делать? Хотя, может, ответ на последний вопрос будет следовать из первых двух.

                                                                        Можно просто взять, и написать vector-ref, который проверяет, что индекс не выходит за границы массива. Просто обычным предикатом.

                                                                        Так кто в идрисе мешает?

                                                                        Можете в Vect, где границы даны на уровне типов:
                                                                        *Data/Vect> :doc Data.Vect.index
                                                                        Data.Vect.index : Fin len -> Vect len elem -> elem
                                                                            Extract a particular element from a vector
                                                                            
                                                                            The function is Total
                                                                        


                                                                        Можете в классический список:
                                                                        Prelude.List.index : (n : Nat) -> (xs : List a) -> {auto ok : InBounds n xs} -> a
                                                                            Find a particular element of a list.
                                                                            Arguments:
                                                                                (auto implicit) ok : InBounds n xs  -- a proof that the index is within bounds
                                                                                
                                                                            The function is Total
                                                                        

                                                                        Но да, нужно будет доказательство:
                                                                        Data type Prelude.List.InBounds : (k : Nat) -> (xs : List a) -> Type
                                                                            Satisfiable if k is a valid index into xs
                                                                            Arguments:
                                                                                k : Nat  -- the potential index
                                                                                
                                                                                xs : List a  -- the list into which k may be an index
                                                                                
                                                                        Constructors:
                                                                            InFirst : InBounds 0 (x :: xs)
                                                                                Z is a valid index into any cons cell
                                                                                
                                                                            InLater : InBounds k xs -> InBounds (S k) (x :: xs)
                                                                                Valid indices can be extende
                                                                        

                                                                        которое, впрочем, легко построить:
                                                                        Prelude.List.inBounds : (k : Nat) -> (xs : List a) -> Dec (InBounds k xs)
                                                                            Decide whether k is a valid index into xs
                                                                            
                                                                            The function is Total
                                                                        


                                                                        Вы не можете в идрисе добавить свои семантические правила.

                                                                        Могу.

                                                                        В точности так же, как в вашем примере выше хаскель позволяет писать на отдельных строках. Добавляете ваши типы в комментариях и проверяете руками. Неудобно и вырвиглазно? Именно!

                                                                        Непонятно, в чём неудобство и вырвиглазность в коде из предыдущего комментария, ну да ладно.
                                                                        • –1
                                                                          > Возможность (и в известном смысле необходимость) реализовывать тайпчекер руками ведёт к набору различных и несовместимых (не в последнюю очередь и семантически) систем типов

                                                                          Но вас же никто не заставляет использовать несовместимые. Какие хотите — такие и берете.

                                                                          > Что значит ассоциирован

                                                                          То и значит. В прямом смысле.

                                                                          > что вы называете данными

                                                                          Данными я называю данные. Любые. У тайпчекера эти данные — это тип терма, частный случай.

                                                                          > Так кто в идрисе мешает?

                                                                          Ну, система типов мешает. Не допускает она таких вольностей.

                                                                          > Но да, нужно будет доказательство:

                                                                          Так в этом и дело. Вместо того, чтобы просто описать требуемый инвариант в виде предиката, вам приходится его кодировать, причем достаточно нетривиальным способом.

                                                                          > Могу.

                                                                          По вашей ссылке ничего нет такого. Может, я не заметил, точнее укажите.
                                                                          • 0
                                                                            Но вас же никто не заставляет использовать несовместимые. Какие хотите — такие и берете.

                                                                            А сторонние библиотеки не будут фрагментированы по используемым системам типов? Или это не страшно?

                                                            • 0
                                                              Ну где нет, а где — даже зависимые подвезли

                                                              Ровно про это я говорил в одном из комментариев выше. Да, это хорошо, но зачем делать из Racket Idris, если можно сразу взять Idris?

                                                              Ну и, на мой субъективный и испорченный ML-семейством вкус, выглядит это всё как-то стрёмно.

                                                              Система типов — костыль для языков, в которых нет мощной макросистемы.

                                                              Я не понимаю, как макросистема сама по себе заменяет систему типов. Предположим даже, что вы через макросистему можете прикрутить к своему языку систему типов, и оно даже будет тайпчекаться в относительно компилтайме. Ну, вы молодец, вы на макросах сделали свой собственный диалект языка и свой собственный тайпчекер. Зачем?

                                                              Вот как раз систему типов вообще и тайпчекер в частности (по сравнению с интерполяцией строк какой-нибудь) лучше иметь в ядре языка, написанную один раз, выверенную сообществом, и так далее.

                                                              Хаскелевский TH лучше убить не вспоминать. Лучше вообще не иметь макросистемы, чем иметь плохую (то же, кстати, для типов верно — лучше вообще системы типов не иметь, чем иметь плохую).

                                                              А я вас ещё с прошлой дискуссии за хаскель помню, я тогда ещё решил, что после вашего утверждения, что хаскель не поддерживает многострочные определения, спор продолжать бессмысленно :)
                                                      • 0
                                                        В Go используются горутины и общаются они через каналы. Этот костыль просто не нужен при правильном построении программы.

                                                        К слову о костылях и каналах. Типобезопасную и проверяемую компилятором STM завезли уже?

                                                        А что для вас язык с идеальным синтаксисом?

                                                        Минимум навязанного синтаксиса, сахар, возможность легко делать свои операторы под задачу и вообще eDSL. Ну, хаскель подойдёт, например.
                                                        • +1
                                                          Async/await — Классический костыль для синхронизации.
                                                          В Go используются горутины и общаются они через каналы. Этот костыль просто не нужен при правильном построении программы.

                                                          Так async/await по сути и есть более удобный синтаксис корутин/горутин/сопроцедур, что есть одно и то же. Тем более, что вообще C# поддерживал в некотором роде сопроцедуры ещё с версии 2.0, когда появилось слово yeild и итераторы. Это уже позволяло (пусть и не так удобно) писать корутины и получать всю прелесть, что сейчас есть с async/await. Так что в этом плане горутины не новость.
                                                          • 0
                                                            Async/await это по сути своей синтаксический сахар, который при компиляции просто заменяется на последовательность вызовов. На сколько помню, все вызовы будут вполне линейны с точки зрения процессора, но программе будет казаться что исполняется параллельно.
                                                            Аналогично и слово yeild. Только там генерируется класс, который, по мере надобности, возвращает результат и имеет внутреннюю стейт машину.
                                                            В отличии от этих методов горутины это реальные параллельные потоки. Да, количество реально параллельных потоков соотносится с количеством ядер процессора. Но все равно они параллельны.
                                                            • +1
                                                              async/await это про асинхронность, которая ничего общего с параллельностью не имеет.

                                                              Тем не менее, что за «линейные относительно процессора вызовы» вы имеете ввиду я так и не понял. async/await разворачиваются в типичную цепочку a.then(b).then©, которая планировщиком где-то выполняется.
                                                              • 0
                                                                На сколько помню, все вызовы будут вполне линейны с точки зрения процессора

                                                                Вы неправильно помните. await выделяет весь код после него в continuation, который кладется на планировщик и может быть выполнен где угодно.

                                                                • 0

                                                                  Горутины ведь тоже синтаксический сахар, превращённый в killer-фичу языка. Async/await является удобной и приятной штукой, но он не гарантирует исполнение на другом потоке, также как и yeild, но в последнем случае нам приходится больше писать самим и мы можем подготовить свой класс Awaiter'а, который всегда будет делать новый поток как только мы напишем yeild return Yeild.RunAsync(...). Но чего об этом спорить — это всего лишь сахар — кто-то любит крепкий чай, а кто-то послаще :)

                                                                  • +1

                                                                    await не должен ничего гарантировать, гарантировать должны нижележащие инструменты, в данном случае объект, на котором авейтимся. Если это какой-нибудь AlwaysExecutableInAnotherThreadTask.GetAwaiter то всё ок. Просто не надо мешать зоны ответственности.

                                                                    • +1
                                                                      Полностью согласен.
                                                • +3
                                                  Хаскелю почти 30 лет, поздно уже переходить ради хайпа, давайте лучше на Idris.

                                                  А если серьёзно, лично я был бы очень рад, если бы больше софта и библиотек писалось на выразительных и безопасных языках.
                                                • 0
                                                  deleted
                                                  • 0
                                                    тенденция скорее такая, что руст сам обрастет той же допотопной дичью :)
                                                  • +29

                                                    Лениться делегировать — это экономия на спичках. Больше огребётесь от огромного количества наследников.


                                                    Наследование это не способ писать меньше кода. Это способ выразить отношение is-a (ну и ещё способ сделать discriminated union, в языках где его нет). Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.
                                                    Примеры:


                                                    1. У вас есть базовый класс, который часто наследуется в вашей системе. Затем, только некоторому числу наследников понадобилось новое поведение — вы меняете базовый класс, но вместе с этим вы также меняете и контракт тех классов, которым это поведение не нужно. Как итог, вам нужно протестировать те компоненты, которые даже не менялись — нарушение OCP. А ещё частенько ведёт и к нарушению LSP.


                                                    2. В укор первому примеру, вы можете сказать — "да я щас наделаю много мелких классов (например, VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable) с точечным поведением и буду множественно наследовать от них". Ок, но чего вы этим сэкономите? Количество LOC будет примерно сравнимым при композиции. Только в этот раз вы усложнили систему, наделав в ней кучу ненужных сущностей.


                                                    3. Имея некоторый базовый класс, вы делаете вид, что знаете как он будет использоваться. Нарушение инкапсуляции здесь ещё грубее — каждый разработчик должен знать детали реализации в базовом классе (иначе опять можно нарушить LSP).


                                                    4. Ещё более опасна длинная цепочка наследования. Например, есть у вас некоторая иерархия с некоторым поведением в самом верхнем родителе. Затем, одному или нескольким наследникам нужно отличное поведение. Как итог порождается ещё одна иерархия классов, что в конечном итоге ведёт к сложности системе.

                                                    Да и вообще сто раз это исписанно.


                                                    ps. В java (и скоро в c#) ведь есть partial interface implementation — пользуйтесь.

                                                    • –1
                                                      Затем, только некоторому числу наследников понадобилось новое поведение — вы меняете базовый класс, но вместе с этим вы также меняете и контракт тех классов

                                                      Нет, в таком случае обычно вводится ещё один промежуточный класс в иерархию

                                                      что в конечном итоге ведёт к сложности системе

                                                      Если существовала бы какая-то «серебряная пуля», которая помогала бы реализовать сложное поведение с помощью простой и очевидной модели, мы бы все ей пользовались :)
                                                      • +5
                                                        Серебряная пуля конкретно для наследования есть и она очень простая: «не наследуйтесь от реализаций».
                                                        • +5
                                                          Да, только «не наследуйтесь от реализаций» можно, по сути сократить до «не наследуйтесь» :)
                                                          • 0
                                                            Только до «не наследуйтесь от неабстрактных классов».
                                                            • 0
                                                              Абстрактные классы либо содержат часть реализации либо заменяются на интерфейсы.
                                                          • 0
                                                            К сожалению, это не «серебряная пуля», а только правило гигиены.
                                                          • +1
                                                            Нет, в таком случае обычно вводится ещё один промежуточный класс в иерархию
                                                            Что в свою очередь делает всю систему еще более запутанной. Видел я систему компонентов из 7+ уровней наследования, казалось бы все красиво и круто, но разбираться в этом – то еще удовольствие :(

                                                            Если существовала бы какая-то «серебряная пуля», которая помогала бы реализовать сложное поведение с помощью простой и очевидной модели, мы бы все ей пользовались :)
                                                            Из своего опыта могу сказать, что для меня «серебряная пуля» – это модульный подход (использовать interface и лишь один уровень иерархии).
                                                            Да, придется дублировать код в разных модулях, но в результате мы получаем Очевидную реализацию и поведение, легкость в тестировании и следование принципам LSP и SRP, что в свою очередь дает нам взаимозаменяемость модулей.
                                                            • 0
                                                              Да, придется дублировать код в разных модулях,

                                                              Почему придется что-то дублировать? Композицию никто не отменял.

                                                              • +3
                                                                В статье есть об этом фраза:
                                                                — И дублировать код делегирования? Это какая-то фигня.

                                                                Как минимум код делегирования нужно будет дублировать. Лично я не считаю это проблемой, но в контексте этой статьи – это «проблема».
                                                                • +1
                                                                  В языке нет композиции «из коробки», в этом проблема.
                                                            • –8
                                                              Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.

                                                              "Не пользуйтесь ООП в ООП"? Ну круто, теперь заживём.


                                                              А ещё частенько ведёт и к нарушению LSP.

                                                              А если писать код как попало, то это "частенько" ведёт к нарушению всего SOLID. Это не причина.


                                                              И вообще, при слепом следовании SOLID и всему зоопарку сокращений с прикольными именами код получается тем ещё говном, так что ненавижу, когда прикрываются коллекциями советов для домохозяк.


                                                              Количество LOC будет примерно сравнимым при композиции.

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


                                                              Ещё более опасна длинная цепочка наследования.

                                                              Жить вообще опасно. Просто если хочешь унаследоваться вместо композиции — подумай 10 раз, унаследоваться глубоко — 100, унаследоваться множественно — 1000.


                                                              Глубокое наследование часто можно наблюдать в иерархиях гуёвых контролов. Потому что это работает.


                                                              Множественное наследование можно наблюдать в простых реализациях паттернов. Потому что это работает.


                                                              Если же пихать везде без разбора — получается чёрт знает что. Но это работает с любой фичей языка. Что угодно можно использовать во вред.


                                                              Интерфейсы ни от чего не спасают. Это ужасная сущность с точки зрения развития системы, потому что их изменять вообще невозможно. Любое изменение — всё, система сломана. Чем это лучше классов, где что-то в предках изменилось, и вдруг поломался потомок? Ну, хотя бы есть ненулевой шанс, что оно будет работать. Изменение интерфейса ломает систему всегда.


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


                                                              Рассуждение выше про интерфейсы немного устаревает с введением костыля под названием "default interface implementation" — интерфейсы теперь становятся недо-классами. Вот только не понимаю, как проповедник всего чистого в коде и идеализированных принципов построения архитектуры может оправдывать эту фичу. Default interface implementaon, между прочим, тоже вполне себе может ломать наследников, причём именно в непредсказуемом стиле, как и любые классы при наследовании.

                                                              • 0

                                                                Народ, если у вас есть мнение, то, пожалуйста, выразите его словами.

                                                                • +1
                                                                  А что тут выражать? Все уже сказано в прошлом комментарии.
                                                                  • 0

                                                                    В каком "прошлом"?

                                                                  • +7
                                                                    "Не пользуйтесь ООП в ООП"? Ну круто, теперь заживём.

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


                                                                    А если писать код как попало, то это "частенько" ведёт к нарушению всего SOLID. Это не причина.

                                                                    Серебряной пули и правда нет. Но есть best practices и они появились не с пустого места. А насчёт аббревиатур — это удобный способ донести мысль другому человеку по-быстрому.


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

                                                                    Везде свои компромиссы. Хотите хорошую систему, с которой приятно работать и удобно вносить изменения — делегируйте; проект небольшой — колбасьте код как угодно.
                                                                    Да и про какие сотни строк вы говорите? Если у вас есть класс, который реализует так много интерфейсов, то проблема возникла раньше. Опять же из-за не следования хорошим практикам.


                                                                    Жить вообще опасно. Просто если хочешь унаследоваться вместо композиции — подумай 10 раз, унаследоваться глубоко — 100, унаследоваться множественно — 1000.

                                                                    Об этом и речь — зачем усложнять и думать 10-100-1000 раз, если можно сделать просто?


                                                                    Интерфейсы ни от чего не спасают. Это ужасная сущность с точки зрения развития системы, потому что их изменять вообще невозможно. Любое изменение — всё, система сломана.

                                                                    Интерфейсы это ваш контракт. Это ваш api, если хотите. И если контракт меняется, значит на то была причина — изменилось требуемое поведение.


                                                                    Чем это лучше классов, где что-то в предках изменилось, и вдруг поломался потомок? Ну, хотя бы есть ненулевой шанс, что оно будет работать. Изменение интерфейса ломает систему всегда.

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


                                                                    Рассуждение выше про интерфейсы немного устаревает с введением костыля под названием "default interface implementation" — интерфейсы теперь становятся недо-классами.

                                                                    Согласен, default interface implementation неоднозначная фича. Пока что, я вижу ей применение для добавление утилитарного поведения, вроде того же observer'a.


                                                                    Кстати, насчёт глубокой цепочки наследования в GUI — тот же реакт построен на High Order Components и там этот подход весьма органичен.

                                                                    • 0
                                                                      Если у вас есть класс, который реализует так много интерфейсов, то проблема возникла раньше.

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


                                                                      В результате в COM мы имеем наследование от IContextMenu1, IContextMenu2, IContextMenu3, IContextMenu4, а в C# имеем ICollection, IReadOnlyCollection, IReadOnlyList (причины разные, результаты разные, но последствия всегда неприятные). И вот никуда от этого не деться. Ну не задизайнить интерфейсы так, чтобы один раз и на всю жизнь.

                                                                      Интерфейсы это ваш контракт. Это ваш api, если хотите. И если контракт меняется, значит на то была причина — изменилось требуемое поведение.

                                                                      Ну вот допустим в C# был бы не класс FileStream, а интерфейс IFileStream. Теперь мы хотим добавить поддержку асинхронного чтения. Ваши действия? Добавить новый интерфейс? Расширить существующий? Любое решение с интерфейсами (без default interface implementation) будет неудобным для потребителя.


                                                                      С композицией надо проверить только там, где изменилось.

                                                                      Идеализирование. Реализация всего может поменяться в любой момент несовместимым способом. Ломается всё, независимо от архитектуры, поэтому всё равно вы всё будете тестировать, если хотите спокойно спать по ночам.

                                                                      • +3
                                                                        Интерфейсы это ваш контракт. Это ваш api, если хотите.

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

                                                                        • +1

                                                                          Некоторые советуют дополнять реализацию абcтрактными тестами

                                                                          • +1

                                                                            Ну вот как-то так и приходится выкручиваться.

                                                                            • 0

                                                                              Так и для абстрактых классов придется делать то же самое. Или что-то типа Code.Contracts

                                                                              • 0

                                                                                Я больше про поддержку контрактного программирования на уровне языка :)

                                                                            • 0
                                                                              Просто перебрать всех наследников недостаточно хорошо?
                                                                              • +2
                                                                                А как если у вас opensource библиотека у которой тысячи пользователей и каждый может сделать своего наследника от ваших классов?
                                                                                • 0
                                                                                  Эмм, и как вы предлагаете тестировать даунстрим-код? Как бы
                                                                                  Derive a specific test class per implementer

                                                                                  подразумевает, что у нас есть код всех имлементеров.
                                                                                  • 0

                                                                                    Для даунстрима Derive a specific test class per implementer выполняет даунстримщик.

                                                                                • 0

                                                                                  Там же написано — каждый наследник может иметь свой конструктор + дополнительно свой набор каких-то еще тестов. Абстрактный тест вызвает абстрактный factory method для создания конкретной реализации.


                                                                                  К тому же какие-то ассерты тоже могут быть абстрактными

                                                                          • +3
                                                                            Народ, если у вас есть мнение, то, пожалуйста, выразите его словами.

                                                                            Человек, для которого ООП сводится к наследованию ("Не пользуйтесь ООП в ООП") — настолько далёк от понимания предмета, что нет смысла даже тратить время на объяснения — просто ставишь минус и идёшь дальше.

                                                                            • 0
                                                                              Это, наверное, человек, для которого ООП сводится к избеганию лишнего дублирования кода. Такой взгляд тоже кажется имеющим право на существование.
                                                                              • +2
                                                                                Человек, для которого ООП сводится к наследованию ("Не пользуйтесь ООП в ООП")

                                                                                Это передёргивание слов. Я нигде не говорил, что наследование — единственное. Но всё-таки наследование и полиморфизм — столпы классического ООП. Заметать их под ковёр странно.

                                                                            • +2
                                                                              в большинстве языков нет реализации интерфейса через член
                                                                              Можно другими словами или пример кода, что бы стало понятней о чем речь?
                                                                              • +4

                                                                                Фиче-реквест для C# не нашёл. Идея в том, чтобы писать что-то подобное (синтаксис условный):


                                                                                class MyCollection : ICollection, ICollection<T>, IReadOnlyCollection<T> {
                                                                                  private IList<T> _collection;
                                                                                  [ ICollection is implemented by _collection ]
                                                                                  [ ICollection<T> is implemented by _collection ]
                                                                                  [ IReadOnlyCollection<T> is implemented by _collection ]
                                                                                }

                                                                                вместо ручной реализации каждого метода:


                                                                                  // ...
                                                                                  int Count { get { return _collection.Count; } }
                                                                                  bool IsReadOnly { get { return _collection.IsReadOnly; } }
                                                                                  // ...

                                                                                Собственно, сказка для композиции.

                                                                                • 0
                                                                                  Интересно, а в каких языках такое есть?
                                                                                  • 0

                                                                                    Очень похожее есть в PHP — ниже есть пример. Только нет явной привязки "имплементации" к интерфейсу.

                                                                                    • 0
                                                                                      Swift?
                                                                                        • 0

                                                                                          В котлине – почти то, что нужно. Но там, я так понял, это возможно только для делегата, определяемого в конструкторе.

                                                                                          • 0
                                                                                            Иначе небезопасно — поведение становится неконсистентным. Оно зависит от того, проставлен ли у меня член-делегат в корректное значение, или нет. Тогда даже имея корректный not null объект MyCollection, я все равно не могу быть никогда уверен, что я могу вызывать на нем с гарантированно корректными аргументами методы интерфейсов, которые он должен реализовать.

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

                                                                                            А так у вас есть гарантии — на not null объекте всегда можно корректно вызывать методы интерфейсов, которые он реализует, и дальнейшее поведение зависит только от реализации.

                                                                                            Хотите поменять вложенный объект, который определяет поведение? Вы всегда можете сделать вместо:

                                                                                            class Derived(b: Base) : Base by b


                                                                                            вот так:

                                                                                            class Derived(var b: Base) : Base by b


                                                                                            и потом где-то в коде:

                                                                                            derived.b = otherB


                                                                                            и компилятор также проверит, что otherB — not null, и поведение останется консистентным.

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

                                                                                            derived = Derived(null!!) // не делайте так


                                                                                            или даже так:

                                                                                            class Derived(var b: Base = null!!): Base by b // не делайте так тем более!
                                                                                            • 0
                                                                                              (хотя, кстати, я неправ — Kotlin даже не позволит сделать такие хаки, тогда для таких случаев только делать подобный код на Java без синтаксического сахара)
                                                                                              • +1
                                                                                                Хотите поменять вложенный объект, который определяет поведение? Вы всегда можете сделать вместо:
                                                                                                class Derived(b: Base): Base by b
                                                                                                вот так:
                                                                                                class Derived(var b: Base): Base by b
                                                                                                и потом где-то в коде:
                                                                                                derived.b = otherB

                                                                                                Как бы не тут-то было. Написать-то вы так можете, и даже компилятор не ругнется, но работать оно не будет:


                                                                                                The by-clause in the supertype list for Derived indicates that b will be stored internally in objects of Derived and the compiler will generate all the methods of Base that forward to b.

                                                                                                Т.е. оно внутри где-то сохранится, и делегировать будет все время одному и тому же объекту. Вот, например, обратите внимание, что в обоих случаях выводится "Hello from A"

                                                                                          • +2

                                                                                            В delphi. Как минимум с 7. Не слишком полезная фича, имхо.

                                                                                            • 0
                                                                                              Интересно, а в каких языках такое есть?
                                                                                              В Delphi есть. Точно есть в 7-й версии (2002 года), а может и раньше появилось.
                                                                                              • 0
                                                                                                Ключевое слово implemets появилось в Delphi 4 в 1998 году, это я точно помню.
                                                                                            • +1

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

                                                                                              • +5

                                                                                                Да, но несуществующий код лучше сгенерированного.

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

                                                                                                  А вот вещи вроде асинхронных конструкторов, которые нужны то и дело, действительно упростили бы все.

                                                                                                  То есть это хорошо, конечно, но есть более ценные фичи.
                                                                                                  • +1

                                                                                                    Это у вас "врапперов" мало. Однако есть сторонники идеологии, что должны быть только интерфейсы и их реализации, наследования быть не должно, только композиция. Не могу сказать, что это плохая идея, но без подобного сахара ей следовать проблематично, и сгенерированный код мало помогает.

                                                                                                    • 0
                                                                                                      В таком случае да, было бы полезно.

                                                                                                      Но в реальности я такого кода встречал мало. Все же часто используется отношение is, композиция тоже используется, но тогда вызывающий код просто игнорирует закон Деметры и стучится к нужным свойствам напрямую. По крайней мере кодовая база десятков проектов, что я видел, устроена именно так. Писать однострочные прокси обычно никто не хочет.
                                                                                                      • +1
                                                                                                        Но в реальности я такого кода встречал мало.

                                                                                                        Потому что все горазды рассуждать про преимущества композиции над наследованием, про разработку через тестирование, про закон Деметры и прочее, а когда надо делать работу, а не чесать языком, берут и пишут как все простые смертные.

                                                                                            • 0

                                                                                              Нашёл: C# Feature Request: Expression bodied member syntax for interface implemented by field.


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

                                                                                              • +1
                                                                                                «Реализация интерфейса через член»… Долго думал…
                                                                                                • +3
                                                                                                  Программисты, как и математики, к слову «член» должны быть нечувствительны.
                                                                                                  • –1
                                                                                                    «Она была программисткой, а потому к члену была нечувствительна...»
                                                                                            • 0
                                                                                              К сожалению, агрегации «из коробки», в отличие от наследования, в языках программирования, по крайней мере распространённых, нет.
                                                                                              • 0
                                                                                                Объясните о чем вы, Field в объекте это не агрегация / композиция?
                                                                                                • –1
                                                                                                  Нужно специальное ключевое слово для поля, которое обозначало бы, что оно и есть делегат.
                                                                                                  • +1

                                                                                                    Kotlin, делегирование реализации интерфейсов, делегирование пропертей. Слово — "by".

                                                                                            • +2
                                                                                              Мдя. Надо ставить тег «Юмор». Бивис и Баттхет обсуждают ООП. И не понимая что слово интерфейс означает в обычной жизни.
                                                                                              • +3
                                                                                                "— Правда? А что такое интерфейс? Это то же самое что и класс?
                                                                                                — Ну… Не совсем!
                                                                                                — В каком плане?
                                                                                                — Не один из его методов не должен иметь реализации."
                                                                                                В Плане Java и C# — не верно, а в общем плане
                                                                                                public abstract class MyInterface {
                                                                                                  public abstract void f();
                                                                                                }
                                                                                                — интерфейс
                                                                                                • +12
                                                                                                  — Правда? А что такое интерфейс? Это то же самое что и класс?
                                                                                                  — Ну… Не совсем!
                                                                                                  — В каком плане?
                                                                                                  — Не один из его методов не должен иметь реализации.

                                                                                                  Очень уж мне не нравится это определение интерфейса. Лично для себя сформулировал, что интерфейс — это описание контракта, и это не класс со всеми методами без реализаций. Это разные сущности, в том-же C# можно явно реализовать интерфейс, попроб