Стандарты программирования на С++. 101 правило и рекомендация | страница 65
Открытое наследование в соответствии с принципом подстановки Лисков (Liskov Substitution Principle [Liskov88]) всегда должно моделировать отношение "является" ("работает как"): все контракты базового класса должны быть выполнены, для чего все перекрытия виртуальных функций-членов не должны требовать большего или обещать меньше, чем их базовые версии. Код, использующий указатель или ссылку на >Base
, должен корректно вести себя в случае, когда указатель или ссылка указывают на объект >Derived
.
Неверное использование наследования нарушает корректность. Обычно некорректно реализованное наследование не подчиняется явным или неявным контрактам, установленным базовым классом. Такие контракты могут оказаться очень специфичными и хитроумными, и программист должен быть особенно осторожен, когда их нельзя выразить непосредственно в коде (некоторые шаблоны проектирования помогают указать в коде его предназначение — см. рекомендацию 39).
Наиболее часто в этой связи упоминается следующий пример. Рассмотрим два класса — >Square
(квадрат) и >Rectangle
(прямоугольник), каждый из которых имеет виртуальные функции для установки их высоты и ширины. Тогда >Square
не может быть корректно унаследован от >Rectangle
, поскольку код, использующий видоизменяемый >Rectangle
, будет полагать, что функция >SetWidth
не изменяет его высоту (независимо от того, документирован ли данный контракт классом >Rectangle
явно или нет), в то время как функция >Square::SetWidth
не может одновременно выполнить этот контракт и свой инвариант "квадратности". Но и класс >Rectangle
не может корректно наследовать классу >Square
, если его клиенты Square полагают, например, что для вычисления его площади надо возвести в квадрат ширину, либо используют какое-то иное свойство, которое выполняется для квадрата и не выполняется для прямоугольника
Описание "является" для открытого наследования оказывается неверно понятым при использовании аналогий из реального мира: квадрат "является" прямоугольником в математическом смысле, но с точки зрения поведения >Square
не является >Rectanglе
. Вот почему вместо "является" мы предпочитаем говорить "работает как" (или, если вам это больше нравится, "используется как") для того, чтобы такое описание воспринималось максимально правильно.
Открытое наследование действительно связано с повторным использованием, но опять же не так, как привыкло думать множество программистов. Как уже указывалось, цель открытого наследования — в реализации заменимости (см. [Liskov88]). Цель отрытого наследования не в том, чтобы производный класс мог повторно использовать код базового класса для того, чтобы с его помощью реализовать свою функциональность. Такое отношение "реализован посредством" вполне корректно, но должно быть смоделировано при помощи композиции или, только в отдельных случаях, при помощи закрытого или защищенного наследования (см. рекомендацию 34).