Плохие метафоры

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

Отношение наследования классов (**) обычно моделирует отношение вида «является разновидностью» (is a special kind of). Жираф является разновидностью млекопитающего, поэтому класс Giraffe наследуется от класса Mammal, который, в свою очередь, наследуется от класса Animal, который наследуется от Object. И это здорово, поскольку это явно представляет отношение типа «является разновидностью». Однако у меня всегда были проблемы с фундаментальной метафорой «наследования». Почему «наследование»? Вы наследуете генетическую информацию и другие свойства, так что если вы законный лорд, то вы унаследуете это звание от своих родителей. И если вы нарисуете диаграмму классов, то она будет походить на «фамильное дерево», в котором класс-наследник является «потомком» базового «родительского» класса. И действительно, многие говорят о базовом классе, как о «родителе» дочернего класса, особенно при разговоре с начинающими программистами.

Но метафора «наследования от родителя к потомку» является ужасной. Жираф не является «потомком млекопитающего»; жираф является потомком Папы Жирафа и Мамы Жирафы. «Потомок» не является «особой разновидностью родителя». На самом деле, вы наследуете лишь половину своего генотипа от каждого из родителей, и вы можете унаследовать реальные свойства из любого другого родственного отношения или, если на то уж пошло, даже не из родственного отношения вовсе. В языках программирования классы «наследуются» только от родственных типов, при котором наследуются все их члены (*). В реальной жизни, у всех двое родителей (***), но в языках программирования это не так: некоторые языки позволяют иметь множество «родителей», а некоторые – только одного. В реальности, один конкретный человек наследует определенные свойства от одного конкретного родителя, а двое других детей могут унаследовать совершенно другие свойства; в языках программирования отношение «наследования» не применимо на уровне индивидуальных объектов, каждый ребенок наследует одинаковый набор свойств от своих родителей. И в реальности вы наследуете реальные ценности только после смерти предков!

Но, постойте, все еще хуже. В любом языке программирования, который поддерживает лексическую вложенность типов (lexical nesting) и именное выделение подтипов (nominal subtyping), метафора родитель-ребенок является неоднозначной:

class B<T>
{
class D<U> : B<U> { }
}

А ну ка, быстро ответьте на вопрос: какой родительский тип у B<string>.D<int>? Это B<T>, B<string> или B<int>? Лексически этот тип располагается внутри B<T>, логически – внутри B<string>, и наследуется от B<int>; какой из этих трех типов является родительским? Если вы нарисуете граф лексических или логических отношений, то в результате получите граф, выглядящий в точности как «семейное дерево», показывающее отношение наследования. Лексическое вложение дает доступ ко всем свойствам окружающего типа, включая ненаследуемые члены и такие члены, как закрытые конструкторы, к которым обычно нет доступа извне! И не ясно, почему один тип «родительских» отношений более «родительский» нежели другие.

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


(*) За исключением конструкторов и деструкторов.

(**) Здесь я буду говорить только о наследовании классов; хотя моя критика одинаково применима и к наследованию интерфейсов, но я не хочу касаться тонкостей в различиях между наследованиями классов и интерфейсов. Кроме того, я вообще не люблю использование метафоры наследования применительно к интерфейсам; в данном случае «контрактные обязательства» кажутся более подходящей метафорой.

(***) Предполагаем, что речь идет о видах с половым размножением.

Оригинал статьи