Перевод ироничного поста из блога Боба Мартина в котором он рассуждает о том, насколько неудачным является использование слова 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 — вреден и губителен!


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

Комментарии (354)


  1. Hedgar2018
    19.03.2018 02:20
    -1

    Пора уже перейти на Rust и выкинуть всю эту допотопную дичь. А со временем в Ruste всё допилят, если там чего-то еще не хватает.


    1. Flux
      19.03.2018 14:39
      +3

      — Пора уже перейти на C и выкинуть всю эту допотопную asm дичь.
      — Пора уже перейти на C++ и выкинуть всю эту допотопную C дичь.
      — Пора уже перейти на Java/.Net и выкинуть всю эту допотопную дичь.
      — Пора уже перейти на Go и выкинуть всю эту допотопную дичь.

      Пора уже перейти на Rust и выкинуть всю эту допотопную дичь.
      © Hedgar2018 — Full-stack Golang & JS developer

      Никогда такого не было — и вот опять! Когда на haskell переходить будем с этой допотопной дичи?


      1. Eagle_NN
        19.03.2018 16:35
        -1

        До Rust еще далеко… Да и не факт что к тому времени что то другое не изобретут… А вот в 2018 году на Go вполне пора переходить :)


        1. TheShock
          19.03.2018 16:55
          +1

          На Go вообще никогда не стоит переходить


          1. Eagle_NN
            19.03.2018 16:56

            Наверняка такому категорическому заявлению есть и железная аргументация...?


            1. TheShock
              19.03.2018 16:57
              +4

              Сразу после того, как вы аргументируете, что на Go надо переходить в 2018 =)


              1. Eagle_NN
                19.03.2018 17:10
                -1

                1. Быстрый
                2. Стабильный
                3. Развивается
                4. Структурирован (Сокращает количество ошибок)
                5. Мультиплатформенный
                6. Общего назначения

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

                P.S. Да, я на всем перечисленном писал. :)


                1. TheShock
                  19.03.2018 17:24

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

                  1. Непродуманный и устаревший синтаксис языка, провоцирующий быдлокод
                  2. Непоследовательная и плохая стандартная библиотека
                  3. Плохой менеджер пакетов


                  1. Eagle_NN
                    19.03.2018 17:31

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


                    1. Athari
                      19.03.2018 17:42

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

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


                    1. TheShock
                      19.03.2018 17:46
                      +4

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

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

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

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

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

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


                      1. w0den
                        19.03.2018 19:28

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


                        1. zagayevskiy
                          20.03.2018 15:44
                          +2

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

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


                1. Athari
                  19.03.2018 17:26

                  С — переросли

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


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

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


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

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


                  1. TheShock
                    19.03.2018 17:29
                    +1

                    Весь кровавый интерпрайз там
                    А кроме энтерпрайза сейчас 95% геймдева — это один из трех языков: C++, C# или JS.


                    1. Eagle_NN
                      19.03.2018 17:56

                      Я и не говорил что языки отмерли. Я просто обозначал вектор развития.
                      Сам не так давно вынужден был на asm под ARM кодить. Но это не значит что это тренд…


                      1. Athari
                        19.03.2018 19:33

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


                        1. Eagle_NN
                          20.03.2018 11:33

                          Не все и не всегда работают чисто ради денег.
                          У некоторых есть основные проекты «нетренд» и дополнительные, в которых инструментарий не лимитируется.
                          А вообще по ответам, к сожалению, тенденция всех форумов прослеживается и тут. Только в 1 ответе было показано с примерами что человеку нравится С++. Остальные просто ругают то что не пробовали или вообще, просто, ругают, при этом не говорят где лучше.
                          Грустно все это, ну да ладно. У каждого свой путь самовыражения.


                          1. 0xd34df00d
                            20.03.2018 18:33

                            Не обязательно сильно много ковыряться в каком-то языке и досконально его пробовать, чтобы понять, интересен ли он вам или нет. И если выбирать, на что потратить своё время, скажем, на Go или на какой-нибудь Idris, особенно если при этом не нужно задумываться о деньгах, то выбор-то довольно простой. Для меня, по крайней мере.


                1. 0xd34df00d
                  19.03.2018 19:54
                  +1

                  1. На С++ можно писать не медленнее.
                  2. С++ очень следит за сохранением совместимости с имеющимся кодом, иногда даже излишне.
                  3. С++ развивается. Вон, в 20-й версии наверняка будут корутины, концепты и компилтайм-рефлексия, и почти наверняка адекватные модули, например. Если повезёт, успеют прикрутить к корутинам всё необходимое для выражения через них произвольных линейных монад.
                  4. Особенно их сокращает паттерн проверки на ошибки, наверное.
                  5. Как и куча других языков.
                  6. Как и куча других языков.


                  1. Athari
                    21.03.2018 02:32

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


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


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


                    1. 0xd34df00d
                      21.03.2018 04:07

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

                      И никаких пары лет.


                      1. PsyHaSTe
                        21.03.2018 11:14

                        А если примут, то что? Стреляться?


            1. Athari
              19.03.2018 17:18

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



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


              1. Eagle_NN
                19.03.2018 17:20

                Нескромный вопрос, а вы это пробовали делать, или не видел, но осуждаю?


                1. TheShock
                  19.03.2018 17:25

                  Я пробовал делать и немало. Отвратительный язык. Хотя с `go something()` придумали неплохо, но это не перевешивает его недостатки.


                1. Athari
                  19.03.2018 17:40
                  +3

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


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


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


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


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


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


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


                  1. Eagle_NN
                    19.03.2018 18:13

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

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

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

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

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


                    1. Varim
                      19.03.2018 18:20

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


                    1. TheShock
                      19.03.2018 18:32

                      Парадигма Go — простота. Использование дженериков ведет к усложнению кода

                      То есть


                      var identity types.Identity
                      var err = json.NewDecoder(json).Decode(&identity)

                      Это быстро и просто?


                      var identity = JsonConvert.DeserializeObject<Identity>(json);

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


                      Использование дженериков, очевидно, ведет к упрощению кода, а не усложнению. Хватит повторять библию Гошников. Есть куча примеров, где отсутствие дженериков ведет или к отсутствию статики или к копипасту.


                      1. Eagle_NN
                        19.03.2018 18:43
                        -3

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

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


                        1. mayorovp
                          19.03.2018 18:52
                          +4

                          template != generic

                          Дженерики уже до предела упрощены.


                          1. Eagle_NN
                            19.03.2018 19:36
                            -4

                            Я в курсе. Это просто пример приключений для пятой точки.


                            1. TheShock
                              19.03.2018 19:50
                              +2

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


                          1. 0xd34df00d
                            19.03.2018 19:59

                            Полиморфные методы в ML-подобных языках тоже предельно просты и вполне мономорфизируются при компиляции.


                            1. mayorovp
                              19.03.2018 20:03

                              Так ведь они больше похожи на generic чем на template же. Или я ошибаюсь?


                              1. 0xd34df00d
                                19.03.2018 20:24

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

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

                                Но вообще я это всё скорее в подтверждение ваших слов.


                                1. mayorovp
                                  19.03.2018 20:35

                                  Хм, посмотрел я по вашим ссылкам… Похоже, все-таки успели испортить хорошие языки шаблонами. Кошмар начинается с появлением Type families. Даже не представляю как может работать вывод типов в таких условиях.


                                  1. 0xd34df00d
                                    19.03.2018 20:49

                                    Ну, на практике семейства типов не сильно-то его и ломают, особенно инъективные.

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

                                    А в Idris вывода типов функций, считайте, толком и нет (за парой исключений), но это в основном потому, что в присутствии зависимых типов вывод типов неразрешим в смысле Тьюринга.


                        1. TheShock
                          19.03.2018 19:00
                          +3

                          Конкретных примеров как в одну сторону так и в другую можно найти массу
                          Но это ведь вы категорично заявили, без дополнительных «если»:
                          Использование дженериков ведет к усложнению кода


                    1. JekaMas
                      19.03.2018 18:43
                      +2

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


                    1. mayorovp
                      19.03.2018 18:55
                      +4

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


                      1. Szer
                        19.03.2018 19:24
                        +2

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

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


                    1. Athari
                      19.03.2018 19:56

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

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


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

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


                      1. DistortNeo
                        19.03.2018 20:45
                        +1

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

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


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


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


                    1. 0xd34df00d
                      19.03.2018 19:58

                      В Go используются горутины и общаются они через каналы. Этот костыль просто не нужен при правильном построении программы.

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

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

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


                    1. alex1t
                      20.03.2018 18:40
                      +1

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

                      Так async/await по сути и есть более удобный синтаксис корутин/горутин/сопроцедур, что есть одно и то же. Тем более, что вообще C# поддерживал в некотором роде сопроцедуры ещё с версии 2.0, когда появилось слово yeild и итераторы. Это уже позволяло (пусть и не так удобно) писать корутины и получать всю прелесть, что сейчас есть с async/await. Так что в этом плане горутины не новость.


                      1. Eagle_NN
                        20.03.2018 18:59

                        Async/await это по сути своей синтаксический сахар, который при компиляции просто заменяется на последовательность вызовов. На сколько помню, все вызовы будут вполне линейны с точки зрения процессора, но программе будет казаться что исполняется параллельно.
                        Аналогично и слово yeild. Только там генерируется класс, который, по мере надобности, возвращает результат и имеет внутреннюю стейт машину.
                        В отличии от этих методов горутины это реальные параллельные потоки. Да, количество реально параллельных потоков соотносится с количеством ядер процессора. Но все равно они параллельны.


                        1. PsyHaSTe
                          20.03.2018 20:30

                          async/await это про асинхронность, которая ничего общего с параллельностью не имеет.

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


                        1. lair
                          20.03.2018 21:29

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

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


                        1. alex1t
                          21.03.2018 10:09

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


                          1. PsyHaSTe
                            21.03.2018 11:15
                            +1

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


                            1. alex1t
                              21.03.2018 12:42
                              +1

                              Полностью согласен.


      1. 0xd34df00d
        19.03.2018 19:52

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

        А если серьёзно, лично я был бы очень рад, если бы больше софта и библиотек писалось на выразительных и безопасных языках.


    1. strangeraven
      20.03.2018 00:38

      deleted


    1. guai
      20.03.2018 18:27

      тенденция скорее такая, что руст сам обрастет той же допотопной дичью :)


  1. Veikedo
    19.03.2018 02:56
    +1

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


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


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


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


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


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

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


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


    1. DrPass
      19.03.2018 03:03

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

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

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

      Если существовала бы какая-то «серебряная пуля», которая помогала бы реализовать сложное поведение с помощью простой и очевидной модели, мы бы все ей пользовались :)


      1. Bonart
        19.03.2018 08:40

        Серебряная пуля конкретно для наследования есть и она очень простая: «не наследуйтесь от реализаций».


        1. DrPass
          19.03.2018 14:15
          +1

          Да, только «не наследуйтесь от реализаций» можно, по сути сократить до «не наследуйтесь» :)


          1. Flammar
            19.03.2018 18:31

            Только до «не наследуйтесь от неабстрактных классов».


            1. semmaxim
              20.03.2018 11:04

              Абстрактные классы либо содержат часть реализации либо заменяются на интерфейсы.


        1. Flammar
          19.03.2018 18:31

          К сожалению, это не «серебряная пуля», а только правило гигиены.


      1. AndreyRubankov
        19.03.2018 10:06

        Нет, в таком случае обычно вводится ещё один промежуточный класс в иерархию
        Что в свою очередь делает всю систему еще более запутанной. Видел я систему компонентов из 7+ уровней наследования, казалось бы все красиво и круто, но разбираться в этом – то еще удовольствие :(

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


        1. Bonart
          19.03.2018 10:18

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

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


          1. AndreyRubankov
            19.03.2018 11:44
            +1

            В статье есть об этом фраза:

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

            Как минимум код делегирования нужно будет дублировать. Лично я не считаю это проблемой, но в контексте этой статьи – это «проблема».


          1. Flammar
            19.03.2018 18:32

            В языке нет композиции «из коробки», в этом проблема.


    1. Athari
      19.03.2018 10:46

      Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.

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


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

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


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


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

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


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

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


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


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


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


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


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


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


      1. Athari
        19.03.2018 11:40

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


        1. mayorovp
          19.03.2018 11:46

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


          1. Athari
            19.03.2018 11:56

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


        1. Veikedo
          19.03.2018 12:20
          +1

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

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


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

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


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

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


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

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


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

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


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

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


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

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


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


          1. Athari
            19.03.2018 12:43

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

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


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

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

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


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

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


          1. VolCh
            19.03.2018 13:03

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

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


            1. ApeCoder
              19.03.2018 13:14

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


              1. VolCh
                19.03.2018 13:22

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


                1. ApeCoder
                  19.03.2018 13:28

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


                  1. VolCh
                    19.03.2018 18:16

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


              1. PsyHaSTe
                19.03.2018 22:30

                Просто перебрать всех наследников недостаточно хорошо?


                1. vedenin1980
                  19.03.2018 22:36
                  +2

                  А как если у вас opensource библиотека у которой тысячи пользователей и каждый может сделать своего наследника от ваших классов?


                  1. PsyHaSTe
                    19.03.2018 23:57

                    Эмм, и как вы предлагаете тестировать даунстрим-код? Как бы

                    Derive a specific test class per implementer

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


                    1. ApeCoder
                      20.03.2018 10:06

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


                1. ApeCoder
                  20.03.2018 10:05

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


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


        1. Dicebot
          19.03.2018 18:39
          +2

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

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


          1. Flammar
            19.03.2018 19:07

            Это, наверное, человек, для которого ООП сводится к избеганию лишнего дублирования кода. Такой взгляд тоже кажется имеющим право на существование.


          1. Athari
            19.03.2018 20:05

            Человек, для которого ООП сводится к наследованию ("Не пользуйтесь ООП в ООП")

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


      1. Varim
        19.03.2018 11:57

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


        1. Athari
          19.03.2018 12:05
          +1

          Фиче-реквест для 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; } }
            // ...

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


          1. Varim
            19.03.2018 12:27

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


            1. VolCh
              19.03.2018 13:05

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


            1. dpodoprosvetov
              19.03.2018 13:25

              Swift?


            1. xander27
              19.03.2018 13:43

              1. ookami_kb
                19.03.2018 14:19

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


                1. artemshitov
                  19.03.2018 20:09

                  Иначе небезопасно — поведение становится неконсистентным. Оно зависит от того, проставлен ли у меня член-делегат в корректное значение, или нет. Тогда даже имея корректный 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 // не делайте так тем более!


                  1. artemshitov
                    19.03.2018 22:37

                    (хотя, кстати, я неправ — Kotlin даже не позволит сделать такие хаки, тогда для таких случаев только делать подобный код на Java без синтаксического сахара)


                  1. ookami_kb
                    20.03.2018 13:14
                    +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"


            1. Akon32
              19.03.2018 17:51
              +1

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


            1. qw1
              19.03.2018 17:55

              Интересно, а в каких языках такое есть?
              В Delphi есть. Точно есть в 7-й версии (2002 года), а может и раньше появилось.


              1. DrPass
                20.03.2018 03:07

                Ключевое слово implemets появилось в Delphi 4 в 1998 году, это я точно помню.


          1. Veikedo
            19.03.2018 12:31

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


            1. Athari
              19.03.2018 12:45
              +1

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


              1. PsyHaSTe
                19.03.2018 22:32
                -2

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

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

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


                1. Athari
                  19.03.2018 22:45

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


                  1. PsyHaSTe
                    20.03.2018 11:42

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

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


                    1. Athari
                      20.03.2018 12:02

                      Но в реальности я такого кода встречал мало.

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


        1. Athari
          19.03.2018 12:14

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


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


        1. vg7
          19.03.2018 15:42

          «Реализация интерфейса через член»… Долго думал…


          1. DrPass
            19.03.2018 17:31

            Программисты, как и математики, к слову «член» должны быть нечувствительны.


            1. vg7
              19.03.2018 18:44
              -1

              «Она была программисткой, а потому к члену была нечувствительна...»


    1. Flammar
      19.03.2018 18:30

      К сожалению, агрегации «из коробки», в отличие от наследования, в языках программирования, по крайней мере распространённых, нет.


      1. Varim
        19.03.2018 18:35

        Объясните о чем вы, Field в объекте это не агрегация / композиция?


        1. Flammar
          19.03.2018 19:10
          -1

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


          1. zagayevskiy
            20.03.2018 15:55
            +1

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


  1. Steamus
    19.03.2018 04:13
    +1

    Мдя. Надо ставить тег «Юмор». Бивис и Баттхет обсуждают ООП. И не понимая что слово интерфейс означает в обычной жизни.


  1. Myxach
    19.03.2018 04:13
    +1

    "— Правда? А что такое интерфейс? Это то же самое что и класс?
    — Ну… Не совсем!
    — В каком плане?
    — Не один из его методов не должен иметь реализации."
    В Плане Java и C# — не верно, а в общем плане

    public abstract class MyInterface {
      public abstract void f();
    }
    — интерфейс


  1. saltukkos
    19.03.2018 06:31

    — Правда? А что такое интерфейс? Это то же самое что и класс?
    — Ну… Не совсем!
    — В каком плане?
    — Не один из его методов не должен иметь реализации.

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


    1. Free_ze
      19.03.2018 12:32

      Чем абстрактный класс — не описание контракта?


      1. zodchiy
        19.03.2018 12:50

        В чем преимущество (кроме наследования) абстрактного класса без реализации перед интерфейсом?


        1. Free_ze
          19.03.2018 12:59

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


          1. mayorovp
            19.03.2018 13:43

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


            1. Free_ze
              19.03.2018 13:44

              Тут это уже поднималось


      1. DistortNeo
        19.03.2018 12:50

        Да, абстрактный класс тоже можно использовать как контракт, но возможности интерфейсов C# шире — вы можете навесить интерфейс в классе-потомке.

        class CBase
        {
            public void Foo() { ... }
        }
        
        interface IFoo
        {
            void Foo();
        }
        
        class CDerived: CBase, IFoo {}
        


        Попробуйте, не меняя класса CBase, сделать то же самое, но с помощью классов


        1. Free_ze
          19.03.2018 13:06

          В C# нет множественного наследования ведь) Если бы было, то ничто не мешало бы запилить подобное и для абстрактного метода Foo.


          1. DistortNeo
            19.03.2018 13:19

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


            1. Free_ze
              19.03.2018 13:27

              Напишите подобное на C++
              C++ — плохой пример, там куча проблем с дизайном. Я могу на C#-подобном псевдокоде написать, хотите?
              Представьте, что оно есть.
              Не вопрос)
              class CBase {
                  public void Foo() { ... }
              }
              abstract class BaseFoo {
                  abstract void Foo();
              }
              class CDerived: CBase, BaseFoo {}


              1. DistortNeo
                19.03.2018 14:27

                Такое возможно только в одном случае: если метод BaseFoo.Foo() перекрывает CBase.Foo() и имеет автоматическую неизменяемую реализацию:

                class CDerived: CBase, BaseFoo
                {
                    // Compiler generated
                    public sealed override void Foo() { CBase.Foo(); }
                }


                1. Free_ze
                  19.03.2018 15:40

                  Похоже на то. Это уже технические детали реализации компилятора/CLR, неизменяемость зависит от виртуальности CBase.Foo.


                  1. DistortNeo
                    19.03.2018 15:56

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

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


                    1. Free_ze
                      19.03.2018 16:06

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

                      Гораздо проще раздить интерфейсы и абстрактные классы.
                      Выделить чистые контракты в API — вполне съедобная идея, но только не ценой множественного наследования, ИМХО.


      1. VolCh
        19.03.2018 13:06

        Тем, что в него можно добавить реализацию, состояние и вообще почти всё, что угодно :)


        1. Free_ze
          19.03.2018 13:11

          Почему контракт не может быть настраиваемым?


          1. VolCh
            19.03.2018 13:23

            Это уже контракт, а шаблон контракта.


            1. Free_ze
              19.03.2018 13:33

              Почему шаблон контракта — не контракт? У контракта вполне может быть такая гибкая природа, лишь бы клиент был в курсе.


      1. saltukkos
        19.03.2018 14:12

        Не знаю, насколько точную я аналогию смогу привести, но интерфейс — это принципиальная схема какого-то устройства, а абстрактный класс — это, если хотите, печатная плата, в которую надо впаять нужные детали.
        Если говорить в терминах С++, то эта «плата» — таблица виртуальных функций. Если объект унаследован от N абстрактных классов, то, в зависимости от того, под каким из N типов вы на него смотрите, this сможет принимать до N+1 значения. В случае с интерфейсами — это один объект, который реализует N интерфейсов и this там одинаковый. Учитывая то, что мы захотим виртуальный деструктор — вот вам и пачка смешений.
        Например, если посмотреть на __declspec(interface), то он подразумевает под собой novtable, из-за чего мы не можем удалить объект по ссылке на интерфейс.


        1. Free_ze
          19.03.2018 14:56

          интерфейс — это принципиальная схема какого-то устройства, а абстрактный класс — это, если хотите, печатная плата, в которую надо впаять нужные детали
          Это справедливо лишь в случае, если абстрактный класс содержит состояние. Тогда интерфейс, как синтаксическая конструкция, избыточен.
          Если говорить в терминах С++
          Да, в C++ плохая реализация множественного наследования. Другие языки вводят специальные «правила разруливания», позволяющие использовать одного родителя, а не таскать за собой топу зачастую идентичных состояний.


          1. saltukkos
            19.03.2018 15:15

            Да, я с вами согласен, что в случае, когда у вас есть класс, в котором все методы чисто виртуальные — эта синтаксическая конструкция эквивалентна интерфейсу.
            Но есть одно «но». Никто вам не гарантирует, что завтра какой-нибудь абстрактный програмист не решит добавить в этот базовый абстрактный класс маленький флажочек/кэш, и тогда код скомпилируется, но если где-то упаси боже (увидел, что вы C#/C++ разработчик), при маршалинге из нативных плюсов в Managed-мир вы решите кастануть указатель к IntPtr, а потом где-то обратно передать в нативный мир, кастанув к указателю на базовый класс, то this поедет и код будет мазать по памяти (понятно, что это легко решается типизацией IntPtr до какого-то своего типа в managed-мире, но всё же).
            Пример, конечно, надуманный, но я хочу донести то, что ЯП предоставляет гарантии и уменьшает количество способов стрельнуть в ногу. И понятие интерфейса в том же самом С++ дало бы гарантию того, что в этой сущности не может быть состояния.


            1. Free_ze
              19.03.2018 15:32

              Никто вам не гарантирует, что завтра какой-нибудь абстрактный программист не решит
              Я правильно понимаю, что человек лезет изменять контракты и не знает, как они работают?

              ЯП предоставляет гарантии и уменьшает количество способов стрельнуть в ногу
              Не бесплатно. Сужая возможности язык вынуждает писать больше кода. Больше кода — больше багов, как известно.


              1. saltukkos
                19.03.2018 15:53

                Я правильно понимаю, что человек лезет изменять контракты и не знает, как они работают?

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

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


                1. Free_ze
                  19.03.2018 16:00

                  Нет, речь не про это, эти изменения можно осуществить без изменения контракта.
                  Согласен. Но для этого отдельное понятие «интерфейс» не необходимо.

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

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


                  1. saltukkos
                    19.03.2018 16:13

                    Именно так, тоже хотел об этом написать, можно добавить ключевое слово interface хоть в С++, оставив множественное наследование (если бы не проблема с виртуальными деструкторами).
                    Проблема с виртуальным деструктором тоже решаема, но это тема для отдельной дискуссии, уходящей в итоге в наличие «object» в языке.


                    1. 0xd34df00d
                      19.03.2018 20:17

                      Если всё, что вам нужно от интерфейсов — отсутствие состояния и реализаций функций, то это можно будет сделать с метаклассами.


              1. areht
                19.03.2018 20:14

                > Больше кода — больше багов, как известно.

                Источник знания покажете?


                1. Szer
                  19.03.2018 20:23

                  Ну, ГОСТ 27.002—89 и https://ru.wikipedia.org/wiki/Вероятность_безотказной_работы


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

                  Для кода тоже работает.


                  1. 0xd34df00d
                    19.03.2018 20:25

                    Компьютеры и всякую гидравлику на самолётах и космических аппаратах зря резервируют?


                    1. Szer
                      19.03.2018 20:33
                      -1

                      Так это разное. Блоки кода — последовательно соединенные элементы, каждый со своей вероятностью отказа.


                      P — вероятность отказа системы. p — вероятность отказа узла системы. n — кол-во узлов


                      Последовательное соединение узлов: P=p^n
                      Параллельное: P=1-(1-p)^n


                      Для p=0,99 n = 5,
                      P последовательного ~= 0.95
                      P параллельного = 0,9999999999


                      Последовательное соединение теряет надёжность. Параллельное — приобретает.


                      http://lib.alnam.ru/book_rdm.php?id=204


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


                      1. mayorovp
                        19.03.2018 20:37

                        Почему вы считаете, что у любых строк кода одинаковая вероятность отказа?


                        1. Szer
                          19.03.2018 20:38

                          Почему вы считаете, что у любых строк кода одинаковая вероятность отказа?

                          Потому что это всего лишь пример. Я ещё считаю что строчек кода всего 5, это не смутило?


                          По ссылке в книге полная формула приведена для общего случая.


                          1. mayorovp
                            19.03.2018 20:42

                            Меня смутило другое. Правило "Чем больше объектов в группе, тем ниже надежность всей группы" работает только при постоянной вероятности отказа объекта.


                            1. VolCh
                              19.03.2018 20:49

                              Главное, что при ненулевой вероятности отказа.


                              1. mayorovp
                                19.03.2018 21:08

                                Нет, совсем не главное. Сравните что меньше: 1 — 0,93 или 1 — 0,995?


                                1. Szer
                                  19.03.2018 21:12

                                  Нет, совсем не главное. Сравните что меньше: 1 — 0,93 или 1 — 0,995?

                                  Условия разные, в одном случае 5 в 10 раз более надёжных элементов, а в другом 3 очень ненадёжных.


                                  Очевидно же, что говорить о снижении надёжности системы с увеличением кол-ва элементов в ней можно только при прочих равных условиях, а именно что элементы те же самые.


                                  Аналогия: Как сравнить надёжность 3х строк на C# и 100 строках на asm?


                                  Я думаю надо сравнивать 3 строки на C# и 100 строк на C#


                                  1. mayorovp
                                    19.03.2018 21:14

                                    А почему вы думаете что элементы те же самые? Напомню, изначально как раз и шла речь о разных объемах кода на разных языках:


                                    Сужая возможности язык вынуждает писать больше кода. Больше кода — больше багов, как известно.


                                    1. Szer
                                      19.03.2018 21:17

                                      А почему вы думаете что элементы те же самые? Напомню, изначально как раз и шла речь о разных объемах кода на разных языках

                                      Согласен, эту цитату я упустил.
                                      Что не отменяет того что принцип работает. Применение его к очень маленьким элементам (вроде одной строки кода) под вопросом.


                            1. Szer
                              19.03.2018 20:55

                              Правило "Чем больше объектов в группе, тем ниже надежность всей группы" работает только при постоянной вероятности отказа объекта.

                              Я думаю в этом случае только подсчёт вероятности отказа сложнее — через цепи Маркова (редирект на пдф). Придётся мучаться либо с интегралами по времени, либо измерять вероятность отказа в определённом состоянии (с учётом того как объект попал в это состояние).


                              Интуитивно должно работать так же: сложнее система (больше последовательных элементов) — надёжность меньше. Дублирование системы (больше параллельных элементов) — повышает надёжность.


                              1. mayorovp
                                19.03.2018 21:11

                                Не должно. Ошибки проектирования могут вовсе не давать никакой вероятности отказа (если только не брать вероятность в пространстве всех возможных программ) — но несколько элементов вместе дадут 100% отказ.

                                Классическая ситуация — нарушение LSP при наследовании квадрата от прямоугольника.


                                1. Athari
                                  19.03.2018 22:57
                                  +1

                                  Насчёт LSP, квадрата и прямоугольника есть иные мнения: одно, второе. Первое — верно с любой точки зрения. Второе — спорное, но я придерживаюсь его. Интерфейс IRectangle никак не запрещает свойствам меняться по желанию левой пятки, в том числе при изменении другого свойства. Какую конкретно логику и контракты вы вкладываете в этот интерфейс — большинством языков программирования не описывается.


                                  Скажем, я опишу IWindow с IsMinimzed, присвою значение, потом вдруг окажется, что там не то значение, которое я присвоил, а в BoundingRect вообще непонятно что. Это нарушение контракта? Нет, это юзер нажал кнопочку.


                      1. 0xd34df00d
                        19.03.2018 20:52

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


                        1. Szer
                          19.03.2018 21:00
                          -1

                          И это всё очень нетривиально для строк кода.

                          Инженерия делает проще.
                          Берём тысячу плат и тестируем час. Отказало 2 из 1000? Эмпирическая вероятность отказа одной платы (как целого объекта) в течении часа — 0.2% (а надёжность — 99.8%)
                          Повторяем с прочими элементами, считаем суммарную надёжность.


                          С программными продуктами можно сделать похоже.
                          Выкатываем web application, нагружаем час, считаем кол-во out of service, service deny, timeout, 502 и пр. на общее кол-во запросов.
                          Абстрактно всё то же самое, программирование та же инженерия.


                  1. areht
                    19.03.2018 21:28

                    Область и условия применения: Настоящий стандарт устанавливает основные понятия, термины и определения понятий в области надежности.
                    Настоящий стандарт распространяется на технические объекты

                    Ну если вы считаете, что это про код…

                    Даже не знаю, то ли у вас тогда спросить «как вы тройное резервирование в исходном коде делать предлагаете?», то ли предложить попробовать глючность программы по количеству опкодов в .exe считать.


                    1. Szer
                      19.03.2018 21:32

                      Даже не знаю, то ли у вас тогда спросить «как вы тройное резервирование в исходном коде делать предлагаете?», то ли предложить попробовать глючность программы по количеству опкодов в .exe считать.

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


                      Ну, и банальный


                       retry 5 (fun () -> saveOnDisk ())

                      в коде, где saveOnDisk может завалиться с ненулевой вероятностью.


                      1. mayorovp
                        19.03.2018 21:33

                        Вот-вот, банальный retry 5 — и уже выбивается из общего «закона».


                        1. Szer
                          19.03.2018 21:41

                          Вот-вот, банальный retry 5 — и уже выбивается из общего «закона».

                          Это просто незнание "законов". А в терминах ТАУ — этот блок всего лишь система с отрицательной обратной связью.


                          Added: я не призываю считать надёжность каждой строчки.


                      1. areht
                        19.03.2018 22:39

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

                        Ваша теория или работает, или не работает. Если вы в ГОСТе ограничений не покажете — значит должно быть можно по опкодам считать.

                        > retry 5 (fun () -> saveOnDisk ())

                        О да, такое по надежности явно превосходит одиночный вызов как раз в (1-(1-p)^n)/p раз. Особенно, когда дескриптор закрыт строкой выше.

                        И не надо подменять надежность и параллелизацию кода на установку дополнительных железок.


                        1. Szer
                          19.03.2018 23:16

                          Ваша теория или работает, или не работает. Если вы в ГОСТе ограничений не покажете — значит должно быть можно по опкодам считать.

                          Конечно можно, кто ж мешает. Если заняться нечем в ближайшие пару лет.


                          О да, такое по надежности явно превосходит одиночный вызов как раз в (1-(1-p)^n)/p раз. Особенно, когда дескриптор закрыт строкой выше.

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


                          И не надо подменять надежность и параллелизацию кода на установку дополнительных железок.

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


                          1. areht
                            19.03.2018 23:42

                            > Если заняться нечем в ближайшие пару лет.

                            Так вы вручную считать хотите?

                            > Не от закрытого дескриптора строкой выше, нет.

                            То есть закрытый дескриптор под вашу формулу пересчёта кода в баги не подходит? Ок, вопросов больше не имею.

                            > Дополнительные фейловер инстансты моей системы — это как раз оно самое, что не так?

                            Всё хорошо, кроме того, что вы приравняли «код» и «систему».


                            1. Szer
                              19.03.2018 23:52

                              Так вы вручную считать хотите?

                              Я вообще не хочу считать.


                              То есть закрытый дескриптор под вашу формулу пересчёта кода в баги не подходит?

                              Передёргивание. Я такую "формулу" не приводил. Но приводил ту, которая может посчитать надёжность системы из надёжностей её элементов.
                              И она не моя, вы мне льстите.


                              Всё хорошо, кроме того, что вы приравняли «код» и «систему».

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


                              1. areht
                                20.03.2018 02:10

                                > Но приводил ту, которая может посчитать надёжность системы из надёжностей её элементов.

                                Ну, главное разобрались, что к моему вопросу о происхождении «Больше кода — больше багов» ваш ответ отношения никакого не имеет.

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

                                У меня ощущение, что у вас есть собственное понимание «кода», «системы», «объектов», «баг» и «взаимодействующих». Или вы троллите.

                                Давайте так: научный подход — это не «объясните чего ж такого магического», а подтверждение гипотеза экспериментом. А расчёты по результатам вашего рассмотрения в отношении кода не сойдутся с реальностью. Вот покажете эксперимент — будет повод для разговора.


          1. 0xd34df00d
            19.03.2018 20:16

            Другие языки вводят специальные «правила разруливания», позволяющие использовать одного родителя, а не таскать за собой топу зачастую идентичных состояний.

            Так и в плюсах есть virtual-наследование. Другое дело, что про него должны знать все потенциальные участники потенциального ромба, это недостаток, да.


        1. DistortNeo
          19.03.2018 15:51

          На самом деле, это implementation specific.

          Есть такая штука — Borland C++ Builder, так вот он абстрактные классы, не имеющие полей, реализует именно как интерфейсы, без заведения дополнительного поля vtable. И при этом никаких __declspec указывать не надо — он сам определяет по семантике, является ли сущность полноценным классом или легковесным интерфейсом.

          А вообще, такое поведение связано с бинарной совместимостью с Delphi, как раз таки имеющего интерфейсы, и желанием Borland остаться в рамках стандарта C++.


          1. saltukkos
            19.03.2018 15:59

            так вот он абстрактные классы, не имеющие полей, реализует именно как интерфейсы
            Отлично! Это то, что и нужно, чтобы называться интерфейсом. А теперь давайте в идельном мире закрепим это явно на уровне языка, чтобы это происходило всегда по спецификации, а когда не происходило — не компилировалось. И вот мы получаем ключевое слово interface.


            1. DistortNeo
              19.03.2018 16:33

              Ну вот так и сделали: создали ключевое слово «interface», а потом решили, что множественное наследование не нужно и выпилили его.


              1. Athari
                19.03.2018 17:49

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


      1. Flammar
        19.03.2018 19:59

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


  1. iit
    19.03.2018 07:53

    Я php-шник и могу делать еще большее зло выводя общий код в трейты, описывать под них интерфейсы и вообще отказываться от базовых классов, тупо подмешивая функционал в нужные объекты.


    php код не для слабонервных
    interface ObservableInterface {
        public function addObserve(ObserverInterface $observer);
        public function notify();
    }
    
    trait Observable {
    
    protected $observers = [];
    
    public function addObserve(ObserverInterface $observer)
        $this->observers[] = $observer;
    }
    
    public function notify(){
        array_map(function($observer){ $observer->update(); }, $this->observers)
    }
    
    }
    
    class MySomeWidget implements ObservableInterface, WidgetInterface {
        use Observable, WidgetUx, WidgetSupport, WidgetConfigs;
    }
    
    class MyParser implements ObservableInterface {
        use Observable;
    }
    
    class MyCollection extends Illuminatie\Support\Collection implements ObservableInterface{
        use Observable;
    }


    1. GraneDirval
      19.03.2018 09:58
      +1

      Мэтт Зандстра с вами бы не согласился. В его книге по объектам и паттернам как раз такой пример идёт как норма. Собственно, это действительно нормально, естественно при условии использования по назначению :)


      1. iit
        19.03.2018 11:29

        забыл оставить тег <irony>


    1. VolCh
      19.03.2018 10:22

      Лучше бы от наследования отказались в Collection, а не в Widget. :)


      1. iit
        19.03.2018 11:27

        Если бы я описывал свою коллекцию со всей этой фигней то да.


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


        Хотя в данном случае, если мне хочется чтобы любое изменение в коллекции генерировало событие update мне конечно придется переопределить некоторые методы.


  1. SergeyVin
    19.03.2018 08:42

    А теперь добавим в эту солянку default-методы в интерфейсах в Java 8. Вот уж костыль из костылей...


    1. Dair_Targ
      19.03.2018 13:41

      interfaces + defaults как-раз таки делают из всей этой «ООП»-каши что-то вразумительное:
      — появляется возможность множественного наследования с проверками отсутствия неочевидного кода во время компиляции (unrelated defaults)
      — кодогенераторы вроде immutables.github.io позволяют очень сильно очистить кодовую базу от всякого шлака вроде equals и toString, в результате чего остаётся практически чистая бизнес-логика
      — накнец-то можно замокать любой(!) оъект, что существенно облегчает написание автотестов

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


      1. Varim
        19.03.2018 13:51

        иметь возможность добавлять реализацию классом произвольного интерфейса без изменения самого класса
        Есть ключевое отличие от partial class?


        1. lair
          19.03.2018 13:59

          А что, можно сделать partial class для неконтролируемого тобой класса (в терминах .net — для класса в другой сборке)?


  1. lany
    19.03.2018 09:34

    Самое интересное, что в MyObservableWidget вы должны будете переопределить все методы MyWidget, изменяющие состояние, чтобы вызвать в них notify(). При этом вы попадаете на жёсткую зависимость от реализации MyWidget. При добавлении нового метода в MyWidget, который изменяет состояние, вам придётся изменять MyObservableWidget, иначе ничего вас не спасёт от багов. А если метод вроде addAll вместо ручной реализации в новой версии начнёт в цикле вызывать add, вы пришлёте миллион эвентов вместо одного (либо обратное произойдёт, тогда вы в новой версии вообще не пришлёте эвент). В этой ситуации невозможность множественного наследования и необходимость вручную делегировать пару методов к какому-нибудь подобию javax.swing.event.EventListenerList — это наименьшая из ваших проблем. Я считаю, если вы не используете аспекты или что-то аналогичное (а я совсем не призываю их использовать), то вы не сможете сделать изменяемую структуру данных отдельно от нотификаций, а потом прикрутить нотификации в дочернем классе. Вообще наследование конкретных классов плохо пахнет. Если вам при этом приходится переопределить реализацию N методов (например, "все методы, которые изменяют состояние"), вы точно ищете себе проблемы. На это напоролся, например, EclipseLink, который расширил ArrayList, переопределив все методы, а в Java 8 — сюрприз — появились новые методы.


    1. areht
      19.03.2018 10:04

      > При добавлении нового метода в MyWidget, который изменяет состояние, вам придётся изменять MyObservableWidget, иначе ничего вас не спасёт от багов.

      При добавлении нового метода в MyWidget, который изменяет состояние, вам придётся не забыть добавить куда-то нотификацию, да.

      Если у вас нет привычки смотреть на происходящее в наследниках при правке базового класса — вас ничего не спасёт от багов (к сожалению).


      1. VolCh
        19.03.2018 10:24
        +1

        Доступа к наследникам может вообще не быть.


      1. lany
        19.03.2018 10:52

        Не все клиенты вашего класса могут быть вам доступны. А если доступны все и всегда (у вас не библиотека, а приложение, которое не подразумевает сторонних плагинов), то необходимость выделения виджета без нотификаций вызывает ещё больше вопросов. Ключевой принцип ООП — инкапсуляция. В том числе она означает, что пока существующий класс сохраняет свой контракт, приложение не должно ломаться. Добавление нового метода или изменение реализации addAll через add не изменяет существующий контракт, но ломает приложение.


        1. areht
          19.03.2018 13:38

          > Добавление нового метода или изменение реализации addAll через add не изменяет существующий контракт, но ломает приложение.

          Я против такого определения «контракта». Если у вас приложение сломалось — кто-то нарушил контракт. Или это не контракт, а undefined behaviour.


          1. lany
            19.03.2018 13:41
            -1

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


            1. mayorovp
              19.03.2018 14:02

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


              1. lany
                19.03.2018 14:12

                Если в контракте написано, что "метод add добавляет элемент, а метод addAll добавляет все элементы", то базовый класс волен поменять реализацию и использовать, либо не использовать add внутри addAll, этим он контракт не нарушает.


                1. mayorovp
                  19.03.2018 14:18

                  Этим он не нарушает публичный контракт. А вот контракт для расширения он именно что нарушает…


            1. areht
              19.03.2018 15:42

              Ну нет, наследник — новый класс, он имеет право на свою логику, тем более, что это не нарушает даже LSP.

              Вот полагаться, что addAll в родителе дергает/не дергает add, если это отдельно не прописано — не стоит.


    1. iit
      19.03.2018 11:32

      Не знаю как в java но в php можно тупо сделать wrapper который будет прозрачно проксировать вызов любого метода да и еще и notify вызывать при этом.


  1. Scf
    19.03.2018 09:48

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


    Дальше не читал. Основное отличие интерфейса — поддержка "множественного наследования".


    1. Scf
      19.03.2018 22:19

      Ого. Это за "дальше не читал" или за основное отличие интерфейса? Если второе, то хотелось бы критики :-)


      1. ookami_kb
        20.03.2018 16:55
        +2

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


        1. Scf
          21.03.2018 08:52
          -1

          Можно пример языков, где есть и множественное наследование, и интерфейсы? Я знаю только два языка с множественным наследованием — C++ и Scala. И там, и там интерфейсов нет.


          1. ookami_kb
            21.03.2018 12:20
            +3

            Эм… ну Вы не путайте причину и следствие. Это не интерфейс нужен для поддержки множественного наследования, это некоторые языки, которые запрещают множественное наследование классов, разрешают множественное наследование интерфейсов, потому что оно не имеет побочных эффектов. Соответственно, в таких языках как Java вводится ключевое слово interface, чтобы синтаксически различать эти сущности. В таких же языках как python в этом ключевом слове нет необходимости, поскольку разница чисто семантическая.


            В общем же случае интерфейсы – это вообще не про наследования. Это отдельная семантическая сущность, контракт, если хотите, которому должен следовать класс. Поэтому, на мой взгляд, в Java как раз правильно "наследование" интерфейсов идет через implements – класс реализует интерфейс, а не наследует его. А наследование классов – это вообще про is-a отношение. Так что использовать интерфейсы, чтобы обойти ограничение во множественном наследовании, это не очень правильно.


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


  1. dimka_sokol
    19.03.2018 09:58

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

    Если я не ошибаюсь, в C# 8.0 интерфейсы смогут определять реализацию по умолчанию, так что это заявление скоро будет неактуальным.


    1. Myxach
      19.03.2018 10:25

      И поэтому, и JAVA 8, и C# 8, ломают интерфейсы.
      Интерфейсы в понятие опп, никогда не имели и не будут иметь реализацию


      1. Athari
        19.03.2018 10:57

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


      1. lair
        19.03.2018 12:40
        +3

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

        Осталось найти это "понятие ООП" и выяснить, почему оно — правильное, а любое другое, в котором интерфейсов нет вообще или они могут иметь реализацию, — неправильное.


        1. Myxach
          19.03.2018 15:22

          Интерфейсы это спецификация только, я про это. Интерфейсов может не быть, ну если интерфейс изначально что-то реализовывает, то это уже не интерфейс, как-бы вы его не называли. В C++ же, изначального ключевого слово интерфейс не было, ну понятие было и обозначало оно именно это. А Дефолтные методы это костыль — был, есть и будет


          1. lair
            19.03.2018 15:34

            Интерфейсы это спецификация только, я про это.

            А с чего вы решили, что это определение — правильное?


      1. PsyHaSTe
        20.03.2018 00:03
        +1

        Чем связка

        interface IFoo 
        {
        }
        
        public static class IFooExtensions
        {
           public static void Foo(this IFoo @this) {} 
        }

        лучше, чем просто написать
        interface IFoo 
        {
           public default void Foo() {} 
        }

        ?

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


        1. Athari
          20.03.2018 03:26

          Второй способ лучше, потому что default interface implementation полноценно участвует в полиморфизме. Первый способ — синтаксический сахар над неполиморфным вызовом статического метода.


          1. PsyHaSTe
            20.03.2018 11:45

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


        1. areht
          20.03.2018 12:39

          > на самом деле они точно так же связаны.

          Ну а вы, например, вынесите IFooExtensions в отдельную сборку — развяжутся?


  1. AntonLozovskiy
    19.03.2018 09:58

    Проблема не в том, что не смогли реализовать в C# наследование от множества классов… Язык пытались сделать таким, чтобы в нем было меньше внутренних проблем, которые потом сложно выявить. как и по умолчанию убрали поддержку работы с указателями в том виде, как она есть в C++. С одной стороны это хорошо, с другой плохо, но сидеть ныть о том, что это хреновый вариант — глупо… не нравится пиши на другом языке — благо выбор сейчас богатый


  1. gnkoshelev
    19.03.2018 10:12
    +2

    Класс подразумевает наличие состояния (state).
    Интерфейс — нет.

    Да, в Java 8 добавили default-методы и статические методы, а в Java 9 — приватные default-методы и приватные статические методы, но с состоянием это не имеет ничего общего.

    Если не ошибаюсь, в C# вошло в моду называть интерфейсы с префиксом I, т.к. этот костыль помогает глядя в код понять, что после : находится класс или интерфейс. А использование implements / extends делает код лучше для восприятия (субъективно).


    1. PsyHaSTe
      20.03.2018 00:07

      Если не ошибаюсь, в C# вошло в моду называть интерфейсы с префиксом I

      Этому правилу уже лет 15, начиная с версий 1.Х и он описан в древнейших официальных гайдлайнах. Не очень похоже на «моду».

      т.к. этот костыль помогает глядя в код понять, что после: находится класс или интерфейс. А использование implements / extends делает код лучше для восприятия (субъективно).

      Что делать с ипользованиями вне наследования? Например, когда это поля/параметры/…

      Это не больший «костыль», чем писать название переменных с маленькой буквы, а имена классов — с большой.


      1. DistortNeo
        20.03.2018 00:24

        Это не больший «костыль», чем писать название переменных с маленькой буквы, а имена классов — с большой.

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


        То же самое и с интерфейсами, когда имя интерфейса и класса совпадает: List и IList, Dictionary и IDictionary и т.д.


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


        1. TheShock
          20.03.2018 00:33

          Мне, кстати, ужасно не нравится традиция C# называть свойства с большой буквы. Читал документацию и все время думал, что они обращаются к статичному методу класса. Думал еще, что такую серьезную библиотеку так странно сделали. Оказалось, я ошибался:

          Container.Bind<IFoo>().To<IBar>().FromResolve();


          1. PsyHaSTe
            20.03.2018 01:19

            Вопрос привычки. Вполне логичное правило, чтобы отличать свойства класса от локальных переменных. Тем более, что от этого ничего не страдает. В угловых скобках может быть только тип (соответственно IFoo и IBar это типы), а вне их — типом только если есть намекающий `new`. В остальных случаях это всегда свойство. Таким образом мы минимизируем количество коллизий, когда нам нужна доп. информация чтобы понять, что это перед нами.


            1. TheShock
              20.03.2018 02:05

              Какой привычки? Я уже два года на шарпах пишу, сколько еще ждать, пока привыкну?
              Всмысле типом может быть намекающий new? Это вполне может быть статический метод класса
              Зачем отличать от локальных переменных? Методы обычно очень короткие — всегда видно локальные переменные и так. Тем более свойства, которые не get-set пишутся с маленькой буквы, хотя разницы не должно быть никакой. Бред какой-то. Это такая мелочь, но, пожалуй, бесит меня больше всего в C#


              1. PsyHaSTe
                20.03.2018 02:18

                Какой привычки? Я уже два года на шарпах пишу, сколько еще ждать, пока привыкну?
                Всмысле типом может быть намекающий new? Это вполне может быть статический метод класса

                В угловых скобках? Не может.

                Самое простое — покажите код, где может быть как тип, так и не тип, и из-за стиля именования непонятно, что где.

                Зачем отличать от локальных переменных? Методы обычно очень короткие — всегда видно локальные переменные и так. Тем более свойства, которые не get-set пишутся с маленькой буквы, хотя разницы не должно быть никакой. Бред какой-то. Это такая мелочь, но, пожалуй, бесит меня больше всего в C#

                Свойства всегда пишутся с большой буквы, не знаю, откуда эта информация про «не гет сет». Решарпер на дефолтных настройках как раз следует стандартному гайдлайну. Ну, разве что я предпочитаю еще у приватнах переменных префикс "_" писать. Но тут уже вопрос привычки.

                Методы обычно очень короткие — всегда видно локальные переменные и так.

                Когда короткие, а когда нет. Я не замерял, но в сложных продуктах средняя длина метода 50-100 строк. Типичный такой пример.

                Ну и да, тут нет варианта писать или не писать — как-то писать надо. Текущие правила, как я уже сказал, уменьшают количество коллизий.


                1. TheShock
                  20.03.2018 03:01

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

                  private int[] buckets;
                  private Entry[] entries;
                  private int count;
                  private int version;
                  private int freeList;
                  private int freeCount;
                  private IEqualityComparer<TKey> comparer;
                  private KeyCollection keys;
                  private ValueCollection values;


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


                  Это может быть как и статический метод Bind класса Container, так и динамический метод свойства. И невозможно определить, что именно это.

                  Вот Hello World пример:
                  using Zenject;
                  using UnityEngine;
                  using System.Collections;
                  
                  public class TestInstaller : MonoInstaller
                  {
                      public override void InstallBindings()
                      {
                          Container.Bind<string>().FromInstance("Hello World!");
                          Container.Bind<Greeter>().AsSingle().NonLazy();
                      }
                  }
                  
                  public class Greeter
                  {
                      public Greeter(string message)
                      {
                          Debug.Log(message);
                      }
                  }


                  Я искренне считал первое время, что этот пример означает следующее:
                  Zenject.Container.Bind<string>().FromInstance("Hello World!");
                  Zenject.Container.Bind<Greeter>().AsSingle().NonLazy();


                  И суть в том, что нельзя узнать, как именно оно есть на самом деле. А вот если бы все свойства были с маленькой буквы — было бы очевидно, потому что. Ну вот вам пример. Это корректный код на C#. Я в нем обращаюсь как к классу, так и к свойству. Так вот — какой вариант правильный:
                  1. ProjectContext — это класс, а LazyInstanceInjector — это свойство, или
                  2. LazyInstanceInjector — класс, а ProjectContext — свойство?

                  Вы можете ответить на этот вопрос без дополнительного контекста?

                  using Zenject;
                  
                  public class TestInstaller : MyClass
                  {
                     public IEnumerable<object> MyMethod ()
                     {
                        return ProjectContext.HasInstance
                           ? LazyInstanceInjector.Instances
                           : null;
                     }
                  }


                  Название свойств с маленькой буквы полностью убрало бы этот гемор

                  Я не замерял, но в сложных продуктах средняя длина метода 50-100 строк

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

                  Текущие правила, как я уже сказал, уменьшают количество коллизий.

                  С нуля вверх? Это невозможно. И если ваши слова бы имели логику, то как вы избегаете коллизий локальных переменных и приватных свойств?


                  1. Athari
                    20.03.2018 03:38

                    Это может быть как и статический метод Bind класса Container, так и динамический метод свойства.

                    Более того, "Container" может быть и свойством, и классом одновременно, и что вызывается — зависит и от инстансных членов класса свойства, и от статических членов класса. Это не баг, это фича.


                    Если хотите знать, что используете, то поставьте решарпер и включите расширенную подсветку.


                    Ну или на VB немного код попишите. Там вообще всё с большой буквы. Потом будете с радостью вспоминать C#, где хоть какое-то разнообразие.


                    И если ваши слова бы имели логику, то как вы избегаете коллизий локальных переменных и приватных свойств?

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


                    1. TheShock
                      20.03.2018 03:48

                      Это не баг, это фича.

                      В чем фича?

                      Решарпер у меня стоит. С IDE, конечно, значительно меньше проблем, но это не значит, что я не имею права считать такую идею плохой — она реально мешает пониманию кода, в чем я лично убедился совершенно недавно. Значительно лучше, если бы все публичные/защищенные поля и свойства были с маленькой буквы (какого они вообще разделяются, это нарушение инкапсуляции). Но я понимаю, что так, к сожалению, уже не будет. А жаль.


                      1. Athari
                        20.03.2018 03:59

                        Если бы были CClass, PProperty, MMethod, FField в дополнение к IInterface, было бы лучше? Не думаю. Если что-то непонятно, есть IDE. C R# вопрос про разновидность идентификатора не возникает никогда, потому что всё видно по цвету. Проблемы нет.


                        1. TheShock
                          20.03.2018 04:23

                          было бы лучше?

                          Я же написал как было бы лучше. Зачем вы мне отвечаете, если не читаете мои сообщения? Вот что я писал:
                          Значительно лучше, если бы все публичные/защищенные поля и свойства были с маленькой буквы
                          Дальше
                          вопрос про разновидность идентификатора не возникает никогда

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


                          1. Athari
                            20.03.2018 11:51

                            Еще раз — у меня вопрос возник, когда я читал документацию с примерами.

                            Если у вас возник такой вопрос, то это проблема документации.


                  1. mayorovp
                    20.03.2018 06:16

                    В вашем же примере куча свойств с маленькой буквы:

                    Это поля, а не свойства!


                    1. TheShock
                      20.03.2018 06:19

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


                      1. lair
                        20.03.2018 09:31

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


                      1. iPilot
                        20.03.2018 18:32
                        +1

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


                        1. TheShock
                          20.03.2018 18:36

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


                          1. areht
                            20.03.2018 19:32
                            +1

                            > тебя не должно интересовать — поле это или свойство, инкапсуляция ведь

                            Если в C# синтаксис работы с полями и свойствами похож — это ещё не инкапсуляция.

                            Ну и на практике это тоже не работает. Начиная с того, что с полями и свойствами через Reflection надо работать отдельно (а это куча тулинга).


                          1. PsyHaSTe
                            20.03.2018 20:33

                            Все это прекрасно, я даже в принципе хотел бы, чтобы ПОЛЕЙ не было в языке вообще, но этого не будет.

                            Насчет «не должно быть разницы» — поля всегда приватные, а поля как правило публичные. Так что видеть разницу в видимости в названии переменной часто полезно. Ну и всякие мелочи вроде использования с `ref` и все такое прилагается.


                          1. Athari
                            21.03.2018 02:42

                            Когда ты обращаешься к array.length — тебя не должно интересовать — поле это или свойство

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


                            А в результате разработчик библиотеки должен иметь легкую возможность подменить поле свойством.

                            Не должен. Ломается бинарная и даже сорцовая совместимость.


                        1. PsyHaSTe
                          20.03.2018 20:31

                          При чем тут легаси? У класса может быть приватное поле, которое не должно быть видно извне. Обычный сценарий.


                  1. PsyHaSTe
                    20.03.2018 12:10
                    +1

                    Выше уже ответили, что это поля, а не свойства. Разница существенная, но для разрешения я как раз и написал про "_".

                    Что касается вашего примера, то тут есть два способа разрешить его:

                    1. Подсветка в IDE подсвечивает тип аквамариновым, а поле оставляет белым. Решарпер тут не нужен, это дефолтная подсветка во всех связанных тулзах, даже менее мощных, чем студия
                    image
                    2. Если ВДРУГ подсветки нет, то можно использовать следующую логику: методы очевидно являются мутабельными, а изменять глобальную статическую переменную из инстансного метода это… запашок. Так что это свойство с таким именем, скорее всего protected в базовом классе.

                    Да, и благодаря префиксу I я сразу вижу тут в этом коде ошибку, потому что биндить интерфейс можно только к классу, но никак не к другому интерфейсу.

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

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

                    С нуля вверх? Это невозможно. И если ваши слова бы имели логику, то как вы избегаете коллизий локальных переменных и приватных свойств?

                    Как это с нуля? Окей, пишем с маленькой, как теперь без контекста понять, локальная это переменная или свойство? «Ну метод маленький, там все видно» — не аргумент, ибо он не отменяет коллизии, а переводит стрелки в стиле «самдурак» на программиста.


                    1. TheShock
                      20.03.2018 18:19

                      Выше уже ответили, что это поля, а не свойства. Разница существенная, но для разрешения я как раз и написал про "_".

                      Вы их переменными назвали, не полями:
                      приватнах переменных префикс "_" писать


                      Да, и благодаря префиксу I я сразу вижу тут в этом коде ошибку, потому что биндить интерфейс можно только к классу, но никак не к другому интерфейсу.

                      Нету там ошибки

                      В продакшн коде, где не специлисты уровня майкрософт код пишут средний размер кода поболее будет.

                      Сомнительно. Я специалист далекий до МС, но методы у меня корокие.

                      «Ну метод маленький, там все видно» — не аргумент,

                      Почему не аргумент? Метод значительно меньше, чем все файлы, а значит значительно уменьшает влияние коллизий.


                      1. PsyHaSTe
                        20.03.2018 20:27

                        Сомнительно. Я специалист далекий до МС, но методы у меня корокие.

                        Значит, у вас достаточно простая область вроде написания WebAPI прокси для другого сервиса. При сложной логике, например при работе с компилятором, получается что-то в таком духе. Разбить его на кучу мелких методов можно, но читаемости это не добавит.

                        Нету там ошибки

                        Я про этот код:
                        Container.Bind<IFoo>().To<IBar>().FromResolve();


                        Судя по официальной доке, эта запись означает, что вместо интерфейса IFoo прокидывать инстанс IBar, инстанс IBar создать нельзя, значит ошибка.


                        1. TheShock
                          20.03.2018 21:41

                          Значит, у вас достаточно простая область вроде написания WebAPI прокси для другого сервиса

                          GameDev.

                          При сложной логике, например при работе с компилятором, получается что-то в таком духе

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

                          Я про этот код:

                          Да, я понял. Там нету ошибки.


                          1. PsyHaSTe
                            20.03.2018 21:59

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

                            Какие крайности? Это единственный класс с логикой, все остальное — просто ДТО, которые и считать-то не обязательно.
                            Да, я понял. Там нету ошибки.

                            Окей, а в этом коде — есть:
                            builder.RegisterType<IFoo>.As<IBar>();


                            1. TheShock
                              20.03.2018 22:08

                              Окей, а в этом коде — есть:

                              Не знаю. А что это за код?


                        1. Athari
                          21.03.2018 02:47
                          +1

                          При сложной логике, например при работе с компилятором, получается что-то в таком духе.

                          Я не вижу сложной логики, я вижу сложный API (AddModifiers(Token(SyntaxKind.PublicKeyword)) вместо public). Реальных ветвлений в коде метода мало.


                          1. PsyHaSTe
                            21.03.2018 11:18

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


                            Про ветвления никто не говорил ничего.


            1. Flammar
              20.03.2018 14:07

              Вполне логичное правило, чтобы отличать свойства класса от локальных переменных.
              Спасибо, интересное объяснение. Сегодня, правда, для этого есть IDE с подсветкой синтаксиса и с переходом к декларации по ссылке. Как и для отличения класса от интерфейса.


        1. PsyHaSTe
          20.03.2018 01:17

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

          Не понял этого утверждения. Намешано про типы, префиксы, регистры…

          То же самое и с интерфейсами, когда имя интерфейса и класса совпадает: List и IList, Dictionary и IDictionary и т.д.

          IList реализует не только List, и не только в стандартной библиотеке.


      1. ApeCoder
        20.03.2018 13:34

        Имхо это костыль:


        • Классы, префиксы энамы и прочее — это типы и находятся в одном адресном пространстве. Их не надо разделять.
        • Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс. Т.е. убрав эту I можно было бы уменьшить compile time dependency
        • Если у нас класс называется так же как интерфейс, это значит, что мы что-то не выразили в имени (IList это список вообще, а List — это конкретная реализация, которая уже не список вообще, но называется как список вообще. До дженериков ее назвали ArrayList — что, имхо, более явно отличает абстрактный список от конкретной реализации)


        1. PsyHaSTe
          20.03.2018 13:45
          +2

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

          Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс. Т.е. убрав эту I можно было бы уменьшить compile time dependency

          Не могли бы, потому что тесты

          Если у нас класс называется так же как интерфейс, это значит, что мы что-то не выразили в имени (IList это список вообще, а List — это конкретная реализация, которая уже не список вообще, но называется как список вообще. До дженериков ее назвали ArrayList — что, имхо, более явно отличает абстрактный список от конкретной реализации)

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


          1. ApeCoder
            20.03.2018 14:19

            Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс. Т.е. убрав эту I можно было бы уменьшить compile time dependency

            Тесты это другой код.


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

            Не явлюятся ли моки другой реализацией?


            1. Flammar
              20.03.2018 14:36
              +3

              Не являются ли моки другой реализацией?
              Да, являются — единственной другой реализацией, создаваемой на лету и не имеющей нормального имени класса.


              1. ApeCoder
                21.03.2018 10:00

                Итого


                • IFoo это на самом деле "какая-то фигня для тестирования" а не "Foo вообще"
                • Foo это "Foo вообще" (но пользователь использует IFoo в качестве "Foo вообще")
                • Тест подсовывает неименованную частичную реализацию IFoo

                Лично я предпочитаю так не делать. По поводу моков была продуктивная дискуссия c lair


        1. Athari
          20.03.2018 14:29
          +2

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

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


          И в случае List<T> сильный упор был сделан на производительность, а не на красоту абстракций. В этом классе нет виртуальных методов, а вызов через интерфейс полиморфен всегда, причём в ранних версиях дотнета заметно медленнее обычного полиморфного вызова, если не изменяет память.


          1. ApeCoder
            20.03.2018 14:37

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


          1. DistortNeo
            20.03.2018 17:51

            а вызов через интерфейс полиморфен всегда, причём в ранних версиях дотнета заметно медленнее обычного полиморфного вызова, если не изменяет память.

            Сейчас однаково.
            Разница есть только при навешивании ограничений на generic-параметры: Foo<T>(T param) where T: ISomething будет чуть (но только совсем чуть!) медленнее, чем Foo(ISomething param).


            1. TheShock
              20.03.2018 18:20

              А разве это все не в Компайл-Тайм делается?


              1. PsyHaSTe
                20.03.2018 20:34
                +1

                Для структур делается в компайл тайм. На дотнексте целый доклад был про то, как превращать if (typeof(T) == typeof(SomeType)) в if (true).


                А для всех классов вообще используется одна-единственная реализация Foo__Cannon


                1. DistortNeo
                  20.03.2018 22:02

                  Для структур делается в компайл тайм

                  Да, но это происходит на этапе выполнения программы при JIT-компиляции, а не при создании сборки.


              1. DistortNeo
                20.03.2018 22:01

                Нет. В случае дженерик-функции тип `T` просто становится её параметром. Аналогично работают дженерики в Java.

                Кстати, такое поведение позволяет делать виртуальные дженерик методы, что невозможно в случае шаблонных методов C++.

                Примечание: когда T — структура, а не класс возможна генерация отдельного кода для каждого из типов — это приводит к code bloating, но позволяет избавиться от боксинга.


  1. Athari
    19.03.2018 10:20
    +1

    Если бы проблема была только в interface a.k.a. protocol… Чтобы не прикасаться к проклятому ромбу, дизигнеры языков плодят сущности, придумывая всякие trait a.k.a mixin, кроме того сами интерфейсы мутируют в недо-абстрактные классы добавлением default interface implementation, а поверх всего этого добра сбоку прикрепляются extension methods a.k.a. helper classes a.k.a protocol extensions.


    Остапа понесло. Он почувствовал прилив новых сил и шахматных ООП идей.


    При этом сама статья — тоже тот ещё бред.


    1. Никаких "специальных случаев" быть не может. Ты или полноценно реализуешь множественное наследование, как в C++, или не делаешь этого вообще. Иначе возникает момент, когда нельзя наследоваться из-за того, что где-то в предках кто-то решил унаследоваться от чего-то другого.


    2. Кроме высосанной из пальца причины про "невозможность" ромба и "лени" у авторов языков были объективные причины не реализовывать множественное наследование: авторы джавы упарывались по "простоте", а множественное наследование — "сложно"; авторы шарпа делали "джаву по-нормальному", и не нашли достаточно оправданий отступить от разделения типов базовых классов, ну и плюс упирали на "изменение базы должно ломать как можно меньше".


    3. Изменение interface на class — это ломающее изменение что в Java, что в C#. Более того, любое изменение интерфейса — ломающее изменение. Это ужасно. Костыль с default interface implementation — попытка замести эту проблему под ковёр и сделать из интерфейсов более гибкие и послушные абстрактные классы.


    1. Flammar
      20.03.2018 14:14

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


      1. Athari
        20.03.2018 14:32

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


  1. KIVagant
    19.03.2018 10:36

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


    1. iit
      19.03.2018 12:01

      На самом деле я тоже так думал, пока не додумался об одной маленькой проблеме — трейт не самостоятельный объект и если к нему привязывать интерфейс тогда у нас 3 варианта.


      1) Интерфейс не должен влиять на исходный объект куда примешивается никак — тогда собственно зачем он нужен
      2) Необходимо выдавать ошибку если класс не реализует интерфейс трейта, что усложнит код
      3) Необходимо делать магию что у объекта как бы есть интерфейс но он не описан явно, что будет выдавать еще больше юмора, особенно если методы интерфейса переопределяются родительским объектом или другим интерфейсом.


  1. maxzh83
    19.03.2018 11:30
    +2

    — Правда? А что такое интерфейс? Это то же самое что и класс?
    — Ну… Не совсем!
    — В каком плане?
    — Ни один из его методов не должен иметь реализации.

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


  1. mayorovp
    19.03.2018 11:38

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

    Нет, нет, и еще раз нет! Эта проблема так и не была толком решена в C++. На нее просто закрыли глаза, порекомендовав программистам так не делать. (Это вообще популярный способ борьбы с языковыми проблемами в С++)


    Проблема тут не только в переменных полях, она в методах.


    Рассмотрим простейший "бриллиант":


    class B { 
        void foo() { /* ... */ } 
    }
    class D1 extends B {
        void foo() { super.foo(); } 
    }   
    class D2 extends B {
        void foo() { super.foo(); } 
    }   
    class M extends D1, D2 {
        void foo() { ??? } 
    }

    Как теперь в классе M переопределить метод foo так, чтобы базовая реализация была вызвана не два раза, а один?


    В языке C++ это можно сделать только копированием логики из D2 в M (или из D1 в M).


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


    1. Athari
      19.03.2018 11:55

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


      Если происходит что-то подобное, и надо разруливать хитросплетения из B.foo, D1.foo, D2.foo и вызывать это всё в нужной последовательности, и в этом есть какой-то смысл, то, вероятно, авторам всего этого добра стоило выделить основную логику foo в D1 и D2 в отдельные методы doFoo.


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


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


    1. MaxLich
      19.03.2018 12:45

      А если в D1 и D2 будут разные реализации метода foo(), в классе M не будет переопределён этот метод, и у объекта типа M вызвать метод foo(), то какая реализация отработает?


      1. mayorovp
        19.03.2018 12:50
        +2

        Ошибка компиляции будет (в C++). Это еще одна проблема множественного наследования.


        1. Antervis
          19.03.2018 14:14

          Как теперь в классе M переопределить метод foo так, чтобы базовая реализация была вызвана не два раза, а один?

          в с++: M::foo() { D1::foo(); }


          1. mayorovp
            19.03.2018 14:20

            И теряем ту логику, которая была добавлена в D2::foo.


            1. Antervis
              19.03.2018 17:33

              и точно та же проблема будет при аналогичной композиции


              1. mayorovp
                19.03.2018 18:01

                Какая именно проблема является "точно той же"?


                Разумеется, когда "бриллиант" уже начал рисоваться (классы B, D1 и D2) — правильно замкнуть его никакая композиция уже не поможет. Композицию надо применять с самого начала, вместо наследования.


                1. Antervis
                  19.03.2018 20:13

                  Простой пример:
                  struct Point {
                      float x, y;
                  };
                  
                  class Ellipse
                  {
                      Point p1, p2;
                  public:
                      // set/get/whatever
                  };
                  
                  class Rectangle
                  {
                      Point p1, p2;
                  public:
                      // set/get/whatever
                  };
                  
                  // А теперь я хочу вписанный в прямоугольник овал
                  class EllipseInRectangle 
                  {
                      Ellipse _ellipse;
                      Rectangle _rect;
                      // Тут четыре Point, но необходимо и достаточно две точки
                  public:
                      // И что делать будем?    
                  };
                  


        1. Eagle_NN
          19.03.2018 14:34

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


          1. MaxLich
            20.03.2018 12:05

            Ну это хорошо, что в Си++ эта ситуация проверяется, и компилятор не даст пустить это в продакшен. В других языках с возможностью множественного наследования это может вызвать ошибки в работе самой программы.
            А если будет другая ситуация: в методе foo() класса M вызвать аналогичный метод предка (super.foo()), то какой метод вызовется?


        1. MaxLich
          19.03.2018 17:01

          Да, то-то и оно. При множественном наследовании не только проблема с переменными, но и с методами.


          1. mayorovp
            19.03.2018 18:02

            Кажется, именно так я сразу и сказал. О чем же вы в таком случае спорите?


            1. MaxLich
              20.03.2018 11:59

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


  1. bm13kk
    19.03.2018 13:09
    +2

    Отличная статья, с котороя я согласен на 100%… если бы заменили интерфейсы на абстрактные классы.


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


  1. lazard105
    19.03.2018 13:25

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

    Тот факт, что в вашем абстрактом классе нет публичных переменных и реализаций методов, не означает, что их туда никто не добавит потом.


    1. Dair_Targ
      19.03.2018 13:45

      Чем плоха реализация метода, которая зависит от других методов? Например:
      interface Repr {
      default String repr() {
      return getClass().getSimpleName() + "(" + value() + ")";
      }
      String value();
      }

      ?


      1. lazard105
        19.03.2018 14:06

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


      1. zagayevskiy
        19.03.2018 14:22

        А чем она хороша, зачем это надо?


        1. Flammar
          20.03.2018 15:11
          +1

          Тем, что, например, в Java можно было бы выкинуть классы типа AbstractCollection и AbstractList, в которых все «остальные» методы определены через один-два «основных» и перенести реализации в соответствующие интерфейсы.


          1. zagayevskiy
            20.03.2018 15:36

            Зачем?


            1. Flammar
              20.03.2018 15:55

              Вопрос был «Зачем выкидывать лишние классы?»?


              1. zagayevskiy
                20.03.2018 16:03

                Кто сказал, что они лишние?


                1. Dair_Targ
                  20.03.2018 23:13

                  А зачем оони нужны?


    1. Flammar
      20.03.2018 14:59

      Не реализацию метода, а переменные состояния aka поля. А так — да, именно чтоб никто никогда не мог.


  1. ganqqwerty
    19.03.2018 13:48

    Интерфейсы в TypeScript при этом вообще другие, они просто описывают примерную форму объекта.


  1. BogdanF
    19.03.2018 13:59

    В C#8 могут появиться дефолтные реализации интерфейсов… Может жизнь упростит. Или усложнит. Вообще, было бы здорово, если бы MS поступил с множественным наследованием как с unsafe — типа, используй на свой страх и риск, только поставь галочку. Вот только пользы от этого, скорей всего, будет гораздо меньше, чем вреда.


  1. AxisPod
    19.03.2018 14:03
    +1

    А смысл наследовать от 2х классов? Как это потом покрывать тестами? Это не C++ и PHP где можно подсунуть стаб.
    А вот агрегация на пару с DI, а далее и IoC решают все описанные проблемы.


    1. Flammar
      19.03.2018 21:01

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


  1. novikovag
    19.03.2018 14:21
    -4

    ООП худшая парадигма программирования.


    1. zagayevskiy
      19.03.2018 14:23

      Для кого?


      1. vg7
        19.03.2018 18:15

        Для быдлокодеров.


  1. sami777
    19.03.2018 14:33

    — Правда? А что такое интерфейс? Это то же самое что и класс?
    — Ну… Не совсем!
    — В каком плане?
    — Не один из его методов не должен иметь реализации.

    Самое печальное, что здесь если даже «не» убрать, все равно получается белиберда.


    1. vg7
      19.03.2018 18:18

      «Не» надо не убирать, а заменить на «ни» — «Ни один из его методов не должен иметь реализации» — так это пишется по-русски.
      А так, никакой белиберды я не вижу.


  1. alexs0ff
    19.03.2018 14:33

    касательно интерфейсов в c#. Не представляю как можно обходится без них при написании юнит тестов. Например есть два класса, один использует другой. Если я в объект передам явно конкретный тип, то при написании теста для одного, мне придется учитывать поведение другого, что добавляет всякой сложно логики. (виртуальные методы не предлагать)

    class A
    {
    public void MethodA(int val){
    return val+1;
    }
    }

    class B{
    A _a;
    B(A a){
    _a = a;
    }

    public int MethodB(int val){
    val +=3;
    return _a.MethodA(val);
    }

    }


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


    1. DistortNeo
      19.03.2018 16:17

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


      1. alexs0ff
        19.03.2018 17:20

        1) Потеря производительности от виртуальных методов где-то 5-10%
        2) Экономить везде на спичках тоже не очень хорошо, всегда открываю профайлер и смотрю где находятся участки, которые нужно «подмазать» и на моей памяти еще ни разу не приходилось править виртуальные методы.


      1. zagayevskiy
        19.03.2018 18:38

        Вот я ещё в С# и джаве не задумывался на эту тему. Если начинаются такие мысли, то надо подумать, а то ли ты делаешь.


  1. Xandrmoro
    19.03.2018 15:05

    Проблему в статье надо решать не множественным наследованием, а миксинами.
    Эх…


    1. Flammar
      19.03.2018 18:44
      -1

      Интерфейсы и есть нечто типа микс-инов.


      1. mayorovp
        19.03.2018 19:00

        Нет, они совершенно различны.


      1. Xandrmoro
        19.03.2018 21:00

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


  1. OYTIS
    19.03.2018 15:26

    TL;DR: хочу множественного наследования в Java, множественное наследование бывает полезно.

    Я вот считаю, что указатели и pointer arithmetic — это важные фичи, и не пишу на Java. Но Java сознательно спроектированна так, что «может быть полезные» фичи приносятся в жертву простоте и унификации.


    1. Free_ze
      19.03.2018 15:35

      Кто мешает игнорировать опасные фичи? Зачем сознательно сужать сферу применимости инструмента?


      1. OYTIS
        19.03.2018 16:31

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


        1. Free_ze
          19.03.2018 19:04
          +1

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


      1. vedenin1980
        19.03.2018 17:49

        Кто мешает игнорировать опасные фичи? Зачем сознательно сужать сферу применимости инструмента?

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

        1. В Java программе может использоваться сотни разных библиотек, как вы сможете гарантировать, что автор одной из них не добавит опасную фичу?
        2. Придется контролировать всех junior'ов и самоучек, чтобы они не использовали эти фичи. Зачем опытным разработчикам тратить на это время?
        3. В мире полоно дилетантов и самоучек, для которых даже циклы что-то сложное (реально видел код состоящий из сотен копипащенных строчек, потому что цикл это думать надо). Если у них будет опасный инструмент — в какой-то момент появится куча кривого и глючного софта. который будет ассоцироваться с языком,
        4. Если опасную фичу не стоит использовать никому — то зачем она нужна? Если бы можно было сказать — а теперь игнорируем опасные фичи и их не используем — то Java и C# оказались бы не нужны, хватило C++, где все игноруют опасые фичи,
        5. Каждая ненужная фича усложняет язык и порог вхождения сильно увеличивается. И получается, что изучить Java нужно полгода, изучить на том же уровне C++ — 2 года, и хуже многие программисты просто не осилят такую сложность, а значит дефицит кадров,


        1. Free_ze
          19.03.2018 18:02

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

          Придется контролировать всех junior'ов и самоучек, чтобы они не использовали эти фичи.
          То есть без этого никого контролировать не нужно? Код-ревью, статические анализаторы и прочие линтеры. Банальные документы-соглашения о процессе разработки.

          В мире полоно дилетантов и самоучек, для которых даже циклы что-то сложное
          Это будет проблемой инструмента лишь в случае, если опасная фича будет необходимой. В остальном же это будет «terra incognita» для избранных.

          Если опасную фичу не стоит использовать никому
          Если звезды зажигают, значит кому-то это нужно. Даже если это не вы.

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


          1. vedenin1980
            19.03.2018 18:22

            Я не должен заботиться об этом, ибо сфера ответственности разработчика библиотеки.

            Да ладно. Вы сдали программу заказчику, а она во время продашена начала падает и вызывает синий экран смерти ОС каждые 15 минут. Чья это проблема ваша или разработчика опенсорс библиотеки? Заказчика не будет волновать чья это сфера ответвенности, его будет волновать вопрос почему он несет миллионые убытки. А даже просто обнаружить какая библиотека использует опасные фичи — нетревиальная задача, тем более заменить ее во время продакшена.

            То есть без этого никого контролировать не нужно? Код-ревью, статические анализаторы и прочие линтеры. Банальные документы-соглашения о процессе разработки.

            Нужно, но чем меньше у начинающих выстрелить в ногу — тем лучше.

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

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

            Если звезды зажигают, значит кому-то это нужно. Даже если это не вы.

            А их не зажигают, на C# и Java большинстом разрабатывающих язык от них отказались. Для тех кому нужно оставили возможность писать небезопасный код на C++ или небезопасные блоки на C#.

            Это будет проблемой инструмента лишь в случае, если опасная фича будет необходимой. В остальном же это будет «terra incognita» для избранных.

            Тогда ответьте зачем появилась Java и C# вместо С++? Можно было бы просто сказать, а теперь мы не используем опасные фичи. Любая фича влечет в себя проблемы поддержки и обратной совместимости, необходимости ее знать всем пользователям. Зачем нужно было придумывать Java смысл которой убрать из С++ самые спорные и опасные фичи (вроде указателей), если можно было просто не использовать?


            1. Free_ze
              19.03.2018 18:46
              -1

              Чья это проблема ваша или разработчика опенсорс библиотеки?
              Какое отношение низкое качество библиотеки имеет к фичам в языке?) Самый очевидный ответ — архитектора, который решил, что использование конкретной библиотеки будет уместно, ибо есть куча софта, код которого вам недоступен или защищен он фиксиков лицензионным соглашением.

              чем меньше у начинающих выстрелить в ногу — тем лучше.
              Хорошее — хорошо, плохое — плохо. Только если эта простота вызвана меньшей гибкостью системы, то это будет больше кода, багов и… сложность поддержки)

              Нет, вы не можете считаться опытным разработчиком, если не знаете все фичи языка и возможные проблемы.
              «Порог вхождения» — это про джуниоров. Опытные его преодолевают.

              на C# и Java большинством разрабатывающих язык от них отказались
              Только те и другие сейчас костылями себе миксины изобретают, а Java-сообщество воет, что unsafe у них забрать хотят. Это так, навскидку.

              Тогда ответьте зачем появилась Java и C# вместо С++?
              Затем, что не было стандартизации C++ и язык долгое время загибался. Сейчас он вполне позволяет регулировать уровень красноглазия и писать годный высокоуровневый код. Битву за десктопную кроссплатформенность Java проиграла старшему брату к настоящему моменту.


              1. Flammar
                19.03.2018 18:53
                +1

                Какое отношение низкое качество библиотеки имеет к фичам в языке?)
                К фичам — косвенное, а к степени опасности «виртуальной машины» — прямое. Одно дело, когда библиотека иногда, раз в полчаса, не работает и пишет сообщения со стек-трейсами в лог, другое — когда раз в полчаса вмест этого происходит BSOD.


                1. Free_ze
                  19.03.2018 18:55
                  -1

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


                  1. vedenin1980
                    19.03.2018 21:05

                    Free_ze, я понял вам хочется развести холивар на тему почему С++ уже торт и круче Java/C# и т.п. языков. Это скучно и не интересно.


                    1. Free_ze
                      20.03.2018 11:16
                      -1

                      Вы мне задали вопрос про языки, я на него ответил. Вам не нравятся мнения, отличные от вашего?


                1. Free_ze
                  19.03.2018 19:01
                  -1

                  Одно дело, когда библиотека иногда, раз в полчаса, не работает и пишет сообщения со стек-трейсами в лог, другое — когда раз в полчаса вмест этого происходит BSOD.
                  Прикладной софт при всём желании не сделает вам BSOD.


      1. Flammar
        19.03.2018 18:46
        +1

        А кто мешает нести лишнюю ответственность непонятно за кого и непонятно за что (конкретно — за используемую стороннюю библиотеку и её авторов)?


        1. Free_ze
          19.03.2018 18:53
          -1

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


        1. PashaNedved
          20.03.2018 08:46

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


  1. owlet255
    19.03.2018 15:41

    Контракт, контракт без реализации и т.п. — не очень хорошее определение интерфейса. Дело в том, что интерфейс не возникает исторически как контракт — так декларируется в учебниках, но рассуждать так — идеализм. Интерфейс определяется в процессе разработки. Если вы его определили как "контракт", то с высокой долей вероятности еще не раз перепишете его детали, когда погрузитесь в разработку. Так что, если и "контракт" — то "контракт, написанный задним числом".


    Вот неплохое определение, на мой взгляд: "Интерфейс — это общая граница между двумя функциональными объектами".


  1. SimSonic
    19.03.2018 18:42

    Но вообще вот эта идея выше в комментариях с определением метода через член очень красиво выглядит. Хочется что-то вроде такого:

    @Override public T[] values() => container::values;

    Плюс минус вариации…


    1. Flammar
      19.03.2018 18:47

      Интересно, в Lombok это реализовано?


    1. mayorovp
      19.03.2018 19:05

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

      Кстати, то что написали вы давно есть в C# и называется Expression Bodied Member.


      1. SimSonic
        19.03.2018 19:22
        +1

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


  1. Flammar
    19.03.2018 18:48
    +1

    — Правда? А что такое интерфейс? Это то же самое что и класс?
    — Ну… Не совсем!
    — В каком плане?
    — Ни один из его методов не должен иметь реализации.
    Определение в стиле «утиной» типизации. Более правильно было бы «класс, который не может иметь состояния», «класс, который не может содержать данные».


    1. ApeCoder
      20.03.2018 14:17

      А как в интерфейс добавить реализацию?


      1. Flammar
        20.03.2018 14:40
        +2

        В Java 8 добавили возможность создавать дефолтные реализации методов.


  1. Flammar
    19.03.2018 19:06
    +1

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

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

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


    1. VolCh
      19.03.2018 20:00
      -1

      PHP тоже динамически типизируемый, но интерфейсы в нём есть :) И появились они не результате стремления не к статической типизации, а к строгой.


  1. Flammar
    19.03.2018 20:10

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


  1. strangeraven
    20.03.2018 00:41

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

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

    К слову, в большинстве проектах на C++, на которых я работал, множественное наследование было тупо запрещено. Это к вопросу о том, насколько «успешно» решили эту задачу в C++


    1. Flammar
      20.03.2018 14:30

      К слову, дебажить и диагностировать metadata-driven системы, а именно ту самую метадату в них — тоже отдельное особое удовольствие…


    1. Athari
      20.03.2018 14:36

      Ну, знаете, C++ — не показатель. В нём UB можно получить от любого чиха, и дебажь потом. Что в каком-то случае оно вылезло из-за множественного наследования и неправильного приведения типа — случайность.


  1. ShadowTheAge
    20.03.2018 11:46

    Лично мне иногда не хватает чего-то типа mixin-ов — небольшого кусочка класса, с полями, методами, свойствами и указанием реализуемых интерфейсов, который можно «примешать» к любому классу (но только один раз)


  1. bystr1k
    20.03.2018 11:46

    В чем реальное отличие от интерфейса?

    Абстрактный класс может в себе хранить состояние, а интерфейс — нет


  1. weekens
    20.03.2018 13:29

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


    1. mayorovp
      20.03.2018 16:41
      +1

      Кажется, вы потеряли частицу «не».


  1. guai
    20.03.2018 14:09
    -2

    Какой хороший у мистера Мартина strawman, здоровенный, с*ка!
    C++ порешал множественное наследование? Но ведь это привело к другой проблеме, которая называется, ну, «C++» :)
    Переусложненный тормозной язык с кучами UB, к которому тулинг могут запилить полтора человека в мире, да и те со временем начинают пилить каждый свой язык :)
    Дизайн языков программирования — это нахождение баланса, чем то жертвуем, что-то хотим во что бы то ни стало.
    Да, в яве решили, что ну его нафиг то множественное наследование, это упростило компилятор, и, вероятно, помогло избежать тормозов.


    1. 0xd34df00d
      20.03.2018 18:40
      +1

      Тормозной, ясно. А в чём тормоза заключаются?


      1. guai
        20.03.2018 19:27

        в скорости сборки, в чем же еще?


        1. Antervis
          20.03.2018 20:32

          лучше 1 раз собрать, чем 100 раз интерпретировать


          1. PsyHaSTe
            20.03.2018 20:36
            +1

            Очень громкое заявление


        1. DistortNeo
          20.03.2018 22:09

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


          1. 0xd34df00d
            20.03.2018 22:47

            Ну окей, отключите -flto или что там у вас на вашей платформе. Случаи, когда использование, скажем, precompiled headers уменьшает время компиляции на порядок, в моей практике нередки.

            Ну ничего, завезут модули (особенно в виде Another take on modules) — будет хорошо.


            1. DistortNeo
              20.03.2018 23:59

              Ну окей, отключите -flto или что там у вас на вашей платформе

              А зачем? Я пишу на С++ исключительно ради производительности, поэтому LTO мне нужно. Там же, где производительность не критична, я пишу на других языках, а не отключаю LTO.


              Насколько я знаю, крупные проекты с LTO часто не собирают даже в релизе — слишком долго. Максимум — компилят так отдельные компоненты.


              1. 0xd34df00d
                21.03.2018 00:32

                А зачем?

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

                Ну и в ряде задач горячий код всё равно оказывается плюс-минус в одном TU, так что от этого LTO ни горячо, ни холодно.


            1. TargetSan
              21.03.2018 13:18

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


              1. Antervis
                21.03.2018 14:27

                n4720.pdf — от 29 января, статус — final draft.


                1. TargetSan
                  21.03.2018 15:04

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


    1. netch80
      21.03.2018 13:10
      +1

      Тормоза сборки C++ на ~99% заключаются в обработке шаблонов, а не в множественном наследовании (которое используется чуть чаще, чем никогда).


      1. guai
        21.03.2018 13:22

        Что только подтверждает мой тезис о поиске баланса в дизайне языков программирования. Хотим наркоманские шаблоны — жертвуем скоростью.
        Есть и другие апекты в дизайне языков программирования, напрямую влияющие на скорость компиляции. Наличие/отсутствие модулей, например.
        А еще аспект совместимости.
        Могла бы ява заиметь множественное наследование? Ну, наверное, могла бы, если поломать всю совместимость, над которой очень пекутся. И ради чего? Вы сами правильно заметили, что она редко когда нужна.


        1. netch80
          21.03.2018 13:36

          > Вы сами правильно заметили, что она редко когда нужна.

          Категорически прошу внимательно читать и не подтасовывать. Я не сказал, что она редко _нужна_, я сказал, что она редко _используется_. А используется она редко, в частности, из-за того, что мало где адекватно реализована, и из-за критиканства неосиляторов. Я её использовал и в Python, и в C++ (без общего базового класса), и как-то программы не рухнули и работают без проблем.

          Остальной ваш пафос пропускаю мимо за непрактичной банальностью.


      1. DistortNeo
        21.03.2018 14:12

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

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


        1. Antervis
          21.03.2018 14:23

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

          Во-первых, самый долгий этап сборки как правило в LTO, а не парсинг заголовочников. Тем более что он не параллелится. Во-вторых, многократный парсинг хедеров чаще связан с тем, что нерадивые кодеры инклюдят много лишнего в хедеры вместо forward declaration. В-третьих, даже в отсутствие модулей, призванных решить эту проблему, существуют PCH.


          1. DistortNeo
            21.03.2018 14:43
            +1

            Во-первых, самый долгий этап сборки как правило в LTO, а не парсинг заголовочников

            Даже с отключенным LTO, если объединить все c/cpp файлы в один, скорость компиляции вырастет в разы.


            Во-вторых, многократный парсинг хедеров чаще связан с тем, что нерадивые кодеры инклюдят много лишнего в хедеры вместо forward declaration.

            И что же делать, если нужен какой-нибудь монструозный Windows.h?
            Плюс сам стандарт C++ не одобряет forward declaration для библиотечных типов.


            В-третьих, даже в отсутствие модулей, призванных решить эту проблему, существуют PCH.

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


            1. Antervis
              21.03.2018 17:08

              Даже с отключенным LTO, если объединить все c/cpp файлы в один, скорость компиляции вырастет в разы.

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

              И что же делать, если нужен какой-нибудь монструозный Windows.h?

              Можно инклюдить хедеры в *.c/*.cpp файлах — тогда они используются только в тех TU, где они нужны. Иногда большие хедеры являются лишь коллекцией из include'ов хедеров поменьше, лишь некоторые из которых востребованы, классический пример — QtWidgets.

              Плюс сам стандарт C++ не одобряет forward declaration для библиотечных типов.

              за интерфейс библиотек должны отвечать их авторы. В собственном коде советую «всегда инклюдить аскетичный минимум»