В начале...


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


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


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


Мрачные были времена.


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


И вот тут ООП взлетел. Было написано множество4 книг, расплодились бесчисленные5 статьи. Так что сегодня-то каждый может в объектно-ориентированное программирование, так?



Увы, код (и интернет) говорит, что не так


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


Когда мантры вредят


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


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


Начнем с основ.


Определения


Далее в статье я буду понимать под ООП "классический" объектный язык, который поддерживает классы со свойствами, методами и простое (одиночное) наследование. Никаких вам интерфейсов, примесей, аспектов, множественного наследования, делегатов, замыканий, лямбд, — ничего, кроме самых простых вещей:


  • Класс: именованная сущность из предметной области, возможно, имеющая предка (суперкласс), определенная как набор полей и методов.
  • Поле: именованное свойство с определенным типом, которое может, в частности, ссылаться на другой объект (см. композиция).
  • Метод: именованная функция или процедура, с параметрами или без них, реализующая какое-то поведение класса.
  • Наследование: класс может унаследовать — использовать по умолчанию — поля и методы своего предка. Наследование транзитивно: класс может наследоваться от другого класса, который наследуется от третьего, и так далее вплоть до базового класса (обычно — Object), возможно, неявного. Наследник может переопределить какие-то методы и поля чтобы изменить поведение по умолчанию.
  • Композиция: если поле у нас имеет тип Класс, оно может содержать ссылку на другой объект этого класса, создавая таким образом связь между двумя объектами. Не влезая в дебри различий между простой ассоциацией, агрегированием и композицией, давайте "на пальцах" определим: композиция — это когда один объект предоставляет другому свою функциональность частично или полностью.
  • Инкапсуляция: мы обращаемся с объектами как с единой сущностью, а не как с набором отдельных полей и методов, тем самым скрываем и защищаем реализацию класса. Если клиентский код не знает ничего, кроме публичного интерфейса, он не может зависеть от деталей реализации.

Наследование фундаментально


Наследование — это фундаментальное понятие ООП. В языке программирования могут быть объекты и сообщения, но без наследования он не будет объектно-ориентированным (только основанным на объектах, но все еще полиморфным).


… как и композиция


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


(Инкапсуляция тоже вещь фундаментальная, но сейчас речь не о ней)


Так от чего весь сыр-бор?


Ну хорошо, и композиция, и наследование фундаментальны, в чем дело-то?


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


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


Наследование смысловое


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


Наследование механическое


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


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


Как не надо наследовать. Пример 1


class Stack extends ArrayList {
    public void push(Object value) { … }
    public Object pop() { … }
}

Казалось бы, класс Stack, все хорошо. Но посмотрите внимательно на его интерфейс. Что должно быть в классе с именем Stack? Методы push() и pop(), что же еще. А у нас? У нас есть get(), set(), add(), remove(), clear() и еще куча барахла, доставшегося от ArrayList, которое стеку ну вообще не нужно.


Можно было бы переопределить все нежелательные методы, а некоторые (например, clear()) даже и адаптировать под наши нужды, но не многовато ли работы из-за одной ошибки в дизайне? На самом деле трех: одной смысловой, одной механической и одной комбинированной:


  1. Утверждение "Stack это ArrayList" ложно. Stack не является подтипом ArrayList. Задача стека — обеспечить выполнение правила LIFO (последним пришел, первым ушел), которое легко удовлетворяется интерфейсом push/pop, но никак не соблюдается интерфейсом ArrayList.
  2. Механически наследование от ArrayList нарушает инкапсуляцию. Клиентскому коду не должно быть известно, что мы решили использовать ArrayList для хранения элементов стека.
  3. Ну и наконец, реализуя стек через ArrayList мы смешиваем две разные предметные области: ArrayList — это коллекция с произвольным доступом, а стек — это понятие из мира очередей, со строго ограниченным (а не произвольным)8 доступом.

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


Как не надо наследовать. Пример 2


Частая ошибка при наследовании — это создать модель из предметной области, унаследовав ее от готовой реализации. Вот, скажем, нам надо выделить некоторых наших клиентов (класс Customer) в определенное подмножество. Легко! Наследуемся от ArrayList<Customer>, называем это CustomerGroup и понеслась.


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


  1. ArrayList<Customer> это уже наследник списка, утилиты типа "коллекция", готовой реализации.
  2. CustomerGroup это совсем другая штука — класс из предметной области (домена).
  3. Классы из предметной области должны использовать реализации, а не наследовать их.

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


Дело не в одиночном наследовании


Одиночное наследование пока остается самой популярной моделью ООП. Оно неизбежно влечет наследование реализации, которое приводит к сильному зацеплению (coupling — прим. пер.) между классами. Может показаться, что беда в том, что ветка наследования у нас только одна на обе потребности: и смысловую и механическую. Если использовали для одного, то для другого уже нельзя. А раз так, может быть множественное наследование все исправит?


Нет. Отношение наследования не должно пересекать границы между предметными областями: инструментальной (структуры данных, алгоритмы, сети) и прикладной (бизнес-логика). Если CustomerGroup будет наследовать ArrayList<Customer> и одновременно, скажем, DemographicSegment, то две предметные области переплетутся между собой, а "видовая принадлежность" объектов станет неочевидна.


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


От инструментов можно наследовать только другие инструменты.


Это очень частая ошибка новичков. Что не удивительно, ведь так просто взять и унаследовать. Редко где встретишь обсуждения, почему именно это неправильно. Еще раз: бизнес-сущности должны пользоваться инструментами, а не быть ими. Мухи (инструменты) — отдельно, котлеты (бизнес-модели) — отдельно.


Так когда же нужно наследование?


Наследуемся как надо


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


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


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


В ситуации, когда вроде бы подходит и то и другое, взгляните на дизайн в двух плоскостях:


  1. Структура и механическое исполнение бизнес-объектов.
  2. Что они обозначают по смыслу и как взаимодействуют.

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


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


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


Наследуем, если:


  1. Оба класса из одной предметной области
  2. Наследник является корректным подтипом (в терминах LSP — прим. пер.) предка
  3. Код предка необходим либо хорошо подходит для наследника
  4. Наследник в основном добавляет логику

Иногда все эти условия выполняются одновременно:


  • в случае моделирования высокоуровневой логики из предметной области
  • при разработке библиотек и расширений для них
  • при дифференциальном программировании (автор снова использует термин "differential programming", очевидно, понимая под ним нечто, отличное от DDP — прим. пер.)

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


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


Приятного кодинга!


Послесловие


Отдельная благодарность сотрудникам ThoughtWorks за их ценный вклад и замечания: Питу Хогсону, Тиму Брауну, Скотту Робинсону, Мартину Фаулеру, Минди Ор, Шону Ньюхэму, Сэму Гибсону и Махендре Кария.




1

Первый официальный ОО-язык, SIMULA 67, появился в 1967 году.


2

Системные и прикладные программисты приняли на вооружение C++ в середине 1980-х, но перед тем, как ООП стал общепринятым, прошел еще десяток лет.


3

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


4

На момент написание этого текста Амазон предлагает 24777 книг по ООП.


5

Поиск в гугле по фразе "объектно-ориентированное программирование" дает 8 млн результатов.


6

Поиск в гугле выдает 37600 результатов по запросу "наследование это зло".


7

Смысл (интерфейс) и механику (исполнение) можно разделить за счет усложнения языка. См. пример из спецификации языка D.


8

С грустью замечу, что в Java Stack унаследован от Vector.


9

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




Переводчик выражает благодарность ООП-чату в Telegram, без которого этот текст не смог бы появиться.
Поделиться с друзьями
-->

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


  1. search
    03.04.2017 11:05
    +4

    "Наследование — карта, которую можно разыграть только один раз."
    (с) Та же книга


  1. lookid
    03.04.2017 11:08

    Кстати, не холивара ради. Если бы C++/Java/С# не пришли бы на смену Lisp, Haskell, Erlang, C? Было бы лучше сейчас или хуже? Не кажется ли вам, что Divide And Conquer, которое закладывали в ООП, не сработало? И что всё можно написать на JavaScript (будь он компилируем, как С, шаблоны и со статической типизацией)?


    1. stargazr
      03.04.2017 12:04
      +6

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


      1. lookid
        04.04.2017 11:17
        -11

        Код свой покажи, петух с умениями?


        1. am-amotion-city
          04.04.2017 12:31

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


          Единственное место, в котором я вживую видел преимущества ООП (ценой невразумительного размера бойлерплат, правда) — это Hadoop и компания. Там действительно сложно было бы обойтись чистой функциональщиной, просто за счет того, что inner редьюсер для состояния внутри системного outer редьюсера для всего остального — это ад, а весь хадуп — один сплошной даже не редьюсер, а трансдьюсер. Я постоянно на рабочем месте пишу на пяти разных языках (ruby, erlang, elixir, R, js), и могу с уверенностью сказать: они все одинаковые (как и женщины после первых девяти).


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


          1. arielf
            04.04.2017 18:52

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


            1. VolCh
              04.04.2017 19:06

              Преимущества ООП я вижу, прежде всего, при моделировании сущностей и связей между ними (с функциональными вкраплениями, прежде всего filter, map и reduce). Когда речь заходит о моделировании процессов, то ООП хорошо если не мешает.


              1. stargazr
                04.04.2017 19:26

                Можно пример?


                Мне на ум приходит data-oriented design ("что быстрее обработать: структуру массивов или массив структур?"), но, как мне кажется, это проблема скорее конкретной технической реализации ООП, а не парадигмы в целом. Кроме того, это может быть и проблемой дизайна: можно выделить в классы блоки сгруппированных данных, а не сущности, ими описываемые, например.
                (Я веду речь вот об этом: http://www.dice.se/wp-content/uploads/2014/12/Introduction_to_Data-Oriented_Design.pdf)


                1. VolCh
                  04.04.2017 19:50

                  Например, бизнес-процесс покупки картошки в магазине. С сущностями, участвующими в процессе, понятно: магазин, покупатель, продавец, картошка, деньги, касса (навскидку). Сам процесс покупки как представить в ООП-парадигме, чтобы это было естественно и понятно, без введения дополнительных сущностей типа PurchaseProcess, PurchaseManager и т. п.?


                  1. stargazr
                    04.04.2017 21:03

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


                    Еще один пример в голову пришел, тоже из геймдева: архитектура Entity-Component-System, которая, якобы, отвергает ООП. Но, в сущности, это просто другой взгляд на систему, с учетом требований, отличающихся от тех, к которым мы привыкли (реконфигурируемые "на лету" сущности и т.п.), и, по сути, тоже вполне в рамках ООП. Это я и имел в виду, когда говорил, что в ООП все же навыки проектировщика играют ключевую роль (это, впрочем, верно и для остальных парадигм).


                    1. VolCh
                      05.04.2017 10:08

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


                      1. vintage
                        05.04.2017 10:31

                        Да что там не удобного-то?


                  1. Cromathaar
                    10.04.2017 22:46

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

                    Подробнее про это можно почитать в главе 20 книги Роберта Мартина «Принципы, паттерны и методики гибкой разработки на языке C#» (или без языка C# в более ранней версии).


                    1. VolCh
                      11.04.2017 07:42

                      Это вы про что конкретно?


                      1. Cromathaar
                        11.04.2017 10:15

                        Про вашу картошку, вестимо :)


                        1. VolCh
                          11.04.2017 13:18

                          У картошки нет поведения — это объект-значение :)


              1. am-amotion-city
                04.04.2017 21:17

                FSM легче всего реализуется на объектах, как раз.


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


                Шах и мат.


                1. VolCh
                  05.04.2017 07:44

                  Речь не про автоматы.


                  1. am-amotion-city
                    05.04.2017 08:29

                    А у вас процессы как определяются, стесняюсь спросить? Из /dev/random читаете, какой шаг следующий?


                    И вообще, я лишь указал на то, что:


                    Преимущества ООП я вижу, прежде всего, при моделировании сущностей и связей между ними [...]. Когда речь заходит о моделировании процессов, то ООП хорошо если не мешает.

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


                    1. VolCh
                      05.04.2017 10:15

                      А они не дискретны и бесконечны по своей сути. На практике дискретны, конечно, потому что цифровые компьютеры дискретны и число комбинаций состояний памяти конечно, но приходится сводить бесконечные аналоговые величины типа времени к дискретным конечным приближениям. Но делать автомат с числом состояний типа 2^64 как-то не хочется.


            1. am-amotion-city
              04.04.2017 19:49

              Это в вас привычка (или пристрастия, или и то, и другое) разговаривает.


              Вообще, разговор про ООП vs ФП мне больше всего напоминает спор, что вкуснее: яблоко, или огурец. На вкус и цвет — сами знаете что, но чтобы не остаться голодным прекрасно годится любой (а еще лучше оба).


        1. stargazr
          04.04.2017 14:50
          -1

          Функтокид бомбанул, найс.
          Десятки лет объектно-ориентированных притеснений дают о себе знать :) Бедненький.
          покормил


    1. Akon32
      03.04.2017 13:25
      +2

      Если бы C++/Java/С# не пришли бы на смену Lisp, Haskell, Erlang, C? Было бы лучше сейчас или хуже?

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


      Не кажется ли вам, что Divide And Conquer, которое закладывали в ООП, не сработало?

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


      И что всё можно написать на JavaScript (будь он компилируем, как С, шаблоны и со статической типизацией)?

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


      1. arielf
        03.04.2017 20:09
        +1

        Если бы C++/Java/С# не пришли бы на смену Lisp, Haskell, Erlang, C? Было бы лучше сейчас или хуже?

        Они и не пришли им на смены, а создали новые ниши. Lisp, Haskell, Erlang и C живут в своих.


        1. VolCh
          04.04.2017 08:29

          Всё-таки C++ заменил C во многих нишах, а потом Java/C# заменили его частично.


      1. vintage
        03.04.2017 21:14

        Это typescript :-) у меня есть мысль компилировать его как раз в бинарник..


  1. http3
    03.04.2017 11:35
    -1

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

    Золотые слова.

    Разумеется, никакие инструкции не заменят голову на плечах.

    Тоже золотые слова.
    А многие ленуются думать своей головой.

    Дальше не по теме:
    Покуда не появился графический интерфейс2, которому, как выяснилось, очень-очень не хватало ООП.

    То есть для программирования сайта на PHP в общем-то ООП не особо-то и нужно? :)

    И вот тут ООП взлетел.

    Может еще и компы стали мощнее и ООП стало не таким дорогим? :)

    Поиск в гугле по фразе «объектно-ориентированное программирование» дает 8 млн результатов.

    Ну это такое.
    Покажет-то он меньше. :)
    И на последних страницах будет шлак :)


    1. VolCh
      03.04.2017 13:20

      То есть для программирования сайта на PHP в общем-то ООП не особо-то и нужно? :)

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


      1. http3
        03.04.2017 17:30

        Этот интерфейс написан на PHP программистом, писавшим сайт? :)


        1. VolCh
          03.04.2017 17:32
          +1

          Заметная его часть — html минимум :)


          1. http3
            04.04.2017 01:57
            -2

            Вы хоть верите (понимаете) в то, что пишете, или просто аццки гоните? :)


            1. VolCh
              04.04.2017 08:34

              Понимаю. В терминах MVC View и Controller классического php-приложения размазаны между сервером и браузером, а HTML-код, генерируемый PHP-кодом — основная (по специфичности для приложения) его часть.


    1. avost
      03.04.2017 14:40

      То есть для программирования сайта на PHP в общем-то ООП не особо-то и нужно? :)

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


      Покуда не появился графический интерфейс2, которому, как выяснилось, очень-очень не хватало ООП
      Может еще и компы стали мощнее и ООП стало не таким дорогим? :)

      Вы про какое время? ооп-ная графика в яве была когда php ещё назывался шаблонизатором персональных страниц.


  1. sbnur
    03.04.2017 13:17

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


    1. geher
      03.04.2017 14:47

      Копипаста была еще когда не то что программирование, письменность не изобрели.
      Причем буфер обмена продвинутый имелся, с управлением множественными копиями.
      Copy text from external sound stream to brain clipboard.
      Paste text to output sound stream.
      И текст программы в машинных кодах копипастили друг у друга через листок бумаги.в


      1. sbnur
        03.04.2017 15:45

        Знаете я вообщето-то работал в те годы


  1. Bonart
    03.04.2017 13:24
    +9

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


  1. am-amotion-city
    03.04.2017 13:40
    +1

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

    Есть языки (наверное, самый яркий пример — руби), где такая имитация совсем не приводит к возне, и, как мне кажется, именно это послужило основной причиной рождения мантры «четыре ноги хорошо, две плохо»^W «композиция заборет наследование». Потому что из [немножественного] наследования композиции не добиться никак, о чем и написано 80% текста выше.


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


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


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


  1. arielf
    03.04.2017 20:06
    +3

    ООП в базовом виде состоит всего лишь из двух постулатов:

    1. всё есть объект
    2. объекты взаимодействуют через посылку сообщений

    Всё прочее, включая классы, не более чем приятное дополнение. Увы, C++ слишком исказил восприятие многих людей.

    Существует две главные парадигмы в ООП: основанная на наследовании (class based) и прототипах (prototype based). В первой к общей идее добавили третий пункт:
    1. всё есть объект
    2. объекты взаимодействуют через посылку сообщений
    3. объекты являются экземплярами классов

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


    1. Danik-ik
      03.04.2017 22:07

      Хотел ответить, промахнулся, написал чуть ниже...


  1. java73
    03.04.2017 21:57

    Спасибо, нашел очередное подтверждение того, о чем сам всё время думаю (ну не всю жизнь, а только сейчас, пока вникаю в азы проектирования, почитывая Макконнелла (гл. 6 стр. 140-145))


  1. Danik-ik
    03.04.2017 22:01

    Я, наверное, испорчен Википедией, но после слова "считается" я автоматически вижу вопрос:"кем?"


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


    Я, к примеру, очарован хипповым раздолбайством js, в частности, полифиллы приводят меня в экстаз, но… Со стороны, только со стороны. И мне строгое наследование кажется гораздо более чистым. Потому, что чистым можно быть от самых разных вещей :)


    1. arielf
      03.04.2017 23:39

      кем?

      In short: авторами ООП концепции.

      In long: с научной позиции оно так и получается. В научном подходе теория объективно считается лучше, если она задействует меньше специальных случаев и исключений. Чем меньше изначальных аксиом, тем лучше. Приветствуется универсальность — чем шире область применения, тем лучше. Теория языков программирования — вполне математична и формальна, и к ней применимы все те же нормы. Причины этих явлений, я сдесь излагать не буду, ибо боюсь соврать, но они вполне объективы, и хорошо изложены у Карла Поппера и Дэвида Дойча.

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


      1. Danik-ik
        04.04.2017 07:00
        +1

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


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


  1. samizdam
    03.04.2017 22:47
    +1

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


    1. f3ath
      03.04.2017 23:53

      Спасибо. Значит мой замысел удался.


  1. jewalky
    04.04.2017 00:24

    Немного не по теме, но зацепило одно замечание — сразу вспомнился GTK, который на не-ООП языке попытался создать ООП просто потому, что ООП якобы необходим для UI.

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

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

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

    Однако система есть и пользуется определённой популярностью.


  1. mitutee
    04.04.2017 00:24

    Можно ли говорить о том, что в JavaScript наследование реализовано в виде композиции (у «наследника» просто есть ссылка на «предка»(prototype))? Я правильно понял?


    1. vintage
      04.04.2017 07:46

      Не совсем. Разница в том, где предок хранит данные. В яваскрипте все данные хранятся в одном и том же обыекте. А вот в каком-нибудь c++ приватные данные каждого класса хранятся в отдельных областях памяти. И хоть там нет "ссылки на родителя", но это самая натуральная композиция.


      1. VolCh
        04.04.2017 08:39

        В яваскрипте все данные хранятся в одном и том же обыекте.

        Што?! В JS данные, к которым можно получить доступ по this.property размазаны по цепочке прототипов.


        1. vintage
          04.04.2017 08:45

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


          1. VolCh
            04.04.2017 08:50

            Не дефолтные, а определенные не в самом объекте.


            1. vintage
              04.04.2017 08:59

              Дефолтные для данного объекта.


              1. VolCh
                04.04.2017 09:07

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


                1. vintage
                  04.04.2017 09:27
                  -1

                  А в каком вы его понимаете?


                  1. VolCh
                    04.04.2017 10:01

                    Как значение, скажем, для числового свойства дефолтным значением может быть 5.


                    1. vintage
                      04.04.2017 10:16
                      -1

                      Которое, если не установлено, будет взято из прототипа.


                      1. VolCh
                        04.04.2017 10:34

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


                        1. vintage
                          04.04.2017 10:53
                          -1

                          Что выглядит как утка и крякает как утка — то и называют уткой.


                          1. VolCh
                            04.04.2017 11:34

                            Именно. если я пишу console.log(this.a) и вижу 5, то для меня 5 обычное значение свойства a объекта, а не какое-то дефолтное. Что оно не собственное, а одного из объектов цепочки прототипов — нюанс.


                            1. vintage
                              04.04.2017 14:15
                              -1

                              Если вы явно не устанавливали значение, а оно есть — это значение по умолчанию. Вот зачем вы с определениями спорите?


                              1. VolCh
                                04.04.2017 15:17

                                Я как раз его явно устанавливаю:


                                const parent = {a: 5};
                                const child = Object.create(parent);
                                console.log(child.a);


                                1. vintage
                                  04.04.2017 15:42

                                  В объект child вы его не устанавливаете, для него это значение дефолтное.


                                  1. VolCh
                                    04.04.2017 16:27
                                    +1

                                    А если сделаю


                                    const parent = {a: 5};
                                    const child = Object.create(parent);
                                    console.log(child.a);
                                    parent.a = 10;
                                    console.log(child.a);
                                    

                                    то, что, второе дефолтное значение создаю, два дефолтных значения?
                                    Спецификация JS явно говорит, что a в таком случае — свойство объекта, унаследованное, но свойство этого объекта: inherited property — property of an object that is not an own property but is a property (either own or inherited) of the object’s prototype/


                                    1. vintage
                                      04.04.2017 21:50
                                      -1

                                      … то вы поменяете дефолтное значение. Вас смущает, что это не константа? Ну так это и не статический язык. Сравните с:


                                      const parent = {a: 5};
                                      const child = Object.create(parent);
                                      child.a = 666;
                                      console.log(child.a);
                                      parent.a = 10;
                                      console.log(child.a);


    1. VolCh
      04.04.2017 08:40

      "С помощью", а не "в виде". Кроме собственно композиции есть ещё механизм доступа с данными по цепочке прототипов.


  1. lalaki
    04.04.2017 12:15
    +1

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


    • кнопка, нажимаемая один раз: нарушает контракт обычной кнопки (только первое нажатие генерирует ожидаемое событие), а не "дополняет" его — тесты обычной кнопки не пройдут для одноразовой


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


    1. vintage
      04.04.2017 14:20
      -1

      UI ничем принципиально не отличается он не-UI. Компоненты точно также наследуются друг от друга.


    1. f3ath
      04.04.2017 22:05

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


  1. boblenin
    04.04.2017 16:33
    -1

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


  1. imilov
    04.04.2017 21:48

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

    Применяя это рассуждение к теме статьи можно сформулировать следующие рекомендации по вопросу composition vs inheritance:

    — если инстансы класса B необходимо хранить в переменных типа A, то B должен быть наследником (подтипом), прямым или опосредованным, класса A;
    — если в первом нет необходимости, имеет смысл проектировать код используя композицию.

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


    1. VolCh
      05.04.2017 10:19

      должен быть

      имеет смысл

      Неравноценные инструкции. Вторая имеет нюансы. А в целом "необходимо хранить в переменных типа A" — редкое требование


  1. michael_vostrikov
    04.04.2017 22:36

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

    Допустим, если бы можно было сделать так:

    class Square variant of Rectangle
    {
        public __match()
        {
            return ($this->width === $this->height);
        }
    }
    
    function someActionWithSquare(Square $s)
    {
        ...
    }
    
    $r1 = new Rectangle(10, 20);
    $r2 = new Rectangle(10, 10);
    
    someActionWithSquare($r1);
    // throw new Exception('Rectangle does not match Square')
    
    someActionWithSquare($r2);
    // success call
    


    1. vintage
      04.04.2017 23:05

      function someActionWithRectangle(Rectangle $r)
      {
          $r->width = 2 * $r->height
      }
      
      someActionWithRectangle(new Square(10))

      Что должно произойти?


      1. michael_vostrikov
        05.04.2017 06:48

        Здесь ничего. Это императивный язык, если мы сделали возможность менять состояние объекта снаружи, значит согласны, что какое-то время он будет в неконсистентном состоянии. Переменная просто не будет Square после вызова, хотя будет Rectangle, и в следующем действии со Square будет ошибка. Можно сделать возможность проверять вручную в контрольных точках через кастинг.

        function someActionWithRectangle(Rectangle $r)
        {
            $r->width = 2 * $r->height
        }
        
        $s = new Square(10);
        someActionWithRectangle($s);
        someActionWithSquare($s);  // exception
        
        (Square)$s; // exception
        


        1. vintage
          05.04.2017 07:41

          Объект, который в любой момент может поменять свой тип — так себе концепция.


          1. michael_vostrikov
            05.04.2017 08:48

            Вас же не смущает, когда вы передаете Child extends Parent в функцию принимающую Parent, и там переменная считается типом Parent.
            Технически переменная все еще будет иметь тип Square, просто при проверках __match() будет возвращать false, и специфичных для Square действий с ней нельзя будет сделать. То есть, до изменений $s instanceof Square == true и $s instanceof Rectanlge == true, а после $s instanceof Rectanlge == true а $s instanceof Square == false.

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

            Другой пример, более практический:

            class Order
            {
                private $productList;
                private $deliveryAddress;
            }
            
            class OrderForCheckout variant of Order
            {
                public function __match()
                {
                    return (count($this->productList) > 0 && !empty($this->deliveryAddress));
                }
            }
            
            function checkout(OrderForCheckout $order)
            {
                ...
            }
            
            $order = Order::findOne($id);
            checkout($order);
            


            1. Akon32
              05.04.2017 09:51

              Вас же не смущает, когда вы передаете Child extends Parent в функцию принимающую Parent, и там переменная считается типом Parent.

              Не смущает, потому что при таком определении Child является Parent (если, конечно, корректно задействован принцип подстановки Лисков, а не построена иерархия, где например треугольник наследуется от линии).


              1. michael_vostrikov
                05.04.2017 16:54

                Ну а Square является Rectangle. Любой Square это Rectangle, но не любой Rectangle это Square.


                1. vintage
                  05.04.2017 19:58
                  -1

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


                  1. INC_R
                    06.04.2017 15:20

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


                    С точки зрения геометрии да, Square является Rectangle. В ООП это не обязательно так. Например, у прямоугольника при изменении Width не должно меняться значение Height. У квадрата же Height тоже изменится, что нарушает LSP. Поэтому такое наследование недопустимо. Допустимо оно только тогда, когда Width/Height неизменяемы.


                    1. michael_vostrikov
                      06.04.2017 15:57

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


                      У квадрата при изменении Height необязательно должна меняться Width, он просто перестанет быть квадратом.


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


                    1. vintage
                      06.04.2017 16:05

                      Наследник естественным образом является производным типом.


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


  1. FadeToBlack
    05.04.2017 08:51

    Вещи из реального мира не работают по принципу НАСЛЕДОВАНИЯ. Они работают по принципу агрегации КОМПОНЕНТОВ. Если брать за пример половое деление клеток, то на этапе кроссинговера происходит обмен участками гомологичных хромосом, и наверняка это участки, кодирующие законченные "фичи" в виде белков и другой информации. То есть, на макроуровне мы видим абстрактное наследование, а если разобраться глубже — то мы видим обмен "компонентами" — в основном, строением белков, которые тоже состоят из неделимых "компонент" — аминокислот. Если брать в пример электронику — то тут становится ясно, что компоненты имеют всякие выводы, которые представляют интерфейс взаимодействий. Внутреннее же устройство никто не завязывает на другие компоненты, оно физически полностью свободно от зависимостей. И не будет так, что мы меняем в одном месте резистор, и от этого меняются все классы резисторов разной мощности...


  1. sshikov
    05.04.2017 09:31

    > ArrayList это уже потомок ArrayList

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


    1. f3ath
      06.04.2017 03:56

      А вот именно к переводчику-то и надо предъявлять. У меня там явно ошибка перевода. Исправил эту фразу.


      1. sshikov
        06.04.2017 10:44

        Да, стало лучше, но по-моему на себя вы это зря.


        Все же у автора есть подозрение на непонимание одной (ради объективности — довольно-таки сложной) вещи — List это не наследник List

        И вот так:
        ArrayList is a subclass of list already, a utility collection — an implementation class.

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


        1. sshikov
          06.04.2017 10:47

          Уф. Всю разметку слопал проклятый долгоносик...


          В общем, речь была о том, что параметризованный List из String это не наследник List из Object (по крайней мере в Java). У параметризованных generic типов вообще все сложнее, чему впрочем хороших объяснений в сети навалом (скажем, вот: https://briangordon.github.io/2014/09/covariance-and-contravariance.html). А особенно когда wildcards имеют место.


  1. grigorym
    06.04.2017 07:42

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


  1. ansash76
    08.04.2017 22:45

    Как по мне наследование — это ЯВЛЯЕТСЯ, собака является животным значит наследуемся. Собака СОДЕРЖИТ блох значит применяем композицию, конечно примитивно, но логика в этом очевидна


    1. VolCh
      09.04.2017 16:04

      На практике лучше "собака является животным — можем наследовать", особенно если одновременно есть и отношение "собака является другом человека" :)


    1. Dmitry_Shapiro
      10.04.2017 04:28

      Собака с блохами все еще является собакой? :)
      Тут как бэ получилась новая «собака с блохами» — наследница просто собаки, которая композирована с блохами )))


      1. stargazr
        10.04.2017 11:39

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