Опубликован: 23.10.2005 | Уровень: специалист | Доступ: платный
Лекция 6:

Используйте наследование правильно

Аннотация: Изучение технических деталей наследования и связанных механизмов в лекциях 7-18 курса "Основы объектно-ориентированного программирования" не означает еще автоматического владения всеми методологическими следствиями. Из всех проблем ОО-технологии ни одна не вызывает столько обсуждений и вопросов о том, как и когда использовать наследование. В этой лекции мы продвинемся в понимании смысла наследования не ради теории, а ради наилучшего применения наследования в наших проектах. В частности, мы попытаемся понять, чем наследование отличается от другого межмодульного отношения, его брата и соперника, - отношения встраивания, чаще всего называемого клиентским отношением. Мы исследуем, когда следует использовать одно, когда - другое, а когда оба отношения являются приемлемыми. Мы установим основной критерий использования наследования, по пути выяснив, в каких случаях его использование ошибочно. Мы сможем создать классификацию различных случаев легитимного использования, часть из которых принимается безоговорочно (наследование подтипов), другие - более спорны. На этом пути мы попытаемся освоить опыт таксономии или систематики, привнесенный из других научных дисциплин.
Ключевые слова: дерево, ПО, программная инженерия, множественное наследование, CAR, объект, наследование, отношение, отношение наследования, автор, текущий счет, полезность, клиентское отношение, система типов, software engineer, описатели, vision, класс, связь, Windows, OS/2, motif, доступ, инструментарий, ACE, postscript, HTML, представление, toolkit, атрибут, поле, компонент, полиморфизм, динамическое связывание, иерархия наследования, полиморфная сущность, store, SQL, Oracle, Sybase, ODBC, инвариант, деление, таксономия, предусловие, постусловие, управление персоналом, булев атрибут, UNIQUE, inspection, подтип, subtype, restriction, variation, facility, абстрактная машина, черный ящик, constant, machinable, конкретизация, свертка, родительский класс, депозитный счет, square, two-sided, атрибут класса, текстовый процессор, chapter, декартово произведение, модель множества, кортеж, segment, абстрактный класс, VDM, comparator, infix, отношение порядка, non-numerical, triggering, механизмы, итератор, polygon, пользователь языка, goto, аргумент, моноид, figures, shipping, бинарный файл, triangle, разрешение конфликтов, бесконечное множество, иерархия классов, булева функция, типизация, ABC, определение процессов, on-the-fly, прецедент, иерархия типов, capacity, representation, свойства множества, лексема, операция объединения, абстрактное свойство, класс приложений, permanent, наследование типов, contract, supervisor, temporary, traversal, bag, последовательным доступом, геометрическая фигура, комплексное число, complexity, дедукция, абстрагирование, расширяемость, конструирование, математика, графика, базы данных, приложение, Пирамида

Как не следует использовать наследование

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

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

Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках (ссылка на первое издание этой книги [M1988]).

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

Характеристики нескольких различных классов объектов

(классы, уже хорошо!)

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

(Нет, опять неудача.) Далее следует пример множественного наследования:

например, пусть мы имеем класс объектов CAR, инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения

(неужели оправдаются наши наихудшие подозрения?)

нового класса CAR-OWNER, комбинирующего атрибуты CAR и PERSON.

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

Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.

Походящая модель

Рис. 6.1. Походящая модель

В формальной записи:

class CAR_OWNER inherit
    PERSON
feature
    my_car: CAR
    ...
end

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

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

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

Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)

Рис. 6.2. Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)

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

Правило: Наследование "Is-a" (является)

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

Другими словами, мы должны быть способными убеждать, что каждый B is an A (отсюда имя: "is-a").

Вопреки первому впечатлению, это слабое, а не строгое правило, и вот почему:

  • Обратите внимание на фразу "привести аргументы". Мы не требуем доказательства того, что каждый B всегда является A. В большинстве случаев мы оставляем пространство для дискуссии. Верно ли, что каждый "сберегательный счет" (savings account) является "текущим счетом" (checking account)? Здесь нет абсолютного ответа - все зависит от политики банка и вашего анализа свойств различных видов счетов. Возможно, вы решите сделать класс SAVINGS_ ACCOUNT наследником BANK_ACCOUNT или поместить его где-либо еще в структуре наследования. Разумные люди могут все же не согласиться с результатом. Это нестрашно, важно лишь, чтобы был случай, для которого ваши аргументы способны устоять. В нашем контрпримере: нет ситуации, при которой аргументы в пользу того, что CAR_OWNER является CAR, могли бы устоять.
  • Наш взгляд на то, что означает отношение " является ", будет довольно либеральным. Он не будет, например, препятствовать наследованию реализации - форме наследования, многими считающейся подозрительной.

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

Алексей Щербанов
Алексей Щербанов
Россия, г. Оренбург
Ксения Маковецкая
Ксения Маковецкая
Россия, Москва, МГЮА им. О.Е. Кутафина, 2014