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


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


  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. Sirikid
    19.09.2016 05:00
    +1

    Теперь понятно почему по умолчанию все классы в Kotlin sealed а методы final.


    1. js605451
      19.09.2016 06:42

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


      1. Sirikid
        19.09.2016 07:44

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


        1. Bonart
          19.09.2016 11:03
          +1

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


          1. Oxoron
            19.09.2016 11:20
            +1

            Или пары шаблонов в студии.


            1. Sirikid
              19.09.2016 11:39
              +1

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


              1. Oxoron
                19.09.2016 11:54

                Я с IDEA не работал, но разве она не поддерживает написание своих сниппетов?


      1. izzholtik
        19.09.2016 10:15
        +8

        Да-да, а потом разработчики под андроид будут вас проклинать, делая обёртки и рефлексивные костыли.


        1. js605451
          19.09.2016 17:20
          +2

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


      1. Bonart
        19.09.2016 10:39
        +4

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


        1. js605451
          19.09.2016 17:27
          +3

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


      1. sunnee
        19.09.2016 12:18
        +6

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


        1. js605451
          19.09.2016 17:55
          +1

          Меня всегда удивляло стремление отдельных людей выдумывать «правила», а потом героически их преодолевать!

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


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

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


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


        1. justmara
          19.09.2016 22:40
          +1

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


          1. Oxoron
            20.09.2016 09:00

            Я тоже копался в некоторых либах от MS. И знаете, internal там далеко не самое страшное зло.


  1. Antervis
    19.09.2016 05:38
    +3

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

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

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


    1. js605451
      19.09.2016 06:55
      +2

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

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


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


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

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


      1. iCpu
        19.09.2016 07:09
        -6

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


        1. js605451
          19.09.2016 07:34
          +7

          Это называется АОП

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


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

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


          1. iCpu
            19.09.2016 07:41
            -10

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

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


            1. js605451
              19.09.2016 17:34

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


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

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


              1. iCpu
                19.09.2016 22:43

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

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


          1. Saffron
            19.09.2016 08:14

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


          1. alsii
            19.09.2016 10:57
            +3

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


            1. asdf87
              19.09.2016 21:21

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


              1. alsii
                20.09.2016 09:55

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


                1. Bonart
                  20.09.2016 10:07

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


                  1. alsii
                    20.09.2016 10:40

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


                    1. Bonart
                      20.09.2016 10:46

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

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


                      1. alsii
                        20.09.2016 12:21

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


                        1. alsii
                          20.09.2016 12:27

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


                        1. wheercool
                          20.09.2016 18:05

                          Я не автор статьи и не эксперт, но это могло бы выглядеть примерно так


                        1. areht
                          21.09.2016 01:56

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

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


            1. Bonart
              20.09.2016 10:50

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


              1. alsii
                20.09.2016 12:42

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


                1. Bonart
                  20.09.2016 12:51

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

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


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

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


                  1. alsii
                    20.09.2016 13:28

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

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


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

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


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


                    1. Bonart
                      20.09.2016 13:42

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

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


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

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


                      1. alsii
                        20.09.2016 14:47

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

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


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

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


                        1. Bonart
                          20.09.2016 14:53

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


                          1. alsii
                            20.09.2016 15:24

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


        1. areht
          21.09.2016 01:28

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

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

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


          1. iCpu
            21.09.2016 05:00

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

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


            1. areht
              21.09.2016 06:16

              Наследование расширяет функционал предка(вообще тезис… спорный, но пусть), враппер расширяет функционал. А к чему это?

              Если вам надо UndoStack, а он sealed, или буфер не protected, и исходников нет — то жопа.
              Если исходники есть — вытащите стратегию, это типа в 3 клика делается. Код с вашим наследником совпадёт чуть менее, чем полностью.


              1. iCpu
                21.09.2016 07:23

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

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

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


                1. alsii
                  21.09.2016 10:07

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

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


                  1. iCpu
                    21.09.2016 10:45

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

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


                  1. areht
                    21.09.2016 11:19

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


                    1. alsii
                      21.09.2016 11:46

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


                      1. areht
                        21.09.2016 12:06

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

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


                        1. Bonart
                          21.09.2016 12:15

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


                          1. areht
                            21.09.2016 12:20

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

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


                            1. Bonart
                              21.09.2016 12:23

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

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


                              1. areht
                                21.09.2016 12:43

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

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


                                1. Bonart
                                  21.09.2016 12:57

                                  Часто вы в C# перекрываете виртуальные члены Object?

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


                                  1. areht
                                    21.09.2016 13:25

                                    А сравнение и 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. iCpu
                            21.09.2016 13:04
                            +1

                            А у интерфейсов они 100% хорошие, симпотичные, да в туфельках отличных, ага.


                            1. areht
                              21.09.2016 13:34

                              А что не так с интерфейсами?


                              1. iCpu
                                21.09.2016 13:45

                                А что не так с публичными классами?


                                1. areht
                                  21.09.2016 13:46

                                  Отличная от нуля вероятность кривой имплементации.


                                  1. iCpu
                                    21.09.2016 13:51

                                    Отличная от нуля вероятность кривой имплементации. И не подумайте, что я вас передразниваю.


                                    1. areht
                                      21.09.2016 13:54

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


                                    1. Oxoron
                                      21.09.2016 13:56

                                      «Имплементация» интерфейса — фича весьма нехарактерная для dotNet.


                        1. alsii
                          21.09.2016 12:19

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


                          1. areht
                            21.09.2016 12:28

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

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


                            1. alsii
                              21.09.2016 17:05
                              +1

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


                              1. areht
                                21.09.2016 19:25

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

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

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


                                1. alsii
                                  22.09.2016 10:15

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


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


                                  1. areht
                                    22.09.2016 10:44

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

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


                                    1. alsii
                                      22.09.2016 11:10

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

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


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

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


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

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


                                      1. Saffron
                                        22.09.2016 11:43

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

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


                                      1. areht
                                        22.09.2016 12:44

                                        > Но это уже не мои проблемы.
                                        > Я закрыл опасное место

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


                                        1. alsii
                                          22.09.2016 16:07

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


                                          1. areht
                                            22.09.2016 16:17

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


                      1. Bonart
                        21.09.2016 12:20

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


                        1. areht
                          21.09.2016 12:56

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


                1. areht
                  21.09.2016 11:14

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

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

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


                  1. iCpu
                    21.09.2016 11:34

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

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


                    1. alsii
                      21.09.2016 12:08

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


                      1. iCpu
                        21.09.2016 12:14

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

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


                        1. alsii
                          21.09.2016 12:24

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


                          1. iCpu
                            21.09.2016 12:54

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


                            1. alsii
                              21.09.2016 17:19

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


                              1. iCpu
                                21.09.2016 19:28

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

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


                    1. areht
                      21.09.2016 12:12

                      > То есть мы выбрасываем

                      Что?

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

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

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


      1. Sirikid
        19.09.2016 07:36

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


        1. Bonart
          19.09.2016 10:45
          +2

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


      1. Antervis
        19.09.2016 07:38

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


      1. wheercool
        19.09.2016 10:38
        +1

        Если кто не понял, то напишу это же проще. Не используйте «Шаблонный метод», а используйте «Стратегию». :)


        1. Azoh
          19.09.2016 11:07

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


      1. babylon
        20.09.2016 18:00

        «Часто забывают, что ООП — это от слова „объект“, а не от слова „класс“ ».
        Часто забывают, что класс это тоже объект…


        1. Sirikid
          20.09.2016 21:03

          Два чаю! К сожалению, а может и к счастью, это утверждение не всегда верно.


    1. Alexeyslav
      14.01.2017 20:52

      Используй лабораторный БП. Безопасный уровень 2.3В на ячейку аккумулятора, для 6-вольтовых это три ячейки, 12-вольтовые это 6 ячеек. Напряжением выше можно прокипятить аккумулятор.
      Это получается 6.9В для 6-вольтового или 13.8 для 12-вольтового. Для кальциевых правда нужно выше напряжение, надо уточнять. Но их скорей всего вообще бесполезно тренировать — доктор сказал в морг, значит в морг.


      1. Antervis
        19.09.2016 12:13
        +1

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

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

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

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


        1. Bonart
          19.09.2016 12:29
          +1

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

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


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

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


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

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


          1. Antervis
            19.09.2016 12:46

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


            1. Bonart
              19.09.2016 12:54
              +1

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


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

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


              1. Saffron
                19.09.2016 14:04
                +4

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


                1. asdf87
                  19.09.2016 21:31

                  Bonart говорит о гибкости и корректности в сложном случае, а вы — о простом случае. Может быть поэтому вы друг друга не понимаете?


                1. Bonart
                  19.09.2016 23:46
                  -1

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


                  1. Saffron
                    20.09.2016 00:59

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

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

                    Мультинаследование.


                    1. Bonart
                      20.09.2016 01:18

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


                      1. Saffron
                        20.09.2016 02:48
                        +1

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

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

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


                1. Source
                  20.09.2016 00:55

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

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


                  1. alsii
                    20.09.2016 14:58
                    +1

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


                    1. Source
                      20.09.2016 22:12

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


                      1. alsii
                        21.09.2016 10:11

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


                        1. Source
                          21.09.2016 13:11
                          +2

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


                        1. Sirikid
                          21.09.2016 14:11
                          +2

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


                          1. Serg046
                            21.09.2016 17:18

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


                          1. alsii
                            21.09.2016 17:28

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

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


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

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


                            1. Sirikid
                              21.09.2016 20:14

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


                              1. Source
                                21.09.2016 23:14

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


                              1. alsii
                                22.09.2016 10:36

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


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

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


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

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


                                1. vintage
                                  22.09.2016 22:51

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


                                  1. alsii
                                    23.09.2016 11:05

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


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

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


                                    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)


                                    1. vintage
                                      23.09.2016 20:34

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


                                      1. alsii
                                        26.09.2016 09:12

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


                                        1. vintage
                                          26.09.2016 09:39

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


  1. senia
    19.09.2016 06:05

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

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

    Например реализовываете вы собственный AbstractSpliterator. Вы можете реализовать только 1 метод в соответствии с контрактом и вообще не разбираться как оно там работает внутри (а работает оно хитро). Можете реализовать 2 или даже 4, если хотите улучшить производительность. Но даже в этом случае знать вам надо только контракты методов, а не то как они используются.


    1. Sirikid
      19.09.2016 06:34

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


      1. G-M-A-X
        19.09.2016 16:32

        А можно не переопределять, а использовать пустые параметры и что-то вроде func_get_args() (PHP) :)


        1. Sirikid
          20.09.2016 01:12

          В смысле? Я не понимаю при чем тут это.


          1. G-M-A-X
            20.09.2016 11:14

            Тогда скажите, что Вы понимаете под «нарушение контракта методов»? Может я Вас не понял :)


            1. Sirikid
              20.09.2016 12:10

              https://goo.gl/b4Se5e


              1. G-M-A-X
                21.09.2016 10:55

                Тогда я говорил об этом:
                «В объектно-ориентированном программировании контракт метода обычно включает следующую информацию:
                возможные типы входных данных и их значение;»

                Объявив пустые параметры и использовав func_get_args() можно не править принимаемые параметры в наследнике вслед за родителем.


                1. Sirikid
                  21.09.2016 14:25

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


                  1. alsii
                    21.09.2016 17:31

                    1. Меня распнут за такой код :)

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


    1. Bonart
      19.09.2016 11:19

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

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


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

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


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

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


      1. senia
        19.09.2016 11:52

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

        Да, некоторые классы предназначены для расширения, некоторые — нет, а в некоторых об этом просто не подумали. То, что в некоторых случаях могут быть проблемы, еще не значит, что нет безпроблемных случаев. Если класс проектировался для расширения, то в нем ловушек быть не должно (например через private _Add), если не проектировался, то и методы не должны быть виртуальными.


        1. Bonart
          19.09.2016 12:45

          Верно для C#, но не для java >= 8, Rust, scala, kotlin, etc

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


          1. senia
            19.09.2016 14:02

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


  1. sentyaev
    19.09.2016 06:18
    +5

    Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.

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

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

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

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


    1. vvadzim
      19.09.2016 11:28

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


      1. sentyaev
        19.09.2016 12:19

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

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

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

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


    1. Bonart
      20.09.2016 00:37

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

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


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

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


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

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


      1. sentyaev
        20.09.2016 02:12

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

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

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

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

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

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

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

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

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


        1. Bonart
          20.09.2016 11:59

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

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


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

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


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

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


          1. sentyaev
            20.09.2016 12:26

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

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

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

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

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

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


            1. Bonart
              20.09.2016 13:03

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

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


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

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


              1. sentyaev
                22.09.2016 16:30

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

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

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


                1. Bonart
                  22.09.2016 17:38

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

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


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

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


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

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


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

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


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

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


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

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


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

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


                  1. sentyaev
                    22.09.2016 23:33
                    -1

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

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

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

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

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

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

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

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


                    1. Bonart
                      23.09.2016 00:18

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

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


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

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


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

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


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

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


                      1. sentyaev
                        23.09.2016 02:01

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

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

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

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

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


                        1. Bonart
                          23.09.2016 15:17

                          Просмотрел статью и комментарии, но доказательств не нашел.

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


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

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


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

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


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

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


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

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


                          1. sentyaev
                            23.09.2016 15:51

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

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

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

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

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


                  1. Saffron
                    23.09.2016 02:44

                    > Что же вы пишете на TypeScript вместо няшного неограниченного JavaScript?

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


                1. vintage
                  22.09.2016 22:56

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


                  1. sentyaev
                    22.09.2016 23:10

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

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

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

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


                    1. vintage
                      22.09.2016 23:40

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

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


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

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


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

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


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

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


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

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


                      1. sentyaev
                        23.09.2016 00:00

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

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

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

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

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

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

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

                        Делаю SPA, не в первый раз. Вернее все делаю и UI и api, и базу.


                        1. vintage
                          23.09.2016 00:18

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

                          Например?


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

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


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

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


                          1. sentyaev
                            23.09.2016 01:39

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

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

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

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


                            1. vintage
                              23.09.2016 10:18

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


  1. js605451
    19.09.2016 06:36
    +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


    1. Bonart
      19.09.2016 11:26

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


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

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


    1. ggrnd0
      19.09.2016 19:33

      Альтернативный подход к построению UI можно посмотреть в книге:
      Design Patterns: Elements of Reusable Object-Oriented Software (Приёмы объектно-ориентированного проектирования. Паттерны проектирования)
      В ней описываются как сами патерны, так и способы их применения на примере старого текстового редактора.


  1. iCpu
    19.09.2016 06:42
    +3

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

    *Нет, это не призыв калечить людей.


    1. G-M-A-X
      19.09.2016 16:43
      -1

      Задачи можно эффективнее решать и без наследования :)

      Отказаться от наследования — это отказаться от лошади в пользу автомобиля :)


      1. iCpu
        19.09.2016 21:55

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

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


        1. G-M-A-X
          19.09.2016 23:32

          А я разве пропагандировал ФП? :)

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

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

          ООП — это тоже мода.


          1. iCpu
            19.09.2016 23:45

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

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

            з.ы. А ныть нынче не в моде? Было бы забавно оказаться в тренде.


    1. asdf87
      19.09.2016 22:05

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


      1. iCpu
        19.09.2016 23:12
        -1

        ИМХО есть небольшая разница между «указать требования для реализации и предоставить реализации по умолчанию» и «делегировать проблему». В качестве примера можно взять те же Qt-шные модели данных, для начала работы с которыми нужно определить 4 метода, но которые, при этом, позволяют полностью изменить практически все аспекты класса.

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


    1. Bonart
      19.09.2016 23:54

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

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


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

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


      1. iCpu
        20.09.2016 00:48

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

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

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

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

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


        1. Bonart
          20.09.2016 01:19

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

          В гранит!


          1. iCpu
            20.09.2016 04:41

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

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

            Главное преимущество интерфейсов над полноценными классами-предками — первых куда проще проектировать. Делает ли это их лучше? А делает ли человека лучше отсутствие рук?


            1. Sirikid
              20.09.2016 04:59

              del


  1. napa3um
    19.09.2016 09:04
    +1

    Наследование интерфейсов — это иерархическая декомпозиция архитектуры проекта на слой «принципов» (интерфейсов) и «сущностей» (объектов). Наследование «рабочих» неабстрактных классов друг от друга — это чуть более «тупой» DRY «по месту», способ избавления от копипасты путём решения головоломки «как распределить по дереву наследования код, чтобы было меньше дублирования». Как правило, программист в выборе «смысла» классов в Си++ растёт через «стадию DRY» до «стадии выделения интерфейсов», а обсуждения, подобные комментариям под статьёй — столкновения тех, кто уже абстрагировался до интерфейсов, и тех, кто пока ещё классами решает только DRY. ИМХО.


    1. johnnymmc
      19.09.2016 09:47
      +1

      Вот расписали бы (какие-то best practices, логику-идеи, примеры) тогда уж как реализовывать DRY и предотвращать повторение логики без использования частичной имплементации внутри базовых классов. Пока мне кажется очень удобным прописывать общую для всех потомков логику в базовых классах (хотя я и понимаю обозначенную в статье проблему), а отсутствие такой возможности в том же Go несколько пугает. Срочно требуется статья о том, как полюбить interface-only подход и начать жить…


      1. napa3um
        19.09.2016 14:38
        +1

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


      1. Bonart
        19.09.2016 23:55

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


  1. areht
    19.09.2016 09:31
    +3

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

    Интересно, откуда эта тройка именно в таком виде пошла. Это же какое-то отечественное изобретение (Архангельский?).


    1. yara_73
      20.09.2016 03:39

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


  1. Azoh
    19.09.2016 09:46

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


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


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

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

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


    1. Bonart
      20.09.2016 00:04

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

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


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

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


      1. codemax
        21.09.2016 09:50

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


        1. Bonart
          21.09.2016 10:08

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


          1. codemax
            21.09.2016 10:54

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


            1. alsii
              21.09.2016 11:34

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


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

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


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

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


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

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


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


              1. codemax
                21.09.2016 12:09

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

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


                1. alsii
                  21.09.2016 16:52

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


  1. vintage
    19.09.2016 09:52
    +1

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

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


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

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


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

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


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

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


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

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


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

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


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


    1. Sirikid
      19.09.2016 11:31
      +2

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

      > Только типичное «я где-то слышал, что наследование — это антипаттерн».
      Не просто наследование а наследование одновременно интерфейса и реализации.


      1. G-M-A-X
        19.09.2016 16:48

        А можно наследовать реализацию без интерфейса этой реализации? :)


        1. Sirikid
          20.09.2016 01:32

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


    1. Bonart
      23.09.2016 00:32

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

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


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

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


      1. vintage
        23.09.2016 00:50
        -1

        1. В любом случае зависимость будем целиком от того кода, который вы не скопипастили.
        2. Композиция в данном случае — частный случай наследования без виртуальных методов и защищённых свойств. Вас никто не заставляет их использовать там, где не надо.
        3. Тут вы похоже вообще про инверсию зависимости.


        1. babylon
          23.09.2016 10:58

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


  1. Sinatr
    19.09.2016 10:59
    +6

    Задался вопросом, что означает «закопайте стюардессу»:

    … Самолёт терпит крушение над океаном, в живых остаются трое: пилот, помощник пилота и стюардесса, — им удаётся выбраться на необитаемый остров. Через месяц жизни втроём пилот сказал: «Долой разврат!» — и убил стюардессу. Ещё через месяц он опять сказал: «Долой разврат!» — и закопал стюардессу. Ещё через месяц он сказал: «Долой разврат!» — и откопал стюардессу...

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

    Почему ссылка на полиморфизм взята для PHP (который я не знаю)? От фонаря (та статья еще хуже чем эта, начинается с растянутого примера, в конце сухо чуток теории)? Зачем тег C# тогда?

    P.S.: справедливости ради признаюсь — про паблик Морозова не знал, рассмешило слегка.


  1. Shamov
    19.09.2016 12:26
    +3

    «Inheritance is the base class of evil» © Sean Parent :)

    После прочтения статьи складывается такое ощущение, что наследование — это какой-то грязный хак, за использование которого должно быть стыдно. Единственный контекст, в рамках которого можно признаться в его использовании, — это ситуация в стиле: «Здравствуйте! Меня зовут Джон. В своих программах я часто использую наследование реализации, а иногда ещё и защищённые члены.» Если базовый поинт именно такой, то тогда всё правильно. Но проблема в том, что эта базовая предпосылка неверна. Наследование — это не какой-то костыльный трюк, а феномен, реально существующий в мире. Точнее, не в самом мире, а в нашем способе описания этого мира. Да, объекты реального мира не наследуют реализацию друг у друга, даже когда они во многом схожи. Но кого волнуют реальные объекты? Ведь люди мыслят абстракциями. А абстракции очень часто весьма неплохо выстаиваются в иерархию с наследованием общих свойств. Для абстракций такое естественно. Всё, что от программиста требуется, — это правильно реализовать в программе абстракции… с учётом того, что они представляют собой иерархию. Но это его работа, чёрт возьми. Предполагается, что специалист должен уметь выполнять свою работу правильно. А если он не умеет, то значит ему нужно учиться, а не искать другой подход к задаче, который не потребует умения правильно выполнять свою работу. Вряд ли такой подход существует. Скорее всего при другом подходе тоже будут сложности. Просто они будут другими.


    1. alsii
      19.09.2016 13:36

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

      Биологические объекты еще как используют. Даже когда не схожи. Иногда получается забавно.


      1. Shamov
        19.09.2016 13:57

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


        1. alsii
          19.09.2016 15:09

          Это не наследование, а инстанцирование.


          1. Shamov
            19.09.2016 16:21

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


            1. Sirikid
              20.09.2016 01:49

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


              1. Shamov
                20.09.2016 06:56

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


                1. Sirikid
                  20.09.2016 07:14

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


                1. alsii
                  20.09.2016 10:28

                  Годы непрерывных маркетинговых исследования убедительно показывают лишь то, что потребителю нужно ровно то, что показывают годы непрерывных маркетинговых исследований. :) Проще: спрос следует за предложением, а не наоборот. Модуль инициализации может быть установлен на конвейере перед упаковочным оборудование, либо встроем в него. Это, однако, потребует меньших затрат, чем необходимость иметь две разновидности станков для производства левых и правых палочек. Ну и гибкость. Вдруг левые (или правые) палочки перестанут быть нужны? Тогда придется выбростить половину оборудования (в нашем случае кода).


                  1. Shamov
                    20.09.2016 10:51
                    -1

                    Серьёзно подумайте о научной карьере в области экономики. Вы только что опровергли всего Адама Смита. Я не шучу. Вообще полностью. Камня на камне не оставили от его теорий. Он всю жизнь писал по большому счету о двух вещах. Во-первых, о том, что спрос рождает предложение. А, во-вторых, о том, что узкая специализация позволяет радикально снизить издержки. На последнем, кстати, основано всё технологическое развитие последних 300-400 лет. Жаль, что всё это оказалось неверным, и что потребовалось столько времени для того, чтобы понять, что всё ровно наоборот.


                    1. Saffron
                      20.09.2016 11:06

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


                    1. alsii
                      20.09.2016 15:21

                      Адам Смит писал несколько о другом. Он имел в виду, что что если у 10% потребителей появилось электрическое овсещение, значительно более удобное чем керосиновое, то скорее всего остальные 90% тоже его захотят (появится спрос) и рынок ответит расширением производства (появится предложение). Я же имею в виду, что спрос на электрическое освещение не может появиться раньше, чем это освещение будет изобретено и на рынке появятся первые доступные потребителю предложения.
                      Если же под словом "спрос" понимать "платежеспособный спрос", то тут и подавно сначала происходит снижение стоимости на товар/услугу, а затем (и что более важно — в результате) расширение спроса.
                      Про узкую специализацию и издержки А.Смит был несомненно прав в те благословленные времена, когда еще не действовал закон Мура, а модели повседневной одежды не менялись к каждому сезону.
                      Но, мне кажется, это уже выходит за рамки исходной статьи. Посему предлагаю вам при желании ответить на этот мной комментарий и простить меня, если я в свою очередь воздержусь от ответа.


                      1. Shamov
                        20.09.2016 18:32

                        К сожалению, я читал Адама Смита, поэтому вам не удастся запудрить мне мозги мутными фантазиями на тему того, что он якобы писал. Ничего подобного он не писал. Более того, его теория объясняет, как именно нереализованный спрос приводит к появлению изобретений. Не к внедрению, а именно к появлению. Из вашего изложения получается, что изобретение освещения произошло, как бы, случайно. Или если не случайно, то уж точно не из-за влияния экономических факторов. Из теории же Адама Смита следует ровно обратное. Когда Томас Эдисон изобрёл лампочку, он не был отшельником, живущим высоко в горах. Он жил среди людей, чувствовал наличие нереализованного спроса и целенаправленно искал способ его удовлетворить, заработав на этом немножко денежек. И снижение стоимости на товары происходит не само собой и не сначала, а потом. Сначала возникает спрос на более дешёвый товар, который никак не удовлетворяется. И его наличие заставляет людей целенаправленно искать способы снижения цены. Собственно говоря, этот непрерывный процесс как раз и называется «научно-технический прогресс». Об инновациях все очень много говорят, но лишь те, кто внимательно читал Адама Смита, точно понимают, что это такое и откуда берутся.


                        1. alsii
                          20.09.2016 20:05

                          Никаких сожалений. То, что вы знакомы с произведениями А.Смита — это прекрасно.
                          Теперь по существу. Во-первых Эдисон не изобрел собственно лампу. "Заслуга Эдисона была не в разработке идеи лампы накаливания, а в создании практически осуществимой, широко распространившейся системы электрического освещения". В частности он разработал применяемую сейчас конструкцию винтового цоколя, которая позволила пользователям менять перегоревшие лампы самостоятельно, не вызывая техника компании. Но это к слову. Во-вторых ваша интерпретация не выдерживает критики, т.к. она не предполагает появления на рынке предложений, спрос на которые отсутствует. А в реальной жизни таких примеров множество. Более того, полным-полно примеров, когда успешные товары и услуги создавались случайно. Так спрос на табак в Европе появился лишь после того, как его привезли из Америки. Также абсолютно случайно был получен тот же LSD. Более того, некоторые вещи изобретались для одного, но применение нашли совсем другое (оральные контрацептивы). Еще раз повторюсь, многоуважаемый г-н Смит, безусловно написал много очень интересных и полезных вещей, но надо воспринимать критически даже Смита.


                          1. Shamov
                            20.09.2016 22:20

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


                            1. alsii
                              21.09.2016 10:28

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

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


                              1. Shamov
                                21.09.2016 11:15

                                Конкретно в этом Смит абсолютно прав. Те экономические законы, которые реально существуют, действительно объективны. Другое дело, что часто за законы выдаются какие-то случайные совпадения, потому что это очень удобно. И объективные законы отнюдь не предполагают отсутствия исключений. В противном случае пришлось бы исключить большую часть химических законов. В химии редкий закон не имеет исключений. Но это всё вообще неважно, поскольку я вообще не понимаю ваш поинт. Что вы хотите сказать тем, что ставите под сомнение закон спроса и предложения? Сделайте позитивное утверждение. Например. Закон о том, что спрос рождает предложение неверен, но зато верен другой закон: предложение рождает спрос. Хотя я не понимаю, как это может работать. Чем мотивируется человек, который делает что-то не для себя, а заведомо для продажи, если не ощущением того, что на результат его труда уже существует спрос? Просто интересно.


                                1. alsii
                                  21.09.2016 18:12

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


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

                                  Вы удивительно точно сформулировали! Именно ощущением. Ощущением того, что если этот товар появится на прилавке и если цена устроит покупателя то его наверно будут покупать. "Я сказал "наверно"? Да нет же! Наверняка будут! Я в этом уверен!". Ощущение уже есть, а спроса еще нет, т.к. покупатель еще не знает о существовании товара. Как только товар будет предложен покупателю мы узнаем есть спрос или нет.


                                  С другой стороны изменение [уже имеющегося] спроса вызывает изменение предложения. Растет спрос — увеличивается предожение (в количественном и/или ценовом выражении). Падает спрос — уменьшается предложение.


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


                                  1. Shamov
                                    21.09.2016 19:26

                                    Теперь всё понятно. Вы просто неправильно понимаете термин «предложение». Когда товар появляется в рекламе… или делается анонс… или кто-то просто разрабатывает концепт и запускает проект на Кикстартере, это не предложение. Предлагать ещё пока нечего. Это просто наживка, которая забрасывается с целью изучить наличие спроса. На данном этапе работают исключительно маркетологи. Производственная фаза в этот момент в лучшем случае лишь спланирована в общих чертах… и то не факт. Для того, чтобы появилось предложение, нужно, во-первых, запустить производство, а, во-вторых, наладить систему сбыта. Товар должен стать реально доступен для покупки и немедленного получения. Предзаказ не считается. Это тоже маркетинговый инструмент. Кстати, маркетологи как раз и занимаются выяснением наличия спроса и изучением его структуры. В том числе и в условиях полного отсутствия предложения. Это один из кейсов, с которым они умеют работать. Так что не так уж это и невозможно, как вы думаете. Более того, в этом нет ничего сложного. Маркетологи просто делают официальные анонсы в СМИ. Или организовывают «утечки» типа «а в новом айфоне не будет миниджека», чтобы понять, как публика отреагирует на такой поворот. Причём задолго до того, как первый айфон без миниджека сойдёт с конвейера.


                                    1. alsii
                                      22.09.2016 10:52

                                      А я с вами и соглашусь и не соглашусь. Спрос возникает тогда, когда пользователель узнает о новом товаре и о его цене (цена важна!). Или не возникает. Он видит рекламный плакат и думает: "Новая модель бурбулятора всего за миллион мегакредитов? Потребляет в три раза меньше квинтессенции, но с удвоеной частотой бурбуления? Да еще в моем любимом серо-буро-малиновом цвете? Надо брать!". И для него не важно, что доступен только предзаказ, он уже хочет ее купить и готов платить. И он отложит этот миллион, и не потратит его ни на что другое. Это спрос.
                                      Все, что делают маркетологи — это действительно лишь исследования. Я собственно об этом и говорил. Результаты исследований имеют некоторую степень достоверности. Иногда прогнозы сбываются, иногда нет. Главная проблема в том, что до сих пор неизвестно как человек принимает решения о покупке. Нет достаточно достоверной "модели покупателя". Поэтому пока мы слушаем маркетологов — это предположения о возможном спросе. Когда мы пледлагаем товар для продажи — это факт.


                                      1. Shamov
                                        22.09.2016 11:53

                                        А в чём несогласие-то? Я именно об этом и говорю. Предзаказ — это просто способ исследования уже существующего спроса. Количество предзаказов и динамика их размещения определяют итоговые параметры того предложения, которое поступит на рынок через какое-то время.

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


  1. Codenamed
    19.09.2016 14:18
    +6

    У автора некорректна сформулирована исходная проблема:

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

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

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

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


    1. Bonart
      19.09.2016 17:03

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

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


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

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


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

      Да неужели? Нарушающий принцип Лисков сразу:


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

      В большинстве случаев это получается само и бесплатно.
      Разве что в ваших предположениях.

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

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


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

      Очень практично:


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

      "Абстрактные интерфейсы" повеселили отдельно.


      1. Codenamed
        19.09.2016 17:31

        Коллега, а где в приведенном вами же примере нарушение инкапсуляции — сокрытия данных и реализации от пользователя объекта?

        Собственно, откуда вы вообще взяли, что от класса-наследника надо скрывать детали реализации или лишать его доступа к данным?


        1. Bonart
          19.09.2016 18:21

          Коллега, а где в приведенном вами же примере нарушение инкапсуляции — сокрытия данных и реализации от пользователя объекта?

          А вы приведите свою реализацию наследника — покажу.


          Собственно, откуда вы вообще взяли, что от класса-наследника надо скрывать детали реализации или лишать его доступа к данным?

          Надо полагать, из названия и назначения уровня доступа private.


          1. Codenamed
            19.09.2016 20:02

            Для того, чтобы правильно реализовать такого наследника (представим себе, что нам это действительно необходимо), нужно знать, как работают эти методы в классе-предке (например, не использует ли в базовом классе AddRange() вызов Add()). От этого зависит реализация наследника. Это нормально.

            Из существование модификатора доступа private следует возможность скрывать члены базового класса от наследников. Но никак не следует необходимость это делать.

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


            1. Shamov
              19.09.2016 22:00
              +1

              Если после такого никто не скажет про хрупкость базового класса, то я даже не знаю…


              1. Bonart
                20.09.2016 00:18
                +1

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


                1. Shamov
                  20.09.2016 08:13

                  Мне-то всё понятно. Но, к сожалению, я не могу засчитать хрупкость базового класса или даже всей иерархии как недостаток. В противном случае придётся признать, что строительство тоже идёт не тем путём. Потому что там имеются ровно те же проблемы. Когда 30-этажное здание уже построено, нельзя просто так взять и поменять что-то в фундаменте. Но все изначально об этом знают. И сразу проектируют фундамент таким образом, чтобы не нужно было его изменять. Казалось бы, альтернатива очевидна: можно строить 2-3 этажные дома из дерева. В случае необходимости их можно приподнять на домкратах и пофиксить фундамент. Можно даже полностью заменить на другой. Гибкость потрясающая. Этажи никак не зависят от фундамента. Могут стоять на всём, что предоставляет интерфейс фундамента. Но вот только 30-этажное здание на таких принципах не построишь. К счастью, строители смогли преодолеть в себе тягу к гибкой архитектуре и стали предварительно проектировать жесткую, сознательно отказавшись от возможности полной переделки зданий после постройки. Благодаря чему мы сегодня живём в больших городах, в комфортабельных квартирах с централизованным водо-/тепло-/энергоснабжением, а не в деревянных домах с туалетом на улице.


                  1. red75prim
                    20.09.2016 09:14
                    +1

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


                    1. Shamov
                      20.09.2016 13:26

                      Cмотря где. Когда вы разрабатываете на переднем крае новых технологий, когда ваш стартап триумфально ворвался на рынок, хотя сам ещё пока не знает зачем, и когда есть риск того, что разрабатываемый вами интернет-магазин в какой-то момент потребуется за неделю переделать в мессенджер, тогда да… гибкость архитектуры должна быть абсолютным приоритетом. Но не все же так работают. Большинство разработок, которые в конечном итоге всё-таки внедряются и приносят реальную прибыль, происходят в консервативных отраслях. Люди с самого начала знают, что именно они делают. Среди них есть архитекторы, которые всю жизнь только и занимаются проектированием архитектуры в определённой предметной области. Архитектура у них сразу получается довольно неплохой. И требования к программам меняются настолько медленно, что за тот срок, пока их накопится заметное количество, можно спокойно разработать новую архитектуру с нуля и реализовать её в коде.


            1. Bonart
              20.09.2016 00:14

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


              Из существование модификатора доступа private следует возможность скрывать члены базового класса от наследников. Но никак не следует необходимость это делать.

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


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


              1. Как вы реализуете те же методы в таких условиях?
              2. Будут ли они зависеть от деталей реализации базового класса?
              3. От какой опасности вас защитил разработчик, запечатавший базовый класс?


              1. Codenamed
                20.09.2016 11:35

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

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

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

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

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


                1. Bonart
                  20.09.2016 11:49

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


      1. bfDeveloper
        19.09.2016 17:53

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

        Никак. По крайней мере в продакшн коде. Если надо будет мокнуть для теста, то переопределяются Add и AddRange и складываются. Требует знания о связи этих методов, не зовут ли друг друга, но тут мы возвращаемся к первому ответу: никак.
        Возможность наследования — часть интерфейса класса и, внезапно, его тоже надо проектировать. А если вы используете наследование для хаков против воли автора класса, то сами себе злобный Буратино. Вы ещё скажите, что package(или internal, только для этого пакета) область видимости это плохо и нарушает инкапсуляцию.
        А вот проектировать под наследование многие действительно не умеют, а потом наезжают на ООП.


        1. Bonart
          20.09.2016 00:23

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

          Еще более внезапно — в статье об этом прямо написано.
          Вот только проектировать под наследование интерфейсов проще и безопаснее. И, что характерно, без всяких наездов на ООП.


      1. Serg046
        19.09.2016 17:55

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

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

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


        1. Serg046
          19.09.2016 18:05
          +1

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


          1. Bonart
            20.09.2016 00:25

            Это всего лишь очень простой учебный случай.


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

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


            1. Serg046
              20.09.2016 14:14

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

              Да, а есть и реальный.
              Например довольно часто вижу подобное, не вижу и не испытываю проблем с подобным:


              class Base
              {
                public void Execute()
                {
                  if (!Validate())
                    throw new ...    
                }
              
                protected virtual bool Validate() => true; // еще лучше abstract, чтобы совсем без претензий к lsp (но формальный контракт и так все объясняет нормально, в отличие например от ICollection.Add())
              }

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


              1. Bonart
                22.09.2016 10:16

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


                1. iCpu
                  22.09.2016 10:37
                  +1

                  О, это же интерфейсы!


      1. alsii
        19.09.2016 19:52
        +1

        Простите за дикий псевдокод...
        class Storage
        {
           public add(Item item) {...}
           public addRange(ItemRange itemRange) {}
        }
        
        class StorageWithCounter
        {
          private int count = 0;
        
          public add(Item item)
          {
            parent.add(item);
            count++;
          }
        
          public addRange(ItemRange itemRange)
          {
            parent.addRange(itemRange);
            count += itemRange.count();
          }
        
          public count()
          {
            return count;
          }
        }


        1. red75prim
          19.09.2016 20:07
          +1

          Там выше был намек что addRange может вызывать add.


        1. Serg046
          19.09.2016 20:09
          +1

          Зря код методов Storage не добавлен. Что если в addRange внутри себя пользуется add? Ведь это вполне может быть. Это обязательное знание при реализации StorageWithCounter, к сожалению.


          1. alsii
            20.09.2016 10:45

            Ээээ… Минуточку, а чем это повредит? Пусть Storage.addRange вызываетStorage.add. Это никак не помешает правильному счету.


            1. Sirikid
              20.09.2016 11:11

              А вот чем: http://ideone.com/72DWTf


              1. alsii
                20.09.2016 11:52

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


              1. alsii
                20.09.2016 12:12

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


                1. Sirikid
                  20.09.2016 12:24

                  Мне нравится, особенно если можно добавлять пачку элементов быстрее чем в цикле.
                  Товарищ Bonart, ознакомьтесь пожалуйста.
                  (Кстати, когда я писал этот код IRL целью было минимализация кол-ва абстрактных методов в интерфейсе, как бы это странно не звучало.)


                1. Bonart
                  20.09.2016 12:44

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


                  1. iCpu
                    20.09.2016 13:09

                    А этот? http://ideone.com/7d4DDJ
                    Мне очень интересно, как вы при ровно одинаковых ограничениях на интерфейс?


                    1. iCpu
                      20.09.2016 13:15

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


                      1. Sirikid
                        20.09.2016 13:34

                        http://ideone.com/SsQkzm
                        Подходит?


                        1. iCpu
                          20.09.2016 13:51

                          Учитывая, что это калька с моего решения — да. Однако это изначально игра в одни ворота, Для наследования реализаций было бы корректно ставить конкретные задачи, ведь наследуется конкретный класс. Пусть это будет Множество (Set).


                          1. alsii
                            20.09.2016 14:44

                            Мне кажется особого смысла нет дальше экспериментировать. Недостатки паттерна декоратор ясны: придется декорировать все методы из реализуемых интерфейсов. Но тут уже отмечалось, что в таких случаях помогают кодогенераторы и т.п.
                            Вообще интересно поразмыслить, возможно ли инкорпорацию реализовать на уровне языка. Что-то вроде: class Child incorporates Parent as parent with ParentInterface { ... } и тогда все методы из интерфейса ParentInterface не определенные в Child заменялись бы на вызовы соответствующих методов из Parent и неявно определялось бы private Parent parent;. Такой вот сахарок...


                            1. Bonart
                              20.09.2016 15:29

                              Для дотнета можно использовать какой-нибудь динамический прокси:
                              https://github.com/kswoll/sexy-proxy


                              1. iCpu
                                20.09.2016 15:50

                                И эти люди запрещают мне ковырять в носу ©


                            1. Sirikid
                              20.09.2016 17:33
                              +1

                              В Kotlin все это есть из коробки: https://goo.gl/4tIwPE


                              1. alsii
                                20.09.2016 18:27

                                О! Я ждал чего-то подобного :) Спасибо!
                                p.s. синтаксис занятный. Все откладывал знакомство, но теперь, пожалуй не удержусь.


                                1. Sirikid
                                  20.09.2016 21:12

                                  Будьте осторожны, вызывает привыкание :)


                                1. vintage
                                  20.09.2016 21:13

                                  В D тоже есть автоделегирование: http://ideone.com/B5rgw7


                              1. Bonart
                                20.09.2016 18:50

                                Отлично — здесь даже кода по сравнению с наследованием больше ровно на одно поле.


                            1. guai
                              21.09.2016 01:32

                              для явы есть Delegate в project lombok


                              1. alsii
                                23.09.2016 11:22

                                Поигрался с Kotlin. Попробовал делегирование от нескольких "базовых" классов — работает. Попробовал он нескольких базовых классов с совпадающими методами. Ругается и просит сделать явное делегирование.


                                Error:(20, 0) Class 'Derivative0' must override public open fun test(): Unit defined in Derivative0 because it inherits many implementations of it

                                Сделал — работает. И делегирование делается в одну строчку. Очень неплохо.


                1. alsii
                  20.09.2016 12:55

                  Ну я сразу отметил, что метод не честный. Хотя на многопоточке и метод с композицией сломается, как мне кажется. И там и там придется думать о синхронизации. Bonart, доработайте этот код, пожалуйста, чтобы получился пример с композицией. Будут хорошо видны преимущества/недостатки.


                1. mobi
                  20.09.2016 12:57

                  Это работает, потому что вы знаете реализацию в предке. А если бы там add() был реализован через создание массива с одним элементом и вызовом addRange()? А если в addRange() может добавиться только половина элементов, а потом будет выкинуто исключение (a la std::bad_alloc)? К сожалению, универсального решения здесь нет.


                  1. alsii
                    20.09.2016 13:53

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


      1. Sirikid
        20.09.2016 02:04

        > Как вы реализуете эти методы в классе-наследнике?
        Лично я сделал AddRange через for и Add в абстрактном классе и явно описал это в документации. Теперь все реализации AddRange должны вести себя так, как будто внутри у них цикл.


        1. Bonart
          20.09.2016 11:27

          Это устранение хрупкости за счет потери гибкости.
          Решение с наследованием интерфейса и декоратором такого недостатка не имеет.


          1. alsii
            20.09.2016 13:01

            Ну вообще, документируя public/protected метод, хорошим тоном будет упомянуть возможные побочные эффекты. Ну хотя бы дать список вызываемых public/protected методов. Если рассматривать это как часть спецификации, то проблем быть не должно. Или я что-то еще упускаю?


            1. Sirikid
              20.09.2016 13:40

              Плохо то, что контракт addRange стал строже а реализация стала частью интерфейса.


            1. Bonart
              20.09.2016 15:26

              Вызов или невызов Add из AddRange (и наоборот) является "побочным эффектом" только из-за поломки инкапсуляции при наследовании реализации.
              При использовании только публичного интерфейса никаким "побочным эффектом" даже не пахнет.


              1. alsii
                20.09.2016 16:56

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


              1. alsii
                20.09.2016 19:08
                +1

                Вот, совершенно случайно наткнулся:


                Effective Java, Programming Language Guide — Joshua Bloch; Forward By Guy L.Steele, Jr. May 2001
                Item 17: Design and document for inheritance or else prohibit it
                First, the class must document precisely the effects of overriding any method. In other words, the class must document its self-use of overridable methods. For each public or protected method or constructor, the documentation must indicate which overridable methods the method or constructor invokes, in what sequence, and how the results of each invocation affect subsequent processing. (By overridable, we mean nonfinal and either public or protected.) More generally, a class must document any circumstances under which it might invoke an overridable method. For example, invocations might come from background threads or static initializers.


                1. Bonart
                  20.09.2016 19:38

                  Ну вот, еще в 2001 году ява-гуру все было понятно.
                  Сдается мне, среди комментаторов, яростно защищавших наследование реализации, найдется максимум один, поступающий в соответствии с этими рекомендациями.
                  А у наследования интерфейсов появляется еще один плюс — экономия на лишней документации.


      1. Antervis
        21.09.2016 06:56

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

        void add(arg_t arg) { Base::add(arg); _counter++; }
        

        И по аналогии с addRange.
        Очень практично:

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

        «Абстрактные интерфейсы» повеселили отдельно.

        А что, наследование как инструмент гарантирует сбои, плохо спроектированную иерархию типов и недобросовестных разработчиков?


        1. Bonart
          21.09.2016 10:19

          А что, наследование как инструмент гарантирует сбои, плохо спроектированную иерархию типов и недобросовестных разработчиков?

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


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


  1. G-M-A-X
    19.09.2016 16:55

    То есть ООП — ерунда (я не адепт ФП, если что :) ).

    Весь этот пафос по поводу повторного использования кода, и 3-ех китов в жизни — ерунда. :)

    Свой код можно как угодно писать. Можно все на интерфейсах.

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

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

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

    А также лучше предусмотреть события в базовом классе, чтобы не нужно было городить огород из ООП-наследования.
    Подписался на события и имеешь профит :)

    А можно использовать декоратор с __call() (PHP): можем скопом добавить поведение всем методам, можем для какого-то метода добавить другое, можно вообще переопределить.

    А также вопрос:
    Стоит ли использовать фреймворки, где в основном все реализовано, а не интерфейсы (PHP)? :)

    Другие примеры дурного использования ООП http://blog.kpitv.net/article/ооп-может-способствовать-лапше-трудному-пониманию-кода-15417/.


  1. vyatsek
    19.09.2016 18:47
    -1

    Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома — антипаттерн Паблик Морозов;
    Только класс потомок это не какой-то сторонний класс, а класс который является подтипом суперкласса. Что такое тип к Бертрану Мейеру, у него хорошо описано.

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

    Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку;
    не всем, а контракту, включая public, internal, protected.

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

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


  1. OlegGelezcov
    20.09.2016 00:26

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


    1. Bonart
      20.09.2016 00:30

      Вообще-то повторное использование кода при наследовании имеет два варианта использования:


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


      1. Antervis
        20.09.2016 13:14

        Вот возьмем c++. Где ломается инкапсуляция от наследования? private члены недоступны наследникам. protected — доступны наследникам, но недоступны как публичный интерфейс.


        1. Bonart
          20.09.2016 13:20

          Почитайте комментарии, начиная со ссылки:
          https://habrahabr.ru/post/310314/#comment_9815832


          1. iCpu
            20.09.2016 13:43

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

            Продолжая тем, что выдуманный вами пример не корректен в силу того, что наследование работает не так, как написали вы, «сделай мне слона из мухи», а из конкретной реализации, о которой класс-наследник обычно имеет значительный объём информации. Когда я наследуюсь от хеш-таблицы, я знаю, как ведут себя ВСЕ публичные методы. Что не скажешь об интерфейсах iContainer.

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

            Вот вам задачка. Нужно сделать хеш-таблицу, в которую нельзя было бы вставлять элемент с одним и тем же ключём дважды за всё время жизни таблицы. Мне нужно переопределить только методы вставки и удаления, примерно 15-20 строк всего. (Плюсы портят статистику из-за operator[], но не значительно) Вам?


  1. izobr
    20.09.2016 01:57

    На практике нельзя обойтись одними интерфейсами. Идеальный интерфейс состоит из одного метода. Если методов больше, то голый интерфейс не выражает отношений между этими методами. Взять хотя бы ICollection из C#: сам интерфейс не гарантирует, что если мы вызвали Add(), то Count изменился на единицу или, что Contains() найдёт добавленный элемент. Автогенерённая имплементация со всеми throw new NotImplementedException() уже удовлетворяет интерфейсу и до рантайма никаких ошибок не обнаружится.

    Так мы приходим к Контрактам. Для. Net есть Code Contracts. Интерфейс и контракты — это уже лучше, чем просто интерфейс, но Контракты заставляют всех имплементаторов повторять часть кода, которая отвечает за поддержание контракта.

    Тогда становится логичным вместо интерфейса дать абстрактный класс, который гарантирует выполнение контракта, где вся изменяемая часть представлена protected abstract методами, а все остальные методы sealed. Можно и интерфейс оставить, для тех, кому абстрактный класс жмёт, но в 99% случаев можно обойтись классом, а не интерфейсом, если это не так, то абстрактный класс просто плохо сделан.

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

    Таким образом, на практике, я рекомендую абстрактные классы с только sealed и abstract методами и интерфейсы с одним методом.

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

    При наследовании интерфейсы, в большинстве случаев, выражают всякие ability (и, зачастую, имеют суффикс able, e.g. IDrawable, IEquatable), а абстрактные классы определяют существо объекта, его реальную принадлежность к категории объектов. Интерфейс с этим не справляется т.к. если мы не обвяжем его контрактами он не гарантирует, что наследник обладает нужным поведением.

    Собственно это всё было к пункту 2:

    Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.


    Все пункты в теоретическом решении, кроме пятого, верны, но на практике, вместо пункта 2, соблюдайте open-closed principle и тогда вам нечего боятся наследования от абстрактного класса, который содержит реализацию, отвечающую, за выполнение того, что мы априори ожидаем от этого объекта. Это зачастую лучше голого интерфейса т.к. он не гарантирует отношений между своими членами и все наследники должны повторять этот код с нуля.


    1. Bonart
      20.09.2016 11:39

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


      1. izobr
        20.09.2016 14:19

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


        1. Serg046
          20.09.2016 14:38

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

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


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


  1. alz72
    20.09.2016 12:58

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

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


  1. stychos
    20.09.2016 20:39

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

    Почему? Есть же не только protected, но и private. Или я что-то не понимаю (мои знания о С++ в основном поверхностные).


    1. Bonart
      21.09.2016 00:21

      Логика разъяснена в статье, учебный пример — смотрите по ссылке ниже
      https://habrahabr.ru/post/310314/#comment_9815832


      1. stychos
        21.09.2016 11:53

        Честно говоря, не совсем понял, в чём там проблема — разве нельзя переопределить Add, и в нём вызывать parent::Add(), инкрементируя счётчик вызовов? — Где тут нарушение инкапсуляции?


        1. Bonart
          21.09.2016 12:26

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


          1. stychos
            21.09.2016 12:31

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


            1. Bonart
              21.09.2016 12:55

              Видимо совсем тупенький — всё равно не понимаю, зачем это надо знать

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


              посчитать количество элементов перед вызовом предка

              И огрести от многопоточности.


              В любом случае, если мы наследуем какие-либо публичные/защищённые методы, мы ведь должны понимать, как они работают.

              Это и есть поломка инкапсуляции. Используя объект я должен знать что он делает, но не должен как.


              1. iCpu
                21.09.2016 13:10

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


                1. Serg046
                  21.09.2016 17:21

                  Если так, то тогда зачем модификатор private? Хватило бы protected тогда.


                  1. iCpu
                    21.09.2016 19:36

                    За исключением редких случаев, когда нужно защитить некоторые поля, вроде ooid, от изменения? Не все же хотят делиться своей бизнес-логикой. Спрятал все данные класса в private структуру, её определил в cpp-файле, в классе только указатель. Всё работает, но как — совсем не понятно.


                    1. Serg046
                      21.09.2016 19:39

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


                      1. iCpu
                        21.09.2016 19:51

                        Если вы о «редких исключениях», то это скорее внутренняя защита от дурака. К примеру, meta-информация о классе не должна быть доступна для изменения пользователем и при этом она не обязательно константа. Технически, она не является частью класса, потому вопрос реализации их связи очень сложен и имеет разные варианты решения. В ряде случаев, приватное наследование — одно из наиболее оптимальных.


                        1. Serg046
                          21.09.2016 19:55

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


                          Но и все-таки, даже останавливаясь на плюсах, если бы это был такой уж редкий кейс, не было бы private, хватило бы ptotected и код ревью.


                          1. iCpu
                            21.09.2016 20:23

                            Во многих языках нет множественного наследования. Это ещё ничего не значит.


              1. stychos
                21.09.2016 13:10

                вы рискуете учесть некоторые добавленные элементы дважды

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

                И огрести от многопоточности.

                Подробнее, пожалуйста.

                Используя объект я должен знать что он делает, но не должен как.

                Под словами «как они работают» я и имел в виду то, что они сделают с данными и какой вернут результат, а не как именно они работают внутри. Если AddRange предка, например, вызывает Add предка, и не добавляет при этом уже ранее добавленные любым из способов элементы, то об этом же должно быть указано в доках к методу, иначе получается undefined behavior какой-то, который и безо всякого наследования сложно понимать и тестировать — он ведь должен сообщить, что он сделал с данными — либо вернуть количество успешно добавленных, либо их коллекцию.


  1. vcooking
    21.09.2016 00:18

    Я пользуюсь ООП (не путать с PLO) так:

    Есть определённая жёстко заданная логика — её реализуют публичные классы:

    public 
    function GetOne;
    function GetTwo;
    


    Есть абстрактные методы, на место которых будет подставлены методы дочерних классов:

      public 
      procedure Open; abstract;
      function TakeOne : type; abstract;
      function TakeTwo ; type; abstract;
      procedure Close; abstract;
    
    function MyBaseClass.GetOne : type;
    begin
      Open;
      result := TakeOne;
      Close
    end;
    
    function MyBaseClass.GetTwo;
    begin
      Open;
      result := TakeTwo;
      Close;
    end;
    


    Каких-либо граблей в таком подходе не видно (кроме Abstract Error).Логика (открыть-взять-закрыть) прописана в базовом классе, хотя детали реализации необходимо реализовать в дочерних. Если нельзя использовать базовый класс для описания логики — то нахрена об вообще нужен?


    1. alsii
      21.09.2016 11:02

      Коллеги, проверьте пожалуйста следующую формулировку:


      Неприватные (public, protected, etc.) методы классса-предка не должны вызывать методы, которые могут быть перекрыты при наследовании.

      Или все еще более жестко:


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

      Это попытка сформулировать некие "правила безопасного наследования". Или я ломлюсь в открытую дверь?


      1. Bonart
        21.09.2016 12:29

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


        1. alsii
          21.09.2016 18:19

          Ok. Тогда попробую так:


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


  1. Bonart
    21.09.2016 00:19

    Если нельзя использовать базовый класс для описания логики — то нахрена об вообще нужен?

    Для описания контракта.


    1. Antervis
      21.09.2016 07:05

      итого несколько контрактов и несколько исполнителей представляют собой одну программную сущность


  1. msts2017
    21.09.2016 13:04

    по поводу Add и AddRange
    обычно такие задачи решают так

    http://ideone.com/fxPGq5


    1. Bonart
      21.09.2016 13:08

      Многопоточности не бывает?


      1. msts2017
        21.09.2016 13:27

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


        1. msts2017
          21.09.2016 15:46

          потокобезопасный вариант
          http://ideone.com/VUG3Tg
          зыЖ написано «отбалды» на яве не пишу.


          1. iCpu
            21.09.2016 16:38

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


            1. msts2017
              21.09.2016 17:21

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


              1. Serg046
                21.09.2016 17:34

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


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


                1. msts2017
                  21.09.2016 17:43

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

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


                  1. Bonart
                    21.09.2016 19:01

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

                    Особенно если предок из чужой библиотеки.


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

                    Вы точно читали статью? В одной процитированной фразе три ложных утверждения.


                    1. В статье показывается непротиворечивость ООП — наследование интерфейсов не ломает инкапсуляцию.
                    2. Задача легко решается наследованием интерфейса,
                    3. Решение наследованием реализации требует знания приватных деталей реализации предка (сломана инкапсуляция).


                    1. Serg046
                      21.09.2016 19:09

                      Вы где-то нас обманываете.


                      msts2017 пишет:


                      По задаче нельзя менять предка

                      Здесь вы соглашаетесь


                      Особенно если предок из чужой библиотеки.

                      А следом, внезапно...


                      Задача легко решается наследованием интерфейса,

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


                      1. red75prim
                        22.09.2016 06:24

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

                        А что тут может быть сложного? https://play.rust-lang.org/?gist=27418cdffe1bbb45bde4c7d3058ec162&version=stable&backtrace=0


                        1. iCpu
                          22.09.2016 07:55

                          Получить гарантии.
                          https://play.rust-lang.org/?gist=27418cdffe1bbb45bde4c7d3058ec162


                          1. red75prim
                            22.09.2016 08:25

                            Хм. То есть код msts2017 http://ideone.com/lh56Ds, большую часть которого составляет борьба с унаследованным поведением, даёт больше гарантий? Это уже смешно.


                            1. iCpu
                              22.09.2016 09:53

                              Да нет, у него тоже бред, потому что он попытался руками сделать мутекс. Вполне подходящие экземпляры для аксиомы Эскобара.

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


                              1. red75prim
                                22.09.2016 11:33

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

                                Можно было и не писать реализацию по умолчанию, а реализовать add_range непосредственно для CollectionImpl. Это ничего бы не изменило. Точнее код бы стал немного менее самодокументированным. Но в документации к интерфейсу всё равно пришлось бы писать, что результат вызова add_range должен быть эквивалентен последовательному вызову add для всех элементов (от начала к концу / от конца к началу / в порядке определяемом реализацией).


                                Далее нигде не используется тот факт, что add_range реализован с помощью вызова add.


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

                                Единственный, кто может контролировать поведение реализаций, — это программист, который их пишет, независимо от того наследование это или композиция.


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


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


                                1. iCpu
                                  22.09.2016 11:45

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

                                  > Далее нигде не используется тот факт, что add_range реализован с помощью вызова add.
                                  >> fn add_range(&mut self, vs: &[u32]) {
                                  >> self.cnt.fetch_add(vs.len(), Ordering::SeqCst);
                                  А что, если добавляются только чётные? Или если коллекция может содержать только уникальные элементы? Или в Add есть ещё какая-нибудь проверка?
                                  Эти же вопросы относятся и к виртуальным методам при наследовании. Ничем, кроме уровня доступа ко внутренностям, они не отличаются.

                                  Всё остальное — суета.


                                  1. red75prim
                                    22.09.2016 12:39

                                    А что, если добавляются только чётные? Или если коллекция может содержать только уникальные элементы? Или в Add есть ещё какая-нибудь проверка?

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


                                    Если нужно сделать единую реализацию CountedCollection для любой реализации Collection, то очевидно, что такая задача не решается. Но и наследование здесь ничем не поможет, если неизвестно как реализуются add и add_range в базовом классе.


                                    1. Bonart
                                      22.09.2016 12:50

                                      Если нужно сделать единую реализацию CountedCollection для любой реализации Collection, то очевидно, что такая задача не решается

                                      Решается через наследование интерфейсов.
                                      "Только четные" — нарушение условий публичного контракта.


                                      1. iCpu
                                        22.09.2016 13:02

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


                                      1. Serg046
                                        22.09.2016 13:47

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


                                    1. iCpu
                                      22.09.2016 12:56

                                      Вооот. Наконец до вас дошло! ISerializable гарантирует только наличие методов, но не их поведение. У вас нет гарантий, что A == unserialize(serialize(A)). То же, в принципе, верно и для виртуального метода, но мы, как минимум, можем оставить тесты, которые нельзя не пройти.


                                  1. red75prim
                                    22.09.2016 12:44

                                    Ничем, кроме уровня доступа ко внутренностям, они не отличаются.

                                    А как же отсутствие проблем с хрупкостью базового класса?


                                    1. iCpu
                                      22.09.2016 12:49

                                      А какая разница? Ваш код упадёт И в случае неверной стратегии, И в случае неверной перегрузки. Ваш код упадёт И в случае неверной реализации интерфейса, И в случае неверного наследования. В чём хрупкость-то?


                                      1. red75prim
                                        22.09.2016 13:11

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


                                        Ладно. Пожалуй хватит, уже по пятому кругу пошли.


                                        1. iCpu
                                          22.09.2016 13:26

                                          Эта же проблема возникает и при агригации, и при добавлении стратегий. Да что там, это абсолютно верно для всего программного кода в мире. Иначе бы Wine не был реализацией «баг в баг».


                                          1. red75prim
                                            22.09.2016 13:38

                                            Несложно предоставить новый интерфейс в дополнение к обратно-совместимой по багам версии. Предоставить новый базовый класс в дополнение к старому как-то уже нетривиально.


                                            1. iCpu
                                              22.09.2016 15:37

                                              А можно подробнее?


          1. msts2017
            22.09.2016 01:21

            конкурентный вариант с вложенными блоками подсчета
            http://ideone.com/lh56Ds


    1. alsii
      21.09.2016 18:55

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


      1. Serg046
        21.09.2016 19:12

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


      1. msts2017
        21.09.2016 19:26

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


        1. Bonart
          22.09.2016 10:24

          В условиях никакого count нет ни явно ни неявно.


        1. alsii
          22.09.2016 11:32

          Мне кажется вы вообще не поняли суть проблемы. Задача такая:


          Есть: класс, который реализует методы add и addRange, но не имеет счетчика добавленных элементов.


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


          Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.


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


          Дополню вопросом к Bonart: Полиморфизм при использовании инкорпорации здорово получается, если разработчик базового класса позаботился о том, чтобы объявить интерфейс и указать, что его класс его реализует. Как быть если он этого не сделал? Объявлять интерфейс самим и писать наследник-декоратор его реализующий?


          1. Bonart
            22.09.2016 12:48

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

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


            Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.

            При использовании наследования реализации. При наследовании интерфейсов — возможно и несложно.


            Как быть если он этого не сделал? Объявлять интерфейс самим и писать наследник-декоратор его реализующий?

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


            1. alsii
              22.09.2016 17:15

              Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.

              При использовании наследования реализации. При наследовании интерфейсов — возможно и несложно.

              Да. Именно это я и имел в виду. Проблема возникает именно при наследовании реализации. Проблема решается инкорпорацией и наследованием интерфейса.


          1. vintage
            22.09.2016 23:01

            Проблема: это невозможно сделать не зная деталей реализации методов add и addRange базового класса.

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


            1. Bonart
              22.09.2016 23:08

              Многопоточность уже отменили?


              1. vintage
                22.09.2016 23:44

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


                1. Bonart
                  23.09.2016 00:33

                  Этого в условиях задачи нет.


                  1. vintage
                    23.09.2016 00:51

                    А shared memory — есть?


                    1. Bonart
                      23.09.2016 01:55

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


                      1. vintage
                        23.09.2016 10:23

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

                        В тех языках, что использую я (JS, D) — не предполагается.


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

                        Тут вы правы.


                      1. msts2017
                        23.09.2016 12:23

                        | А возможность многопоточного использования предполагается по умолчанию.
                        Да не, с чего это, в Java\C# не предполагается.
                        А в каких языках объекты сходу потокобезопасны? (я просто не в курсе)


                        1. vintage
                          23.09.2016 20:44

                          В D, например, пишешь shared class и для объекта автоматически создаётся мьютекс и все публичные методы его захватывают. А если не напишешь — не получишь доступа из другого потока без плясок с бубном.


                          1. msts2017
                            23.09.2016 21:07

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


                            1. vintage
                              23.09.2016 21:51

                              А смысл так делать? Куда лучше, когда по умолчанию к объектам нет доступа из другого потока. Хочешь передать данные — посылай сообщения.


                              1. msts2017
                                23.09.2016 22:22

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