Наследование реализаций: закопайте стюардессу

    Ключевое противоречие ООП


    Как известно, классическое ООП покоится на трех китах:


    1. Инкапсуляция
    2. Наследование
    3. Полиморфизм

    Классическая же реализация по умолчанию:


    1. Инкапсуляция — публичные и приватные члены класса
    2. Наследование — реализация функционала за счет расширения одного класса-предка, защищенные члены класса.
    3. Полиморфизм — виртуальные методы класса-предка.

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


    Наследование ломает инкапсуляцию


    1. Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома — антипаттерн Паблик Морозов;
    2. Реально изменить поведение предка можно только с помощью перекрытия виртуальных методов;
    3. Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку;
    4. Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода;
    5. Информация из пункта 4 зависит от реализации класса-предка, включая приватные члены и их код.

    В теории мы уже имеем былинный отказ, но как насчет практики?


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

    Все, кто используют фреймворки, требующие наследования от своих классов (WinForms, WPF, WebForms, ASP.NET), легко найдут подтверждения всем трем пунктам в своем опыте.
    Неужели все так плохо?


    Теоретическое решение


    Влияние проблемы можно ослабить принятием некоторых конвенций:


    1. Защищенные члены не нужны
    Это соглашение ликвидирует пабликов морозовых как класс.


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


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


    4. Экземпляры предка никогда не создаются
    Это соглашение позволяет избавиться от несоответствия требований к виртуальными методам (публичный контракт класса) с одной стороны и обязанностью ничего не делать (защищенный контракт класса) с другой. Теперь принцип подстановки Лисков можно соблюсти, не вступая в порочную связь с закрытым содержимым предка.


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


    Результат: если класс-предок состоит из публичных виртуальных пустых методов и требований к ним для потомков, то наследование уже не ломает инкапсуляцию. Что и требовалось доказать.


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


    Практические решения


    1. Виртуальные методы-пустышки уже есть во многих языках и носят гордое звание абстрактных.
    2. Классы, экземпляры которых создавать нельзя, тоже есть во многих языках и даже имеют то же звание.
    3. Полное соблюдение указанных соглашений в языке C++ использовалось как паттерн для проектирования и реализации Component Object Model.
    4. Ну и самое приятное: в C# и многих других языках соглашения реализованы как первоклассный элемент "интерфейс".
      Происхождение названия очевидно — в результате соблюдения соглашений от класса остается только его публичный интерфейс. И если множественное наследование от обычных классов — редкость, то от интерфейсов оно доступно без всяких ограничений.

    Итоги


    1. Языки, где нет наследования от классов, но есть — от интерфейсов (например, Go), нельзя лишать звания объектно-ориентированных. Более того, такая реализация ООП правильнее теоретически и безопаснее практически.
    2. Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.
    3. Избегайте наследования реализаций без крайней необходимости.
    4. Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
    5. Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.

    PS: Дополнения и критика традиционно приветствуются.

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

    Ну. И что?
    Реклама
    Комментарии 349
      +1
      Теперь понятно почему по умолчанию все классы в Kotlin sealed а методы final.
        0

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

          0
          В теории это хорошо, но писать sealed/final быстро надоедает, даже с автокомплитом. Та же ситуация что и с переменными и типами, поведение по умолчанию (mutable, nullable) слишком часто надо переопределять.
            +1

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

              +1
              Или пары шаблонов в студии.
                +1
                В данном случае в IDEA, хоте мне больше нравится мысль взять новый «хороший» ЯП и пользоваться всеми плюшками из коробки :) Конечно не всегда есть такая возможность.
                  0
                  Я с IDEA не работал, но разве она не поддерживает написание своих сниппетов?
          • НЛО прилетело и опубликовало эту надпись здесь
              +2

              Я же написал в явном виде — "нужна причина" :-) У вас причина есть, соответственно вы осознанно будете делать классы/методы/конструкторы паблик. Но не у всех есть такая причина.

              +4

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

                +3

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

                +6
                Меня всегда удивляло стремление отдельных людей выдумывать «правила», а потом героически их преодолевать!
                Инкапсуляция на 100% — это утопия, так не бывает в жизни. ООП — лишь средство более элегантно решить некоторые (но далеко не все) проблемы, но не нужно его идеализировать и обожествлять! Если предоставить «клиенту» посмотреть на «внутренности» — это не плохо, просто «клиент» (пользователь библиотеки), принимая решения, должен осознавать последствия сделанного им выбора. Я уверен, что разумный доступ к внутренностям объекта (и одновременным пониманием, чем это чревато) позволяет писать более эффективный код, и делать это быстрее, а не тратить время на героическое преодоление правил. Правила должны помогать осуществлять задумки, а не мешать им. А вот ответственность за сильное связывание, спагетти-код и т.д. лежит не на языке, и даже не на библиотеке, а на конкретном разработчике ее использующем.
                И если библиотека хорошо выполняет свою задачу, пусть она трижды вся public, я буду ее использовать, и скажу ее разработчику лишь спасибо!
                  +1
                  Меня всегда удивляло стремление отдельных людей выдумывать «правила», а потом героически их преодолевать!

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


                  И если библиотека хорошо выполняет свою задачу, пусть она трижды вся public, я буду ее использовать, и скажу ее разработчику лишь спасибо!

                  Ну не всё так просто:


                  • У разработчика при таком подходе будут связаны руки: т.к. всё public и всё часть контракта библиотеки, нельзя будет просто так пойти и отрефакторить что-то. Каждый новый класс — это minor релиз, каждое переименование любого класса — это major релиз. Patch релизы будут предполагать только изменения кода реализации существующих классов и методов.
                  • Вы в итоге разработчику спасибо не скажете, т.к. на ранних стадиях развития библиотеки каждый второй релиз скорее всего будет major — с изменённым контрактом.
                    +1

                    вот так копаешься в либах от Microsoft: вроде всё понятно, так и так, ща вот тут подлезу и будет шоколадно вообще… итут НННА ТЕБЕ INTERNAL.

                      0
                      Я тоже копался в некоторых либах от MS. И знаете, internal там далеко не самое страшное зло.
                +3
                Наследники гиперчувствительны к любым изменениям предка.

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

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

                  Не автор, но прокомментирую :-)


                  Часто забывают, что ООП — это от слова "объект", а не от слова "класс". Задача программиста — сделать так, чтобы получались объекты, обладающие требуемым поведением. В среднестатистическом мейнстримном языке типа C#/Java есть 2 подхода:


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

                  Если весь код написан таким образом, что единственный способ сконструировать объект это new ЧтоТоТам(), конечно тут кроме наследования ЧтоТоТам нет вариантов. Но можно же изначально заложиться на new ЧтоТоТам(new КакЯДелаюВотЭто(), new ИЕщёКакЯДелаюВотЭто()). В таком случае получается на порядок больше мелких классов, где одни классы описывают какой-то конкретный аспект поведения, а другие — просто скручивают несколько таких аспектов поведения в один "настроенный объект". Статья автора, если я правильно понял, про такой подход.

                    –6
                    1) Это называется АОП — Аспектно-ориентированное программирование. И оно — не панацея, так как не позволяет безболезненно связать между собой разные аспекты.
                    2) Это верно только для простых алгоритмов, которые будут вызываться изнутри кода. Вроде SortedVector(SortingAlgorithm); SortedVector(new Bubble()); SortedVector(new QSort());
                    Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта, ваш подход ещё хуже, так как требует объявить ваши КакЯДелаюВотЭто и ИЕщёКакЯДелаюВотЭто друзьями класса ЧтоТоТам. Далеко не все ЯП позволяют потомкам «друзей» оставаться «друзьями».
                      +7
                      Это называется АОП

                      Это ни в коем случае не АОП. Это самое обычное ООП + делегирование.


                      Это верно только для простых алгоритмов… Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта, ваш подход ещё хуже

                      Посмотрите примеры использования паттерна "Стратегия" — это собственно делегирование в чистом виде и есть.

                        –10
                        Это ни в коем случае не АОП

                        Думаете? Подумайте лучше.
                          0

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


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

                          В случае моего примера выше мы в явном виде провели декомпозицию.

                            0
                            >В случае моего примера выше мы в явном виде провели декомпозицию.
                            В случае примера выше вы написали три бессмысленных названия.
                            И будьте скромнее, ваше вашество, не стоит себя на «мы» величать. Если вы, конечно, не коллективный автор.

                            Как я уже писал выше, это работает для простых алгоритмов. Как только у вас появляется сложное поведение, которое не умещается в простую стратегию, все ваши красивые приёмы с декомпозицией перерастают в монстров.
                            Либо это будет чудище Франкенштейна, сшитое из десятков кусков, каждый метод которых принимает по дюжине разных структур, и тогда собирание монстра превратится в настоящее испытание и станет источником багов, а интерфейсы самого чудища в поисках упрощения обрастут сотнями ненужных обывателям внутренних методов.
                            Либо это будут сильно связанные классы, знающие о кишках друг друга и, в итоге, приводящих к тем же проблемам, что и простое наследование, но не дающее его преимуществ.
                            Либо это будет очередное АОП.
                            Вы не можете избавиться от сложности, её можно только выдавливать из одного места в другое. Декомпозиция выглядит достаточно заманчиво и очень часто именно она должна использоваться вместо наследования. Но не всегда.
                          0
                          Тогда считайте, что каждый виртуальный метод — это такая запись делегирования, не загромождающая код. Ведь функция — это тоже объект.
                            +3

                            А мне почему-то кажется что это реализация IoC через DI, в данном случае — constructor injection. А еще мне кажется, что из всего SOLID, Liskov Substitution самый жесткий и самый неоднозначный. Если вдуматься, последовательное его использование эффективно кастрирует саму идею наследования. О чем, впрочем, и написана эта статья.

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

                                Зачем подстраиваться? Изначально же понятно, что полиморфизм обеспечивается только в рамках спецификации класса-предка.

                                  0

                                  Затем, что именно принцип Лисков позволяет обеспечить полиморфизм "в рамках спецификации класса-предка".

                                    0

                                    Да, но наследование ведь изначально предполагает, что потомок реализует спецификацию предка? Реализация меняется, спецификакия — нет.

                                      0
                                      Да, но наследование ведь изначально предполагает, что потомок реализует спецификацию предка?

                                      Это и есть принцип Лисков, соблюсти который при наследовании реализации нетривиально.

                                        0

                                        нетривиально != невозможно. И даже сильнее: нетривиально != сложно. Нетривиально, означает, что потребуются не только руки + клавиатура, но еще и мозг. Но мы ведь не боимся пускать в ход столь мощный инструмент?
                                        Кстати, вас не сильно затруднит дополнить вот это и это вашим вариантом реализации через инкапсуляцию? Мне кажется, это было бы полезной иллюстрацией.

                                          0

                                          через инкапсуляцию композицию, конечно же. прошу прощения.

                                            0
                                            Я не автор статьи и не эксперт, но это могло бы выглядеть примерно так
                                              0
                                              > Нетривиально, означает, что потребуются не только руки + клавиатура, но еще и мозг.

                                              Нет. Есть принципиальная проблема, что «спецификация предка» — это не интерефейс, и формально нигде не описана. То есть меняя поведение в наследнике вы реализуете «спецификацию», которая есть только у вас в голове. А формально, скорее всего (если вы не наследуете абстрактный класс — точно), нарушаете LSP.
                                    0

                                    Почему вы наследование интерфейсов называете кастрацией?

                                      0

                                      потому, что наследуется только интерфейс. Как китайский "iPhone". Выглядит так же, а что внутри неизвестно. Для повтороного использования кода приходится применять другие механизмы (трейты, композицию+декоратор). Нет, я не против всего этого. Более того, прежде чем наследовать свои классы от неподконтрольного мне кода я еще очень подумаю. Но почему я должен отказываться от наследования в собственном коде?

                                        0
                                        Как китайский "iPhone". Выглядит так же, а что внутри неизвестно.

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


                                        Но почему я должен отказываться от наследования в собственном коде?

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

                                          0
                                          Неявное предположение "меньше кода — проще сопровождение" в данном случае неверно.

                                          Дело не собственно в количестве кода. Дело в том, что одинаковая функциональность оказывается реализована во множестве мест. Если требуется ее изменить, менять придется также во многих местах. При этом не очевидно, что в разных местах она будет реализована одинаково. Это все тоже создает проблемы при сопровождении. Решения есть, но они уводят нас очень далеко от ООП.


                                          Минимальной единицей контроля аффекта от изменений становится не один класс, а вся иерархия.

                                          Совершенно верно. В этом и суть областей видимости. Цена известна заранее. Платить ее или нет нужно решать в каждом конкретном случае. Но это не отменяет необходимости поиска консенсуса между теми, кто эту цену в конечном результате будет платить.


                                          Ну и инкапсуляцию тоже нельзя доводить до абсолюта. Скажем полезно будет указать, что вычислительная сложность метода justSolveTheProblemHereAndNow(int n) составляет O(n!), а findTheBestWifeFor(Person p) прокачает через сервер содержимое всех сайтов знакомств в интернете.

                                            0
                                            Дело в том, что одинаковая функциональность оказывается реализована во множестве мест.

                                            Не надо путать композицию с копипастой — одинаковая функциональность реализуется ровно один раз.


                                            Скажем полезно будет указать, что вычислительная сложность метода justSolveTheProblemHereAndNow(int n) составляет O(n!)

                                            Это не нарушение инкапсуляции, а уточнение публичного контракта.

                                              0
                                              Не надо путать композицию с копипастой — одинаковая функциональность реализуется ровно один раз.

                                              Хорошо, не будем.


                                              Это не нарушение инкапсуляции, а уточнение публичного контракта.

                                              Абсолютно согласен. Так же как и "вызывает add()".

                                                0

                                                "Вызывает add" в исходном публичном контракте нет.

                                                  0

                                                  Значит информации этого контракта недостаточно для реализации расширения через наследование.

                                  0
                                  > Если это более сложный класс, который, тем более, должен иметь доступ к приватным полям объекта

                                  Нонсенс. Если внешний код должен иметь доступ к полю — это не приватное поле.

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

                                    Простой пример: UndoStack. Если нам понадобится расширить его функционал возможностью коммитов на внешнюю машину (с вероятными былинными отказами), то без доступа к буферу команд ничего не получится. Никто не оставляет публичными команды манипуляций с буферами и не оставляет их для стратегий. Значит, единственная возможность — лезть в потроха. С наследованием всё просто. А без него?
                                      0
                                      Наследование расширяет функционал предка(вообще тезис… спорный, но пусть), враппер расширяет функционал. А к чему это?

                                      Если вам надо UndoStack, а он sealed, или буфер не protected, и исходников нет — то жопа.
                                      Если исходники есть — вытащите стратегию, это типа в 3 клика делается. Код с вашим наследником совпадёт чуть менее, чем полностью.
                                        0
                                        Напомню,
                                        > 3) Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
                                        > 4) Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.

                                        Такие классы, обычно, поставляются фреймворками. Очень часто фреймворк в бинариках поставляется, и далеко не факт, что его можно будет пересобрать без лишней боли. Очень часто любое изменение исходных кодов фреймворка (равно как и дублирование кода из фреймворка с последующей правкой под себя) требует изменения лицензии. Очень часто архитекторы даже не задумываются, что такая функциональность в принципе может понадобиться, или оставляют это на откуп конечным разработчикам, которые ставят sealed/final на автомате Вот и думайте, что делать, когда пойдёт волна свежих программистов, которые будут поголовно запечатывать свои классы потому, что Bonart так написал.
                                        Так же, напомню, что враппер не обладает никакими из свойств оборачиваемого объекта, даже если их унаследовать от одного интерфейса, связи «is a» точно не получится, что, конечно, не важно, когда у тебя понятия «класс» нет и не предвидится или когда архитектор предусмотрел это и потребовал интерфейс, но так бывает далеко не всегда.

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

                                          Вообще, это достаточно здравая идея. Класс, от которого можно эффективно наследовать должен быт соответствующим образом подготовлен. При его разработке должны быть убраны все возможные побочные эффекты, затрудняющие наследование. Если избавиться от них невозможно, их описание должно быть включено в спецификацию. Такая спецификация должна быть обязательно подготовлена, примерно так. Хорошо, если также будет открыт код. Если этого не сделано наследование от такого класса небезопасно.
                                          B вообще, в некоторых недавно появившихся языках, наследование запрещено по-умолчанию. Чтобы разрешить надо указывать open.

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

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

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

                                                  0
                                                  Так и что здравого в пометке всего sealed?

                                                  Наличие виртуального метода подразумевает подготовку к наследованию. Если вдруг(!) у публичного класса подготовка ненадлежащая — sealed, в качестве исключения.
                                                    0

                                                    У публичного класса в 95 % случаев подготовка отсутствует, в 4,9% — ненадлежащая.

                                                      0
                                                      У публичного класса в 99% нет виртуальных методов. В 0.9% от него никому в голову не придёт наследоваться.

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

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

                                                          0
                                                          > От класса без виртуальных методов наследоваться бесполезно.

                                                          Ну, это вы загнули. Часто вы в C# перекрываете виртуальные члены Object? ) Если бы ToString() и GetHashCode() были в интерфейсе — вообще не перекрывали бы.
                                                            0
                                                            Часто вы в C# перекрываете виртуальные члены Object?

                                                            Да, хочу чтобы сравнение и ключи в dictionary работали нормально.
                                                            Еще хочу чтобы в отладчике и логах информация была читаемой.
                                                            И это ничего, что наследование от object слегка недобровольное?

                                                              0
                                                              А сравнение и dictionary и так, в целом, работают. ToString легко заменить не виртуальной пропертью(или даже полем).

                                                              > И это ничего, что наследование от object слегка недобровольное?

                                                              Как так? Вы же хотите, чтобы в отладчике и логах информация была читаемой?

                                                              Ну ладно, пусть не Object.
                                                              Берём официальное руководство по C#, раздел «наследование» (https://msdn.microsoft.com/ru-ru/library/ms173149.aspx) и смотрим на пример:

                                                              // ChangeRequest derives from WorkItem and adds a property (originalItemID)
                                                              // and two constructors.
                                                              public class ChangeRequest: WorkItem
                                                        +1
                                                        А у интерфейсов они 100% хорошие, симпотичные, да в туфельках отличных, ага.
                                                          0
                                                          А что не так с интерфейсами?
                                                            0
                                                            А что не так с публичными классами?
                                                              0
                                                              Отличная от нуля вероятность кривой имплементации.
                                                                0
                                                                Отличная от нуля вероятность кривой имплементации. И не подумайте, что я вас передразниваю.
                                                                  0
                                                                  В интерфейсе, по определению, нет имплементации. Я думаю, вы обезьянничаете.
                                                                    0
                                                                    «Имплементация» интерфейса — фича весьма нехарактерная для dotNet.
                                                          0

                                                          Насколько я понимаю sealed блокирует дальнейшее наследование. Т.е. помеченный как sealed метод перестает быть доступным дла перекрытия для потомками класса, в которых он описан. Не знаток С#, поэтому поправьте меня пожалуйста, если я не прав. Поскольку это уже не тот метод, который был в классе-предке, его "подготовленность к наследованию" опять под вопросом.

                                                            0
                                                            sealed предлагается использовать на уровне класса.

                                                            sealed на уровне метода подразумевает, что родитель был сначала virtual, потом был наследник, а потом решили, что эта иерархия плохо подготовлена. На мой взгляд, это «мы тут плохо надизайнили, но надо паблишить». Для либы — хак, для своего кода вообще непонятно кому надо.
                                                              +1

                                                              Это чертовски здорово, когда весь дизайн в одних добрых руках. Но может быть такой вариант. Беру я некую библиотку классов, которая написана замечательными специалистами и предполагает, что от их классов будет наследование. Они вылизали свои методы до блеска, исключили все побочные эффекты, снабдили библитотеку подробнейшей документацией, да еще на ютуб выложили 1000 часов лекций по ее использованию.
                                                              И вот пишу я какую-то жутко полезную утилиту, в которой свои классы наследую от классов из супербиблиотеки. Но у меня же нет столько времени и денег, чтобы снимать профессиональные ролики для ютуба? И вылизывать мои методы, которые перекрывают библиотечные, чтобы избежать проблем при наследовании мне тоже особо некогда. И вообще я ее для себя писал, но я же не жадина, пусть люди пользуются! Вот только обещать осутствие проблем при наследовании я не могу. Поэтому я и поставлю sealed на этих моих методах, чтобы никого не вводить "во искушение". Хотите инкорпорировать? Инкорпорируйте! Хотите наследовать? Наследуйте! Хотите перкрывать вот этот и этот метод? Нет, лучше не надо. Вот будет у меня отпуск, я вот этот метод отполирую до блеска, и в следующей версии sealed уберу. То-то вам будет радости :-)

                                                                0
                                                                > Поэтому я и поставлю sealed на этих моих методах,

                                                                вы запечатаете не свой метод, а библиотечный, и «Хотите наследовать? Наследуйте!» станет издевательством.

                                                                В остальном — правильно: sealed позволяет публиковать недоделанную фигню, имитируя заботу о том, кто с матами будет пытаться обойти sealed.
                                                                  0

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


                                                                  Что значит "публиковать недоделанную фигню"? Если я, веду разработку на одном из публичных ресурсов типа github или bitbucket "фигня" будет опубликована начиная с первого коммита.

                                                                    0
                                                                    У вас одни противоречащие параграфы. Зачем мне ваша жутко полезная утилитка, если при любом чихе мне надо наследоваться от базового, теряя ваш функционал? Почему вы при этом говорите «я же не жадина, пусть люди пользуются»?

                                                                    И, главное, зачем именно нужен sealed, если я могу матюгнуться, форкнуть и его удалить. Опубликовав исходники «не вводить во искушение» уже не в вашей власти.
                                                                      0
                                                                      Зачем мне ваша жутко полезная утилитка, если при любом чихе мне надо наследоваться от базового, теряя ваш функционал?

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


                                                                      Почему вы при этом говорите «я же не жадина, пусть люди пользуются»?

                                                                      Потому, что закрывая наследование реализации предоставляю другой, более безопасный [в данном конкретном случае] [с моей точки зрения] метод.


                                                                      Опубликовав исходники «не вводить во искушение» уже не в вашей власти.

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

                                                                        0
                                                                        > инкорпорируйте и не теряйте.

                                                                        Да проще на лету подменять код и ставить везде паблик. Вопрос пары лишних строк в билдфайле.
                                                                          0
                                                                          > Но это уже не мои проблемы.
                                                                          > Я закрыл опасное место

                                                                          Вы или крестик снимите, или одно из двух.
                                                                            0

                                                                            Да почему же? Я что, должен страдать о того, что кто-то форкнул мой код, потом покорежил его, а потом? Строго говоря это уже вообще не мой код. Бред какой-то...

                                                                              0
                                                                              Вы вообще не должны страдать, даже sealed можно не ставить
                                                            0

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

                                                              0
                                                              Если бы наследование было только ради полиморфизма, то добавлять новые (публичные) свойства и методы давно запретили.
                                                        0
                                                        Враппер, реализующий интерфейс, обладает всеми нужными свойствами. Я бы не рекомендовал использовать фреймворк, использующий «is a» по классу, реализующему интерфейс — его проектировали люди, в принципе не понимающие что они делают. Наличие исходников без права их использования — тоже плохой знак.

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

                                                        Впрочем, ставить sealed на автомате — тоже не очень хорошая идея. Надо запрещать случайно прострелить себе ногу, а не осмысленно.
                                                          0
                                                          То есть мы выбрасываем Java, выбрасываем .Net, у них же реализация нефинальных классов сокрыта, выбрасываем Qt, исходники нельзя переиспользовать без покупки, Wx выбрасываем, к чёрту Unity и Unreal Engine. Это, естественно, не полный список, но даже его хватает для того, чтобы понять — в помойку всё.

                                                          Вы немного не понимаете, в чём дело. Порой нужно расширить функциональность для тех классов, в которых изначально такое расширение не предусматривалось не по злому умыслу, а по незнанию. Пример с UndoStack я уже приводил. Если UndoStack вне области, доступной для модификации (как минимум, по банальной причине бинарной совместимости), ваш подход мне ничего не даст. Ничего не дадут мне ни интерфейсы, ни подходы в стиле Rust, ни врапперы. Я либо смогу получить доступ к полям класса, либо мне придётся реализовывать всё самому, и ничего мне не поможет. Да, если на вход объекта передавать интерфейс, а не конкретный класс, то гораздо проще объект заменить, но мне не нужно его заменять.
                                                            0

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

                                                              0
                                                              В статье даны выводы 3 и 4, призывающие запрещать наследование реализаций по умолчанию. Вот и скажите мне, к чему призывает статья.

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

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

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

                                                                    Нет. Я советую если "код из виртуальных методов не вынесен в обычные..." и/или не приняты еще какие-то специальные меры предосторожности по возможности отказаться от использования наследования в пользу инкорпорации + использования интерфейсов. Или не отказываться и принять на себя все риски.
                                                                    Про небезопасность наследования интерфейсов очень хотелось бы послушать подробнее, с примерами. Лучше знать обо всех граблях, уоторыми устлан наш нелегкий путь.

                                                                      0
                                                                      Да только ода проблема. Никаких гарантий.
                                                                      К примеру, вы сделали интерфейс потока данных. И ожидаете, что если запись не пройдёт успешно, вам возвратят ошибку. А вам, допустим, выбрасывают исключение, которое кладёт всё приложение, ведь его никто не ловит. Формально вы не виноваты.
                                                                      Ладно, это пол-беды. другой пример. Вы в вашей стратегии ждёте контейнер для хранения объектов. И ожидаете, что вы сможете в него сбрасывать ссылки на временные объекты. Последнее, что вы ожидаете — это что у контейнера будет глобальное состояние, и объекты не умрут после удаления самого хранилища. В этом случае формально виноват будет пользователь, хотя он не нарушит никаких соглашений. Ведь в интерфейсе, который вы ожидали, не написано, как должны храниться объекты на протяжении жизни экземпляра приложения. С наследованием такая херня не пройдёт, контейнер выполняет свой деструктор, который выполнит деструкторы всех объектов, и даже если в глобальном пространстве сохранятся ссылки, они будут битыми и относительно безвредными.
                                                                      Ладно, у нас же камень преткновения — виртуальные методы. Так и быть, пусть у нас есть класс фигуры. И у него есть виртуальный метод для вычисления центра масс. «Стратегия вычисления центра масс!» — возразите вы. И не сказать, что ошибётесь. Но есть одно но. В простейшем случае центр масс зависит от геометрической формы. Однако он может зависеть от распределения плотностей… И от скорости… И от импульса… Ускорения… Энергии… Так что будет принимать стратегия на вход? Какой у неё интерфейс? Снова перекладываем всё на шею пользователя? Самое грубое решение. В чём же преимущество виртуального метода? В том, что не нужно придумывать интерфейс стратегии, доступ ко всем нужным данным уже имеется. Почему бы нам не сделать доступ ко всем этим данным из интерфейса? Ну, можно, и даже нужно, если это какой-нибудь физический движок. А если нет? Или, если, например, мы не можем предоставить однородный доступ к данным, что тогда? Например, если у нас фракталы есть. Ведь для отрисовки фрактала не нужно знать его форму, достаточно отрендерить его в текстуру и натянуть на AABB, а центр масс вычислять по аналитической аппроксимации. Чисто теоретически, всё это можно сделать на чистых интерфейсах. Будет ли это проще? Будет ли меньше ошибок?

                                                                      Конечно, в моих примерах много «но» и «если», но ведь и в ваших этих «если» не меньше. Можно придумать ещё много примеров, в которых кто-то будет допускать ошибки, в которых формально вы не виноваты. Однако ровно так же не виноваты и те, кто не закрыл свои классы для наследования. Единственная разница, ошибки произойдут в их вотчине. Вот и всё.
                                                              0
                                                              > То есть мы выбрасываем

                                                              Что?

                                                              > Если UndoStack вне области, доступной для модификации (как минимум, по банальной причине бинарной совместимости), ваш подход мне ничего не даст.

                                                              Мой опыт говорит, что в UndoStack сами команды всё равно приватные (в крайнем случае internal), наследованием там ничего не поправить. Если код не задуман для расширения — наследование не спасёт.

                                                              Если вы меня хотите убедить, что наследование можно использовать как грязный хак ради «паблика Морозова» — соглашусь, можно.
                                                  0
                                                  Про последний абзац, в том числе эту идею развивает Егор Бугаенко (yegor256.com), если не слышали о нём рекомендую ознакомится с 105 выпуском подкаста «Разбор полетов».
                                                    +2

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

                                                    0
                                                    Тогда для более-менее серьезного переопределения поведения понадобится создавать сразу несколько аспектов, которые надо еще и как-то инкапсулировать в один «набор аспектов, реализующих вот такое вот поведение». При этом каждый аспект сам по себе будет являться наследником виртуального класса. В конечном итоге мы приходим к куда более сложной и многословной реализации той же самой мысли.
                                                      +1
                                                      Если кто не понял, то напишу это же проще. Не используйте «Шаблонный метод», а используйте «Стратегию». :)
                                                        0
                                                        Одно без другого не получается, скорее не реализуйте «Шаблонный метод» используя наследование
                                                        0
                                                        «Часто забывают, что ООП — это от слова „объект“, а не от слова „класс“ ».
                                                        Часто забывают, что класс это тоже объект…
                                                          0
                                                          Два чаю! К сожалению, а может и к счастью, это утверждение не всегда верно.
                                                        0
                                                        И что же теперь, ограничиваться одним уровнем наследования от абстрактного класса?

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


                                                        Внимание, вопрос: с точки зрения автора статьи, как обходиться в таких ситуациях?

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

                                                          +1
                                                          Как будто что-то плохое. Кто хоть раз ковырялся в семи уровнях наследования WPF, тот меня поймет.

                                                          Один пример плохой реализации не может быть доказательством несостоятельности концепции. Например, иерархия классов виджетов Qt вполне себе адекватна и красива.

                                                          По мелочам обычно выручает декоратор

                                                          Так а в чем профит то? Если мне нужно дополнить всего один метод, то в случае с наследованием я переопределю всего один метод: вызову реализацию предка и допишу пару частных операторов. В случае с декоратором мне придется писать класс-декоратор, который будет либо унаследован от базового декоратора (что следуя вашей логике плохо), либо продублирует код базовой реализации (что уж точно нехорошо).
                                                            +1
                                                            Один пример плохой реализации не может быть доказательством несостоятельности концепции. Например, иерархия классов виджетов Qt вполне себе адекватна и красива.

                                                            И чем же плохо ограничение, препятствующее плохой реализации и облегчающее хорошую?


                                                            Так а в чем профит то?
                                                            Вы точно читали статью и мой предыдущий комментарий?

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


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

                                                            Почему вы проигнорировали композицию с делегированием? Вы точно знаете, что такое декоратор?

                                                              0
                                                              просто посчитайте объем строк кода и число программных сущностей при реализации через наследование и при реализации через декорирование/делегирование/композицию
                                                                +1

                                                                Разница между наследованием и композицией:


                                                                1. Минус одно наследование.
                                                                2. Плюс одно приватное поле
                                                                3. Плюс "шумовой" делегирующий код (можно избавиться используя динамическую типизацию или метапрограммирование).

                                                                Пункты 2 и 3 — экономия при написании кода, пункт 1 — при сопровождении. Итоговый баланс немного предсказуем.

                                                                  +4
                                                                  Я вот искренне не понимаю, зачем нужен пункт 3? Зачем использовать какой-то кодогенератор, когда разработчики языка позаботились об этой ситуации заранее и предоставили тебе наследование? Ведь это упрощённый способ записи делегации. Без бойлерплейта. Из каробки. Может называется по-другому, но работает точно так. А если что-то плавает как утка и крякает как утка, то это и есть утка.
                                                                    0
                                                                    Bonart говорит о гибкости и корректности в сложном случае, а вы — о простом случае. Может быть поэтому вы друг друга не понимаете?
                                                                      –1

                                                                      Наследование как "упрощенная запись делегации" — ложь.
                                                                      "Без бойлерплейта" — ложь.
                                                                      "Работает точно так" — снова ложь.
                                                                      Контрольный вопрос — как скомбинировать функционал нескольких наследников с мелкими модификациями/дополнениями функционала?
                                                                      С декораторами это делается элементарно.

                                                                        0
                                                                        ``Наследование как «упрощенная запись делегации» — ложь'' — ложь
                                                                        ``«Без бойлерплейта» — ложь'' — ложь
                                                                        ``«Работает точно так» — снова ложь'' — ложь

                                                                        > Контрольный вопрос — как скомбинировать функционал нескольких наследников с мелкими модификациями/дополнениями функционала?

                                                                        Мультинаследование.
                                                                          0
                                                                          Мультинаследование
                                                                          Недоступно в большинстве языков.
                                                                            +1
                                                                            Ну а где-то не доступны декораторы, и динамическая генерация (байт)кода. Там, где уже всё есть — можно пользоваться готовым наследованием. Там, где нет — придумывать замену. Но, очевидно, не надо говорить о том, что наследование не нужно, если оно уже реализовано. Хорошо реализованное наследование решает множество проблем композиции без необходимости задействовать кодогенерацию. С другой стороны, можно как в лиспе наследование реализовать через кодогенерацию (метаобъектный протокол). Заодно получить возможность реализовывать разные механизмы наследования в зависимости от выбора метаобъектного протокола. Но какова бы не была реализация, принцип остаётся в силе — один и тот же, и для наследования, и для метагенераторов.

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

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

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

                                                                          +1

                                                                          А вот про эти языки можно поподробнее. Хотя бы список.

                                                                            0

                                                                            Ну, полный список я Вам вряд ли предоставлю. Но для примера в Ruby есть def_delegator для этого, некоторые используют delegate из ActiveSupport, хотя это уже ближе к кодогенерации.
                                                                            Кстати, аналогичную кодогенерацию можно устроить в любом языке где есть compile-time макросы. Rust, Crystal, Nim, etc. к вашим услугам. В Crystal такое есть из коробки, про остальных точно не знаю. Но сделать аналог весьма просто.
                                                                            В функциональных языках делегация тоже хорошо поддерживается, только там она осуществляется от модуля к модулю, например defdelegate в Elixir.

                                                                              0

                                                                              Спасибо! Интересна была как раз реализация средствами языка, а не макросами/препроцессорами. И мне тут привели хороший пример.

                                                                                +2

                                                                                Так а в чём разница то? В языках, где есть нормальные макросы (если у Вас это слово ассоциируется с C++, то забудьте эту ассоциацию), абсолютное большинство средств языка выражается именно через макросы, включая элементы синтаксиса.
                                                                                В Kotlin, насколько я понимаю, пока очень урезанная поддержка делегации — либо всё делегировать, либо ничего.

                                                                                  +2
                                                                                  Не обольщайтесь, та же самая кодогенерация, даже хуже, потому что встроена в язык. AFAIK в Ruby ещё можно перехватывать вызовы несуществующих методов, можно сделать делегирование через эту фичу.
                                                                                    0

                                                                                    В дотнете тоже можно так, через DynamicObject, ну или RealProxy.

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

                                                                                      т.е. препроцессор встроен в компилятор и развертывает эти констркуции в код на Kotlin?


                                                                                      AFAIK в Ruby ещё можно перехватывать вызовы несуществующих методов, можно сделать делегирование через эту фичу.

                                                                                      В PHP тоже можно, но это совсем другая история.

                                                                                        0
                                                                                        Нет, на выходе сразу class-файл, но под капотом там конечно сгенерированные методы.
                                                                                          0

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

                                                                                            0

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


                                                                                            for(i=1; i<10; i++){...}

                                                                                            в такой же код как и


                                                                                            i=1; while (i<10) {...; i++}

                                                                                            это прямая компиляция или встроенный препроцессор? Грань тонка… И важно ли это с практической точки зрения? Мне кажется нет.

                                                                                              0

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

                                                                                                0

                                                                                                Вот что получилось. Даже не знаю, как это оценить.


                                                                                                Заголовок спойлера

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


                                                                                                Exception in thread "main" java.lang.ArithmeticException: / by zero
                                                                                                    at Base.test(Delegation.kt:6)
                                                                                                    at Derivative.test(Delegation.kt:-1)
                                                                                                    at DelegationKt.main(Delegation.kt:20)
                                                                                                  0

                                                                                                  По вашей ссылке у меня не выдаёт никаких исключений.

                                                                                                    0

                                                                                                    Это все потому, что я не умею работать с try.kotlinlang.org
                                                                                                    Попробуйте еще раз по той же ссылке

                                                                                                      0

                                                                                                      А что вы изменили? Я там вроде всё прокликал тогда, а стектрейс так и не появился.

                                                                  0
                                                                  Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода

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

                                                                  Например реализовываете вы собственный AbstractSpliterator. Вы можете реализовать только 1 метод в соответствии с контрактом и вообще не разбираться как оно там работает внутри (а работает оно хитро). Можете реализовать 2 или даже 4, если хотите улучшить производительность. Но даже в этом случае знать вам надо только контракты методов, а не то как они используются.
                                                                    0
                                                                    Точно, сейчас я считаю что большинство выстрелов по ногам происходит из-за нарушения контракта методов, в том числе при переопределении, или их отсутствия.
                                                                      0
                                                                      А можно не переопределять, а использовать пустые параметры и что-то вроде func_get_args() (PHP) :)
                                                                        0
                                                                        В смысле? Я не понимаю при чем тут это.
                                                                          0
                                                                          Тогда скажите, что Вы понимаете под «нарушение контракта методов»? Может я Вас не понял :)
                                                                            0
                                                                            https://goo.gl/b4Se5e
                                                                              0
                                                                              Тогда я говорил об этом:
                                                                              «В объектно-ориентированном программировании контракт метода обычно включает следующую информацию:
                                                                              возможные типы входных данных и их значение;»

                                                                              Объявив пустые параметры и использовав func_get_args() можно не править принимаемые параметры в наследнике вслед за родителем.
                                                                                0
                                                                                1. Я пишу на статически типизированном языке и не хочу вручную проверять типы.
                                                                                2. Если типы/кол-во аргументов изменилось это новый метод (перегрузка) со своим контрактом и можно с чистой совестью его описать.
                                                                                3. Меня распнут за такой код :)
                                                                                  0
                                                                                  1. Меня распнут за такой код :)

                                                                                  Вы гуманист! За такой код полагается только это.

                                                                      0
                                                                      Достаточно иметь контракт переопределяемого метода.

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


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

                                                                      Для интерфейса не требуется как раз потому, что в предке ни один из методов интерфейса не вызывается по определению.
                                                                      А вот для класса давайте рассмотрим следующий вариант использования:


                                                                      1. У вас в базовом классе есть виртуальные методы Add (добавляет элемент) и AddRange (добавляет пачку элементов)
                                                                      2. В наследнике вам необходимо подсчитать общее количество добавленных элементов.

                                                                      Как вы реализуете эти методы в классе-наследнике?

                                                                        0
                                                                        Для интерфейса не требуется как раз потому, что в предке ни один из методов интерфейса не вызывается по определению.
                                                                        Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc

                                                                        Да, некоторые классы предназначены для расширения, некоторые — нет, а в некоторых об этом просто не подумали. То, что в некоторых случаях могут быть проблемы, еще не значит, что нет безпроблемных случаев. Если класс проектировался для расширения, то в нем ловушек быть не должно (например через private _Add), если не проектировался, то и методы не должны быть виртуальными.
                                                                          0
                                                                          Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc

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

                                                                            0
                                                                            Методы расширения отсутствуют в java. И имеют существенный недостаток: невозможно перегрузить более оптимальной реализацией. Например, если в переменной типа Seq[Int] в находится Range, то метод sum на этой переменной имеет сложность O(1) в scala. Если вынести sum в метод расширения, то оптимизировать для конкретных реализаций уже не получится.
                                                                      +5
                                                                      Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.

                                                                      Когда вдруг наследование стало архаизмом?

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

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

                                                                      Это надуманная проблема. Если вы взяли библиотеку и она решает ваши задачи, зачем ее обновлять?
                                                                      Да и в любом случае каждая зависимость от сторонних библиотек добавляет риск, и по идее, вы должны понимать этот риск.
                                                                      Сейчас какая-то странная тенденция, все хотят обновить все до самой новой версии, но помоему даже незнают зачем им это.
                                                                        0
                                                                        Кода «решающего задачу» в реальном мире не бывает — сегодня решает, завтра глюкануло. Сопровождение — самая дорогая часть разработки. Искать «того чувака» что включил — дорого. Разбираться — дорого. Модерировать изменения — дорого. Вылизывать прикладной код — дорого. Приемлемо — либо выкидывать и по-новой писать либо использовать обновляемые компоненты. А, ну да, покрытые тестами. Сейчас не 90-е, что кода на планете не найти.
                                                                        А так, навставлять необновляемого г*на и боятся его тронуть, оправдывая это дзеном, это да, мудрость кармой пи… битых.
                                                                          0
                                                                          Кода «решающего задачу» в реальном мире не бывает — сегодня решает, завтра глюкануло.

                                                                          Интересный у вас мир.

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

                                                                          Особенно когда у вас под сотню зависимостей, которые вы обновляете постоянно и каждое обновление вносит breaking changes.
                                                                          0
                                                                          Когда вдруг наследование стало архаизмом?

                                                                          Не знаю. Наследование интерфейсов до сих пор мейнстрим.


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

                                                                          "С умом" — априори верно, вот только неконкретно.


                                                                          Это надуманная проблема. Если вы взяли библиотеку и она решает ваши задачи, зачем ее обновлять?

                                                                          1. Мне сложно исправлять баги в самой библиотеке, в отличие от ее автора.
                                                                          2. Новая версия может решать больше моих задач с лучшим качеством.
                                                                            0
                                                                            Когда вдруг наследование стало архаизмом?

                                                                            Не знаю. Наследование интерфейсов до сих пор мейнстрим.

                                                                            Спрошу иначе. Когда наследование от обычных классов (имеющих реализацию) стало архаизмом?

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

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

                                                                            Новая версия может решать больше моих задач с лучшим качеством.

                                                                            Может и больше, может и лучше. Но, если обновление библиотеки ломает ваше приложение, то стоит ли оно того? Я даже соглашусь, что иногда стоит.

                                                                            «С умом» — априори верно, вот только неконкретно.

                                                                            Потому что тут все it depends от конкретной задачи.
                                                                              0
                                                                              Спрошу иначе. Когда наследование от обычных классов (имеющих реализацию) стало архаизмом?

                                                                              Давным-давно. Уже "банда четырех" поминала наследование реализации как нежелательное.


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

                                                                              То есть библиотеки вы все-таки обновляете. И без наследования реализаций риски снижаются.


                                                                              Потому что тут все it depends от конкретной задачи.

                                                                              Что не отменяет ряда общих закономерностей и рекомендаций.

                                                                                0
                                                                                Давным-давно. Уже «банда четырех» поминала наследование реализации как нежелательное.

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

                                                                                То есть библиотеки вы все-таки обновляете. И без наследования реализаций риски снижаются.

                                                                                Нет. В любом случае придется тестировать, исправлять и т.д. Если в либе поломали совместимось, это в любом случае дополнительная работа.

                                                                                Что не отменяет ряда общих закономерностей и рекомендаций.

                                                                                Если это про ваши «итоги», то я вот не согласен с пунктами 1, 2, 3 и 5. Это плохие рекомендации.
                                                                                С пунктом 3 не согласен, т.к. вы просто изменили формулироваку принципа «Favor object composition over class inheritance».
                                                                                  0
                                                                                  Нет. В любом случае придется тестировать, исправлять и т.д. Если в либе поломали совместимось, это в любом случае дополнительная работа.

                                                                                  Тестирование и исправление — части нормального процесса обновления. А поломать совместимость при наследовании реализации объективно намного проще.


                                                                                  Если это про ваши «итоги», то я вот не согласен с пунктами 1, 2, 3 и 5. Это плохие рекомендации.

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

                                                                                    0
                                                                                    Это ваше оценочное суждение не подкреплено ни ссылкой на теорию, ни практическими контрпримерами.

                                                                                    Я всегда считал, что интерфейсы это некий костыль, хотя я ими активно пользуюсь и они позволяют решить множество проблем, но решают они эти проблемы зачастую добавляя ограничения. А я считаю, что ограничения — это зло. Например в Java и C# нет множественного наследования. Все что позволяют такие ограничения — это брать на работу плохих программистов и не бояться, что они напишут страшный код, обычно еще такие люди умеют писать только на одном языке, и так мы получили такие названия в должностях как Frontend Developer, Backend Developer. Я не говорю о том, что все должны все знать, это невозможно. Но бояться, что при использовании наследования что-то там сломается… вы серьезно? Есть куча техник как взять риски под контроль, а не вводить искусственные ограничения. Например Collective Code Ownership, TDD, Code Review, DDD и т.д.

                                                                                    Вот неплохое объяснение довольно известного специалиста: http://blog.cleancoder.com/uncle-bob/2015/01/08/InterfaceConsideredHarmful.html

                                                                                      0
                                                                                      Я всегда считал, что интерфейсы это некий костыль

                                                                                      Интерфейсы — костыль ровно в той же степени что и классы.


                                                                                      А я считаю, что ограничения — это зло

                                                                                      Правда? Вы предпочитаете опасные бритвы, кабеля без изоляции, лестницы без ограждения, оживленные перекрестки без светофоров? Что же вы пишете на TypeScript вместо няшного неограниченного JavaScript?


                                                                                      Например в Java и C# нет множественного наследования.

                                                                                      Нет множественного наследования реализаций. Интерфейсов — пожалуйста.


                                                                                      Все что позволяют такие ограничения — это брать на работу плохих программистов

                                                                                      Опять оценочное суждение без малейшего обоснования.


                                                                                      Но бояться, что при использовании наследования что-то там сломается

                                                                                      Зачем бояться? Это просто факт — использование наследования реализаций ломает инкапсуляцию. Точка.


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

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


                                                                                      Вот неплохое объяснение довольно известного специалиста

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

                                                                                        –1
                                                                                        Правда? Вы предпочитаете опасные бритвы, кабеля без изоляции, лестницы без ограждения, оживленные перекрестки без светофоров? Что же вы пишете на TypeScript вместо няшного неограниченного JavaScript?

                                                                                        Кстати мне очень нравятся опасные бритвы, но с ними много телодвижений, а я лентяй.
                                                                                        А вот например TypeScript дает мне больше гибкости, чем Javascript. Можно сделать контракт модуля типизированным и получить профит от статической типизации и intellisense, а внутри например писать как на JS. Можно сделать прототип быстро, срезав углы так сказать, а потом отрефакторить. Ведь на TS можно как на JS писать. Т.е. просто больше возможностей.

                                                                                        Зачем бояться? Это просто факт — использование наследования реализаций ломает инкапсуляцию. Точка.

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

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

                                                                                        Collective Code Ownership, TDD, Code Review, DDD — мне казалось это просто стандартные техники и методологии. Я имею ввиду XP, Agile и т.д.

                                                                                        Дядюшка Боб в этот раз выдал порцию оценочных суждений вместо опоры на факты.

                                                                                        Видимо вы во всем видите только «оценочное суждение», уж простите, за оценочное суждение ваших суждений)))
                                                                                          0
                                                                                          Кстати мне очень нравятся опасные бритвы, но с ними много телодвижений

                                                                                          Вот и с наследованием примерно так же. Я ленивый и лишних телодвижений не люблю.


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

                                                                                          Итак, ограничения в TypeScript (статическая типизация) дают вам больше гибкости, чем куда более свободный JS. Получается, что не все ограничения зло?
                                                                                          Наследование интерфейсов также дает больше гибкости, чем наследование реализации, за счет ослабления зависимостей между классами.


                                                                                          В моем мире это по другому — наследование реализаций может сломать инкапсуляцию, но крайне редко и это можно предотвратить с помощью тестирования.

                                                                                          Это каким образом? Публичный контракт предка не нарушен, а ваш наследник начал косячить. Причем никакое тестирование полного выявление ошибок не дает, а стоят дополнительные тесты куда дороже отказа от наследования реализации.
                                                                                          И если факт поломки инкапсуляции объяснен "на пальцах", доказан теоретически и проиллюстрирован на простом примере прямо здесь, то ваше мнение о его редкости и легкости предотвращения по-прежнему остается мнением, со ссылкой на такуе же точку зрения авторитета без доказательств и наглядных иллюстраций.


                                                                                          Collective Code Ownership, TDD, Code Review, DDD — мне казалось это просто стандартные техники и методологии. Я имею ввиду XP, Agile и т.д.

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

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

                                                                                            Очень просто, писать тесты. Тесты это инвестиции, а не затраты всетаки. Как раз в примере про Add/AddRange тесты бы вам показали проблему. Ну и да, написать тесты грамотно, тоже уметь нужно. У меня складывается впечатление, что вы против TDD, поправьте меня если не прав.

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

                                                                                            В теории, что я читал, я видел только рекомендации выбирать композицию вместо наследования. И я абсолютно согласен с этим в 95% случаев.
                                                                                            Просмотрел статью и комментарии, но доказательств не нашел. Если это про Add/AddRange, это всего лишь один частный пример. Все, что я вижу, это то, что наследование реализации МОЖЕТ сломать инкапсуляцию. С этим согласен.

                                                                                            Жаль диалог зашел в тупик. Было интересно.
                                                                                              0
                                                                                              Просмотрел статью и комментарии, но доказательств не нашел.

                                                                                              Посмотрите работу 1986 года по ссылке.


                                                                                              Очень просто, писать тесты. Тесты это инвестиции, а не затраты всетаки.

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


                                                                                              Как раз в примере про Add/AddRange тесты бы вам показали проблему.

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


                                                                                              Все, что я вижу, это то, что наследование реализации МОЖЕТ сломать инкапсуляцию.

                                                                                              Вы думаете, что сломанная инкапсуляция — это видимый сбой? Но это неправда:


                                                                                              1. Сломанная инкапсуляция — это ошибка, она сразу заложена в наследование реализаций.
                                                                                              2. Хрупкий код наследника, не работающий при изменениях (или просто при недостатке информации) в деталях реализации предка — это дефект, возникший вследствие ошибки.
                                                                                              3. И наконец, некорректный расчет — это сбой как следствие дефекта.

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

                                                                                                0
                                                                                                Посмотрите работу 1986 года по ссылке.

                                                                                                Ок. Ее я не прочитал.

                                                                                                самые опасные сбои в промышленной эксплуатации как раз редкие и плавающие

                                                                                                Согласен, но это нормально. Есть даже теория надежности.

                                                                                                Если писать на Java или C# невозможно отказаться от наследования, я имею ввиду совсем отказаться, тк возможно вы используете библиотеку или фреймворк от которых нужно наследоваться.
                                                                                                И я не разделяю, наследование реализации и наследование интерфейсов как вы, для меня это одно и тоже, в идеале, это не важно (но, как мы знаем, на самом деле важно)
                                                                                                Даже в томже Go есть встраивание (embedding), что похоже на множественное наследование и композицию.
                                                                                          0
                                                                                          > Что же вы пишете на TypeScript вместо няшного неограниченного JavaScript?

                                                                                          Даже в статически типизированных языках есть dynamic cast. Так что к каждой инкапсуляции должен прилагаться способ её нарушить.
                                                                                          0

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

                                                                                            0
                                                                                            Frontend и Backend — это не языки, а предметные области.

                                                                                            Не совсем понял вас. Если это про предметные области DDD, то нет.

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

                                                                                            Я согласен, что разница есть, но она минимальна.
                                                                                              0
                                                                                              А разница… если про программирование, то разница не большая, те же компоненты, классы, те же паттерны.

                                                                                              … реактивное программирование, инкрементальный рендеринг — всё это ой как надо бэкенду :-)


                                                                                              Если говорить по хайлоад например,

                                                                                              Разные приоритеты. Для UI важна отзывчивость, для бэкенда — пропускная способность.


                                                                                              то это не совсем про программирование, это скорее про архитектуру.

                                                                                              Архитектуры-то разные. От слова совсем.


                                                                                              Если про дизайн и верстку, тут да, мне например это скучно делать, но сейчас стало проще, есть bootstrap и его друзья.

                                                                                              Толковый фронтендер его бы ни за что не стал использовать.


                                                                                              Я согласен, что разница есть, но она минимальна.

                                                                                              Боюсь даже представить, как бы вы реализовывали SPA :-)

                                                                                                0
                                                                                                реактивное программирование, инкрементальный рендеринг — всё это ой как надо бэкенду :-)

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

                                                                                                Архитектуры-то разные. От слова совсем.

                                                                                                Я не замечаю, тот же MVC или MVVM например. Теже слои.

                                                                                                Толковый фронтендер его бы ни за что не стал использовать.

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

                                                                                                Боюсь даже представить, как бы вы реализовывали SPA :-)

                                                                                                Делаю SPA, не в первый раз. Вернее все делаю и UI и api, и базу.
                                                                                                  0
                                                                                                  Кстати рекактивным программированием пользуюсь и там и там.

                                                                                                  Например?


                                                                                                  Я не замечаю, тот же MVC или MVVM например. Теже слои.

                                                                                                  Это вершина айсберга. Я, кстати, использую MV.


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

                                                                                                  В частности, поэтому вы — бэкендер, а я — фронтендер ;-)

                                                                                                    0
                                                                                                    Кстати рекактивным программированием пользуюсь и там и там.
                                                                                                    Например?

                                                                                                    Да например с сокетами можно реактивно работать, с коллекциями и очередями. Да и акторы.
                                                                                                    Но это если вы имеете ввиду то, что описано в reactive manifesto, а не functional reactive programming.

                                                                                                    В частности, поэтому вы — бэкендер, а я — фронтендер ;-)

                                                                                                    Ну зачем же сразу ярлыки вешать)
                                                                                                      0

                                                                                                      Боюсь "reactive manifesto" имеет с "реактивным программированием" из общего только слово в названии.

                                                                              +11

                                                                              Автор, попробуйте пожалуйста разобрать конкретный пример — как сделать редизайн какого-нибудь UI фреймворка, чтобы избежать всепроникающего базового класса Control. Я так понимаю, у вас есть опыт с .NET — предложите вариант принципиальных изменений для Windows Forms или WPF, чтобы там не было иерархий типа https://msdn.microsoft.com/en-us/library/system.windows.controls.button(v=vs.110).aspx :


                                                                              System.Object
                                                                                System.Windows.Threading.DispatcherObject
                                                                                  System.Windows.DependencyObject
                                                                                    System.Windows.Media.Visual
                                                                                      System.Windows.UIElement
                                                                                        System.Windows.FrameworkElement
                                                                                          System.Windows.Controls.Control
                                                                                            System.Windows.Controls.ContentControl
                                                                                              System.Windows.Controls.Primitives.ButtonBase
                                                                                                System.Windows.Controls.Button
                                                                                0

                                                                                Концептуально самый простой способ — замена наследования композицией:


                                                                                1. Выделяем публичный интерфейс Control в тип IControl
                                                                                2. Делаем в своем классе поле типа IControl
                                                                                3. Делегируем ему все члены IControl, которые не переопределяем сами.
                                                                                4. Передаем экземпляр Control как параметр конструктора.

                                                                                Вот только надо ли вам в данном случае лопатить кучу легаси?

                                                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                                                  +3
                                                                                  С одной стороны, это действительно теоретическая проблема, выливающаяся в ряд практических проблем, вроде бинарной совместимости модулей и тп.
                                                                                  С другой стороны, это одно из лучших решений с учётом накладных расходов. Дело не только в уменьшении переписываемого кода, именно наследование позволяет делать как максимально гибкий, так и максимально быстрый в расширении код с наиболее понятными требованиями к программисту-пользователю.
                                                                                  Отказываться от наследования — всё равно что отказываться от промышленного оборудования из-за того, что долбарабочий может при нарушении ТБ покалечить себя до смерти — высший уровень тупизны. Да, каждый язык имеет некоторые антипаттерны и некоторые спорные технологии. Да что там, даже упомянутый Go не обошёлся без критики. Проблема не в том, что они есть и сколько их имеется, а в том, обучены ли программисты работе с этими технологиями. Чаще всего нет, и, что хуже всего, программисты крайне редко несут ответственность за собственный непрофессионализм*. Худшее, что с ними может произойти — переход в другую фирму после разорения предыдущей.

                                                                                  *Нет, это не призыв калечить людей.
                                                                                    –1
                                                                                    Задачи можно эффективнее решать и без наследования :)

                                                                                    Отказаться от наследования — это отказаться от лошади в пользу автомобиля :)
                                                                                      0
                                                                                      Задачи можно эффективно решать и без языков программирования высокого уровня. xor eax, eax

                                                                                      Отказ от ООП в пользу ФП — это переход от автомобиля в пользу летающих тарелок. Очень крутой ход, жаль только топлива для них нет. Вообще, выбор любых техник должен быть осмыслен и логичен. Их набор должен давать максимум выгоды при минимуме затрат. А не следовать моде, вроде перевода всего и вся на микросервисы с 25% потерей производительности.
                                                                                        0
                                                                                        А я разве пропагандировал ФП? :)

                                                                                        Я тоже не ведусь на моду. :)

                                                                                        Собственно, поэтому выступаю против фреймворков в PHP.

                                                                                        ООП — это тоже мода.
                                                                                          0
                                                                                          Была модой до середины нулевых. Уже далеко не мейнстрим. А если почитать статьи на том же хабре за последний год, появилась мода хаять ООП и в хвост, и в гриву, особенно, классические классовые меньшинства. И уходить. Либо в монастырь, либо в Go. Кстати, не удивлюсь, если новая статья автора будет как-раз по этому языку.

                                                                                          И, ладно, когда поднимают какую-нибудь интересную багу реализации того или иного языка или освещают вопросы неправильного использования языка, как ребята из PVS-Studio, но ведь чаще всего появляются статьи «People are dying if they are killed», вроде текущей, которая просто приводит к очередному холивару.

                                                                                          з.ы. А ныть нынче не в моде? Было бы забавно оказаться в тренде.
                                                                                      0
                                                                                      Нет ничего проще, чем делегировать проблему «на уровень выше» тому кто использует класс, инструмент, абстракцию и т. д., а там профессионалы разберутся. Вот с null-ом, кажется большинство здравомыслящих людей ошибку поняло и согласилось, что нужно исправлять, а с наследованием, видимо, история только начинается.
                                                                                      В агитируете за прагматичный подход, а мне кажется, что даже в приведенной вами аналогии стабильность лучше.
                                                                                      Вы предлагаете наращивать квалификацию и дисциплину рабочего и это хорошо, но может быть все-таки в данном случае лучше закупить более дорогое, но безопасное оборудование? Ведь оба подхода решают проблему, но стабильность в долгосрочном периоде лучше.
                                                                                        –1
                                                                                        ИМХО есть небольшая разница между «указать требования для реализации и предоставить реализации по умолчанию» и «делегировать проблему». В качестве примера можно взять те же Qt-шные модели данных, для начала работы с которыми нужно определить 4 метода, но которые, при этом, позволяют полностью изменить практически все аспекты класса.

                                                                                        > Вот с null-ом, кажется большинство здравомыслящих людей ошибку поняло и согласилось, что нужно исправлять
                                                                                        Вы про #define NULL 0? Или про шарповый null? Или про nul? Или про nullptr?
                                                                                        Конечно, каждый год появляется куча интересных техник и лучшие из них обязательно нужно внеедрять в язык и тд и тп, но не забывайте одну мелочь. Сегодня средний по размерам проект на хорошем компе с ssd и 32ГБ оперативы собирается с нуля не меньше получаса, занимая в процессе под полтишок гигабайт места различными кешами. 10-15 лет назад, когда критикуемые вами концепции только появлялись и формировались, на компе был гиг оперативы и 120Гб памяти. Всего. А то и этого не было.
                                                                                        Люди — ужасно прагматичные существа. Если они могли сэкономить на null'ах без особых проблем для кода — они это делали. Если они могут модернизировать свой станок так, чтобы он стал менее травмоопасным, не теряя особо в скорости и не обязывая программистов переписывать ВСЁ (что требуют новые языки), они сделают это.
                                                                                        Что выбрать, новый инструмент, который может стать топовым, а может и не стать, или не устаревающую классику, которая, порой, отправляет тебя на стол хирургам в коробке «Puzzle 4000pts»?.. Я предпочту при таком выборе стать ведьмаком.
                                                                                        0
                                                                                        Дело не только в уменьшении переписываемого кода, именно наследование позволяет делать как максимально гибкий, так и максимально быстрый в расширении код с наиболее понятными требованиями к программисту-пользователю.

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


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

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

                                                                                          0
                                                                                          > Да-да, наследование реализации очень дешево и гибко благодаря сломанной инкапсуляции
                                                                                          Напомните, наследование — это одностороннее отношение «является» («потомок является предком»), да? Скажите, как отношение «является» ломает инкапсуляцию? Никак. Если Вася — человек, то он человек от головы до пят и в нём всё человеческое. Инкапсуляцию ломает не наследование, а приватные поля базового класса, недоступные из потомка, и сокрытые реализации методов. И ограниченность ЯП, которые не позволяют частично модифицировать методы. Последнее, видимо, к лучшему.
                                                                                          Быть может, запретим приватные поля и классы с сокрытой реализацией? Было бы неплохо.

                                                                                          >Наследование интерфейсов в ней — рекомендуется
                                                                                          Я написал «Отказываться от наследования интерфейсов...»? Нет, я написал «от наследования», более общее понятие, несущее в данном контексте ровно единственный смысл. Если для вас нужно писать «от наследования частичной или полной реализации», то уж точно не вам рассуждать о заслуженных мною титулах.

                                                                                          >возможность наследования реализаций — допускается.
                                                                                          Я ещё раз перечитал ваши выводы. Особенно тот, про запечатывание. Это вы называете «допускается»?

                                                                                          Как уже написали ранее, принцип подстановки Лисков — это очень полезное, но, тем не менее, не обязательное для объекта ограничение. Хотя бы по той банальной причине, что порой потомок создаётся именно для нарушения этого принципа.

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

                                                                                            В гранит!

                                                                                              0
                                                                                              В парашу. Вы только что подписались под тем, что ваша статья — мусор.

                                                                                              1) Если проблема в сокрытии реализации методов и полей от наследников, то причина не в наследовании и инкапсуляции, а в проприетарности создаваемого кода.
                                                                                              Этот факт в вашей статье отмечен от слов «никак» и «нигде», а это фундаментальное заблуждение, меняющее всё остальное.
                                                                                              2) Если проблема в проприетарности, то лучшее её решение — опенсорс, то есть наследование от правильно построенных классов открытых библиотек безопасно с точки зрения озвученных вами проблем.
                                                                                              Исходный код открыт, есть история изменения версий в виде дампов изменений, позволяющие мониторить совместимость.
                                                                                              3) Интерфейсы НЕ решают проблемы, а лишь маскируют её, переводя на следующий уровень. Это всё равно, что решить проблему гнилой туши в доме покупкой ароматизаторов, бахил и повязок на глаза.
                                                                                              Проблема интерфейсов в том, что они просто накладывают более мягкие ограничения, нежели базовые классы, имеющие реализацию. Мы говорим, что нам не важно, как всё устроено внутри, пускай просто ведёт себя похоже. В итоге, в том месте, где совмещение функциональности базового класса и неправильного наследника сразу дали бы ошибку, неверные реализации интерфейсов пройдут без проверок, и ошибки придется искать уровнем выше. В интерфейсах нельзя спрятать состояние, в интерфейсах нельзя создать код для автоматической проверки объекта самим собой на корректность и непротиворечивость.

                                                                                              Главное преимущество интерфейсов над полноценными классами-предками — первых куда проще проектировать. Делает ли это их лучше? А делает ли человека лучше отсутствие рук?
                                                                                      +1
                                                                                      Наследование интерфейсов — это иерархическая декомпозиция архитектуры проекта на слой «принципов» (интерфейсов) и «сущностей» (объектов). Наследование «рабочих» неабстрактных классов друг от друга — это чуть более «тупой» DRY «по месту», способ избавления от копипасты путём решения головоломки «как распределить по дереву наследования код, чтобы было меньше дублирования». Как правило, программист в выборе «смысла» классов в Си++ растёт через «стадию DRY» до «стадии выделения интерфейсов», а обсуждения, подобные комментариям под статьёй — столкновения тех, кто уже абстрагировался до интерфейсов, и тех, кто пока ещё классами решает только DRY. ИМХО.
                                                                                        +1
                                                                                        Вот расписали бы (какие-то best practices, логику-идеи, примеры) тогда уж как реализовывать DRY и предотвращать повторение логики без использования частичной имплементации внутри базовых классов. Пока мне кажется очень удобным прописывать общую для всех потомков логику в базовых классах (хотя я и понимаю обозначенную в статье проблему), а отсутствие такой возможности в том же Go несколько пугает. Срочно требуется статья о том, как полюбить interface-only подход и начать жить…
                                                                                          +1
                                                                                          Думаю, применимы одновременно оба подхода, главное чётко себе в этом отдавать отчёт и, например, тестировать куски кода (юниты) ограниченные интерфейсами, а не классами. Раньше слово class и для нэйспейсов использовали, просто складывая туда статические функции. Си++ в этом смысле универсален, на нём можно накрутить самые наихитрейшие схемы декомпозиции кода (конечно, не без издержек) ещё до того, как их поддержку внедрят в стандарт, что и обеспечивает его эволюцию и актуальность (и постоянные терминологические споры :)).
                                                                                            0

                                                                                            Чем вас обычная замена наследования композицией не устраивает?

                                                                                          +3
                                                                                          > Как известно, классическое ООП покоится на трех китах: Инкапсуляция, Наследование, Полиморфизм

                                                                                          Интересно, откуда эта тройка именно в таком виде пошла. Это же какое-то отечественное изобретение (Архангельский?).
                                                                                            0
                                                                                            Я с Шилдта начинал там первые главы тоже были именно об этих трех принципах и что самое удивительное на сколько больше больше было уделено времени наследованию, а о декомпозиции там не было написано, если интересна книга, C# for Beginners 3(2).0, C# For Professionals 2.0, также Ч. Петцольд, название книги не помню, но тоже о шарпе и там был аналогичный тезис.
                                                                                            0

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


                                                                                            Не касаясь теоритических проблем, скажу про практические:


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

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

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

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


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

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

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

                                                                                                  Возможно, вы пропустили или не поняли пять пунктов сразу после заголовка "наследование ломает инкапсуляцию"
                                                                                                  Пожалуйста, посмотрите на простейший пример зависимости наследника от приватных соглашений в базовом классе. Никакой рефлексии.
                                                                                                  https://habrahabr.ru/post/310314/#comment_9815832

                                                                                                    0
                                                                                                    Я говорил лишь о контрактах и его изменениях. В вашем примере с AddRange никто не менял контракт. Здесь другая проблема, связанная не с контрактами, а с необходимостью дополнительных знаний о реализации публичного/защищенного метода. Ее существование я не опровергаю. И действительно, изменение внутренней реализации может негативно сказаться на потомках.
                                                                                                      0

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


                                                                                                      Заголовок спойлера

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


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

                                                                                                      Далее. Чтобы корректно реализовать наследование разработчик должен этот код тщательно изучить и понять.


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

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


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

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

                                                                                                        Также есть абстрактные классы, которые содержат некоторую часть реализации и предлагают реализовать некоторые недостающие методы. Разработчик абстрактного класса предлагает некий «контракт»: я тебе даю вот это, верни мне вот это, я сам тебя вызову. Класс выступает неким фреймворком, мы играем по его правилам, а не заставляем его сделать что-то, для чего его не очень-то и задумывали :) В частности, поэтому в некоторых языках виртуальными являются не все методы, а только те, которые таковыми захотел сделать автор, и поэтому просто нельзя переопределять всё, что вздумается.
                                                                                                          0

                                                                                                          Согласен на 100%

                                                                                              +1
                                                                                              Классу-потомку доступны защищенные члены класса-предка.

                                                                                              private поля не доступны в большинстве языков.


                                                                                              Всем остальным доступен только публичный интерфейс класса.

                                                                                              Уровней доступа вообще говоря больше 2: private (не доступен никому вне класса/модуля), package (не доступен за пределами пакета), protected (доступен только потомкам), public (доступен в любом месте программы), export (доступен даже из вне программы).


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

                                                                                              Этот принцип полиморфизма касается любых типов, а не только объектов.


                                                                                              Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода

                                                                                              Это касается любых случаев "обратного вызова". Виртуальные методы — весьма частный случай.


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

                                                                                              Что такое "сила зависимости"? Зависимость либо есть, либо её нет. Любой способ переиспользования кода создаёт зависимость.


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

                                                                                              Не обобщайте, не при любом изменении, а при ломающем совместимость изменении. Классы тут опять же ни при чём.


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

                                                                                                +2
                                                                                                > private поля не доступны в большинстве языков.
                                                                                                Защищенными обычно называют как раз protected поля.

                                                                                                > Только типичное «я где-то слышал, что наследование — это антипаттерн».
                                                                                                Не просто наследование а наследование одновременно интерфейса и реализации.
                                                                                                  0
                                                                                                  А можно наследовать реализацию без интерфейса этой реализации? :)
                                                                                                    0
                                                                                                    Интерфейс можно, интерфейс и реализацию тоже, так почему бы не наследовать только реализацию?
                                                                                                    В C++ можно точно (private-наследование), в Smalltalk вроде по другому и не бывает, а в остальных случаях остается использовать композицию.
                                                                                                  0
                                                                                                  Что такое "сила зависимости"? Зависимость либо есть, либо её нет. Любой способ переиспользования кода создаёт зависимость.

                                                                                                  Вы правда не в курсе?


                                                                                                  1. Наследование реализации создает зависимость от класса-предка целиком.
                                                                                                  2. Композиция создает зависимость от того же класса, но куда более слабую, так как включает в себя только его публичный интерфейс. Можно спокойно менять детали реализации или использовать наследника вместо базового класса.
                                                                                                  3. Композиция через выделенный интерфейс делает зависимость от класса совсем слабой — одну реализацию интерфейса можно безболезненно заменить другой.

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