Наследование программирование с

В самом начале…

…не было ни наследования. Ни композиции. Только код. И код был громоздким, повторяющимся, блочным, несчастным. Многословным и усталым. Копирование и вставка были основными механизмами повторного использования кода. Процедуры и функции были редкостью. Новомодные гаджеты рассматривались с подозрением. Вызов процедуры стоил дорого! Отделение кусков кода от основной логики вызвало путаницу!

Это было Темное Время.

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

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

К сожалению. Код (и Интернет) говорит нет.

Самым большим моментом путаницы и разногласий. По-видимому. Является композиция против наследования. Часто резюмируемая в мантре “

предпочтение композиции над наследованием” … Давайте поговорим об этом.

Мантры Считаются Вредными

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

Я также не поклонник нелепых заголовков clickbait, таких как “Наследование-зло”6, особенно когда автор пытается подкрепить такое возмутительное утверждение. Используя наследование ненадлежащим образом… а потом обвинять наследство. Как плотник, заявляющий. Что молотки бесполезны. Потому что они плохо управляют винтами.

Давайте начнем с основ.

Определения

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

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

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

Наследование имеет фундаментальное значение для объектно-ориентированного программирования. Язык программирования может иметь объекты и сообщения. Но без наследования он не является объектно-ориентированным (просто “объектно-ориентированным”. Но все же полиморфным).

…и Композиция тоже

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

(Инкапсуляция тоже фундаментальна. Но мы не будем много говорить об этом здесь.)

Так из-За Чего вся Эта Суета?

Состав и наследование являются фундаментальными. Так в чем же дело?

Важно думать. Что одно может заменить другое во всех случаях. Или что одно лучше или хуже другого. Как и все остальное в разработке программного обеспечения. Есть компромиссы.

Композиция довольно проста для понимания — мы можем видеть композицию в повседневной жизни: у стула есть ножки. Стена состоит из кирпича и раствора и так далее. Хотя определение наследования простое. Оно может стать сложным и запутанным. Если использовать его неразумно. Наследование-это скорее абстракция. О которой мы можем только говорить. Но не касаться напрямую. Хотя во многих ситуациях можно имитировать наследование с помощью композиции. Это часто бывает громоздко. Цель композиции очевидна: сделать целое из частей. Цель наследования немного сложнее. Потому что наследование служит двум целям-семантике и механике.

Семантика наследования

Наследование фиксирует семантику (значение) в иерархии классификации (таксономии). Упорядочивая понятия от обобщенных к специализированным. Группируя связанные понятия в поддеревья и т. Д. Семантика класса в основном фиксируется в его интерфейсе. Наборе сообщений. На которые он отвечает. Но часть семантики также находится в наборе сообщений. Которые посылает класс. При наследовании от класса вы неявно принимаете на себя ответственность за все сообщения. Которые суперкласс отправляет от вашего имени. А не только за сообщения. Которые он может получить. Это делает подкласс более тесно связанным со своим суперклассом. Чем это было бы. Если бы он просто использовал экземпляр суперкласса в качестве компонента вместо наследования от него. Обратите внимание. Что даже в классах. Которые не “делают” много. Имя класса передает разработчику значительную семантическую информацию о домене.

Механика наследования

Наследование захватывает механику путем кодирования представления данных (полей) и поведения (методов) класса и делает его доступным для повторного использования и расширения в подклассах. Механически подкласс унаследует реализацию суперкласса и. Следовательно. Его интерфейс.

Двойная цель наследования7 в большинстве современных языков ООП, я считаю. Причина большей путаницы. Многие люди думают. Что “повторное использование кода” является основной целью наследования. Но это не единственная его цель. Чрезмерное внимание к повторному использованию может привести к трагически ошибочным конструкциям. Давайте рассмотрим несколько примеров.

Как злоупотреблять наследованием — Пример 1

Начнем с простого и чрезвычайно распространенного примера неправильного использования наследования:

 стек класса расширяет ArrayList { public void push(Object value) { ... } public Object pop() { ... } }

Этот класс будет функционировать как стек. Но его интерфейс смертельно раздут. Открытый интерфейс этого класса не просто push и pop. Как можно было бы ожидать от класса с именем Stack. Он также включает get, set. Add, remove. Clear и кучу других сообщений. Унаследованных от ArrayList. Которые не подходят для стека.

Вы можете переопределить все нежелательные методы и, возможно. Адаптировать некоторые полезные (например, clear). Но это большая работа. Чтобы скрыть ошибку моделирования. Три ошибки моделирования. На самом деле. Одна семантическая. Одна механическая. Одна и та и другая:

  1. Семантически утверждение “Стек-это ArrayList” неверно; Стек не является правильным подтипом ArrayList. Предполагается. Что стек должен обеспечивать соблюдение last-in-first-out-ограничения. Легко удовлетворяемого интерфейсом push/pop. Но не применяемого интерфейсом ArrayList.
  2. Механически наследование от ArrayList нарушает инкапсуляцию; использование ArrayList для хранения коллекции объектов стека-это выбор реализации. Который должен быть скрыт от потребителей.
  3. Наконец, реализация стека путем наследования от ArrayList-это междоменное отношение: ArrayList-это случайно доступная коллекция; Стек-это концепция массового обслуживания со специально ограниченным (неслучайным) доступом8. Это разные области моделирования.

Последний вопрос очень важен. Но несколько неуловим. Поэтому давайте рассмотрим его на другом примере.

Как злоупотреблять наследованием — Пример 2

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

Неправильно. Это были бы междоменные отношения наследования. И их следует избегать:

  1. .
  2. CustomerGroup — это еще один подкласс-доменный класс.
  3. Доменные классы должны использовать классы реализации. А не наследоваться от них.

Пространство реализации должно быть невидимым на уровне домена. Когда мы думаем о том. Что делает наше программное обеспечение. Мы работаем на уровне домена; мы не хотим отвлекаться на детали о том. Как оно делает вещи. Если мы сосредоточены только на “повторном использовании кода” через наследование. Мы будем попадать в эту ловушку неоднократно.

Единственное наследование это не проблема

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

Нет. Отношения наследования не должны пересекать границы домена (домен реализации и домен приложения).

Предпочтительное (по крайней мере. Для меня!) решение-наследовать от служебных классов столько. Сколько необходимо для реализации ваших механических структур. А затем использовать эти структуры в доменных классах через композицию. А не наследование. Позвольте мне повторить это:

Если вы не создаете класс реализации. Вы не должны наследовать его от класса реализации.

Это одна из самых распространенных проблем начинающих — потому что это так удобно! — и причины. Почему это неправильно. Не часто обсуждаются в литературе по программированию. Поэтому я повторю еще раз: ваши классы домена приложения должны использовать классы реализации. А не быть одним. Держите эти таксономии/домены разделенными.

Итак, когда и как мы должны использовать наследование?

Использование Колодца Наследования

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

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

Как решить: Состав или Наследование?

Когда у вас есть ситуация. В которой будет работать либо композиция. Либо наследование. Подумайте о разделении обсуждения дизайна на два:

  1. Представление/реализация концепций вашей предметной области-это одно измерение
  2. Семантика понятий вашей предметной области и их связь друг с другом-это второе измерение

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

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

Ничто не может заменить объектное моделирование и критическое дизайнерское мышление. Но если у вас должны быть какие — то рекомендации. Подумайте вот

о чем: Наследование следует использовать только тогда, когда:

  1. Оба класса находятся в одной и той же логической области
  2. Подкласс это собственно подтип суперкласса
  3. Реализация суперкласса необходима или подходит для подкласса
  4. Усовершенствования. Производимые этим подклассом. В основном носят аддитивный характер.

Бывают моменты когда все эти вещи сходятся:

  • Моделирование предметной области более высокого уровня
  • Фреймворки и расширения фреймворков
  • Дифференциальное программирование

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

Счастливого кодирования!

Приложение

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


1. Первый официально объектно-ориентированный язык SIMULA 67 появился на свет в 1967 году. Объектно-ориентированному программированию уже 48 лет!
2. программисты систем и приложений приняли C++ в середине 1980-х годов. Но повсеместному распространению ООП пришлось подождать еще десятьлет .
3. да, я слишком упрощаю. Игнорируя слушателей/делегатов событий/и т. Д.
4. Amazon претендует на 24 777 книг по теме объектно-ориентированного программирования на момент написания этой
статьи 5. Google search претендует на ~8 млн результатов для точной фразы “объектно-ориентированное программирование” на момент написания этой
статьи 6. Поиск в Google дает около 37,600 результаты на точную фразу “наследование-это зло” на момент написания этой статьи
7. Семантика (интерфейсы) и механика (представление) могут быть разделены. За счет дополнительной языковой сложности; см., например, в языке D спецификация по C. J. дата и Хью Дарвен.
8. Обратите внимание с некоторой грустью. Что стек Java-класс наследует от вектора.
9. Проектирование классов для повторного использования с помощью наследования выходит за рамки данной статьи. Просто имейте в виду. Что потребители экземпляров и подклассов имеют различные потребности. Которые должны быть удовлетворены вашим дизайном класса.