Есть три типа наследования.
Это три разных и часто противоречивых отношения. Требовать любого или даже всех не представляет никаких сложностей. Но требование поддержки одним механизмом двух или более из них — значит нарываться на проблемы.
Часто для наследования в ООП приводят контрпример отношений между квадратом и прямоугольником. Геометрически квадрат — это специализация прямоугольника: все квадраты — прямоугольники, но не все прямоугольники — квадраты. Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).
Обратите внимание, что здесь налицо несовместимость между направлением наследования геометрических свойств и свойств абстрактного типа данных у квадратов и прямоугольников. Эти два измерения совершенно не связаны друг с другом ни в какой программной реализации. Мы ещё ничего не сказали о наследовании реализации, так что даже не рассматривали написание программы.
Smalltalk и многие более поздние языки используют простое наследование для наследования реализации, потому что множественное наследование несовместимо с ним из-за проблемы ромба (типажи предоставляют надёжный способ объявить несовместимость, оставляя решение проблемы в качестве упражнения для читателя). С другой стороны, простое наследование несовместимо с онтологическим наследованием, поскольку квадрат является одновременно прямоугольником и равносторонним многоугольником.
Синяя книга по Smalltalk описывает наследование исключительно с точки зрения наследования реализации:
Наследование никогда не было проблемой: проблема в попытке использовать одно дерево для трёх разных концепций.
«Предпочитать структуру вместо наследования» — это по сути отказаться от наследования реализации. Мы не можем понять, как заставить его работать, так что давайте вовсе от него откажемся: сделаем совместное использование через делегирование, а не подклассы.
Eiffel и отдельные упорядоченные подходы к использованию языков вроде Java укрепляют отношение «наследование есть создание подтипов», ослабляя отношение «наследование есть повторное использование» (если один и тот же метод появляется дважды в несвязанных частях дерева, вам придётся с этим жить для сохранения свойства, что каждый подкласс является подтипом своего родителя). Это нормально, если вы не пытаетесь также смоделировать и проблемную область с помощью дерева наследования. Обычно литература по ООП рекомендует сделать это, когда речь идёт о проблемно-ориентированном проектировании.
Типажи укрепляют отношение «наследование есть специализация», ослабляя отношение «наследование есть повторное использование» (если обе суперкатегории производят одно и то же свойство экземпляра, то ни одно из них не наследуется и вы должны прописать его самостоятельно). Это нормально, если вы не пытаетесь также рассматривать подклассы как ковариантные подтипы своих суперклассов, но обычно литература по ООП рекомендует сделать это, упоминая принцип подстановки Барбары Лисков и то, что тип в сигнатуре метода означает этот тип или любой подкласс.
Я считаю, что в литературе должно быть написано следующее: «вот три типа наследования, сосредоточьтесь на одном из них». Я также считаю, что языки должны поддерживать это (очевидно, Smalltalk, Ruby и их друзья поддерживают это за счёт отсутствия каких-либо ограничений типов).
Ваша модель области распределения — это не объектная модель. Это не модель абстрактного типа данных. И объектная модель — не модель абстрактного типа данных.
Вот теперь наследование опять стало простым.
- Онтологическое наследование указывает на специализацию: вот эта штука — специфическая разновидность той штуки (футбольный мяч — это сфера и у неё такой-то радиус).
- Наследование абстрактного типа данных указывает на замещение: у этой штуки такие же свойства, как у той штуки, и такое-то поведение (это принцип подстановки Барбары Лисков).
- Наследование реализации связано с совместным использованием кода: эта штука принимает некоторые свойства той штуки и переопределяет или дополняет их таким-то образом. Наследование в моей статье «О наследовании» именно такого и только такого типа.
Это три разных и часто противоречивых отношения. Требовать любого или даже всех не представляет никаких сложностей. Но требование поддержки одним механизмом двух или более из них — значит нарываться на проблемы.
Часто для наследования в ООП приводят контрпример отношений между квадратом и прямоугольником. Геометрически квадрат — это специализация прямоугольника: все квадраты — прямоугольники, но не все прямоугольники — квадраты. Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).
Обратите внимание, что здесь налицо несовместимость между направлением наследования геометрических свойств и свойств абстрактного типа данных у квадратов и прямоугольников. Эти два измерения совершенно не связаны друг с другом ни в какой программной реализации. Мы ещё ничего не сказали о наследовании реализации, так что даже не рассматривали написание программы.
Smalltalk и многие более поздние языки используют простое наследование для наследования реализации, потому что множественное наследование несовместимо с ним из-за проблемы ромба (типажи предоставляют надёжный способ объявить несовместимость, оставляя решение проблемы в качестве упражнения для читателя). С другой стороны, простое наследование несовместимо с онтологическим наследованием, поскольку квадрат является одновременно прямоугольником и равносторонним многоугольником.
Синяя книга по Smalltalk описывает наследование исключительно с точки зрения наследования реализации:
«Подкласс определяет, что все его экземпляры будут, за исключением явно указанных отличий, такими же, как экземпляры другого класса, называемого его суперклассом».Обратите внимание на отсутствующую деталь: не упоминается, что экземпляр подкласса должен быть в состоянии заменить экземпляр суперкласса везде в программе; не упоминается, что экземпляр подкласса должен удовлетворять всем концептуальным тестам для экземпляра своего суперкласса.
Наследование никогда не было проблемой: проблема в попытке использовать одно дерево для трёх разных концепций.
«Предпочитать структуру вместо наследования» — это по сути отказаться от наследования реализации. Мы не можем понять, как заставить его работать, так что давайте вовсе от него откажемся: сделаем совместное использование через делегирование, а не подклассы.
Eiffel и отдельные упорядоченные подходы к использованию языков вроде Java укрепляют отношение «наследование есть создание подтипов», ослабляя отношение «наследование есть повторное использование» (если один и тот же метод появляется дважды в несвязанных частях дерева, вам придётся с этим жить для сохранения свойства, что каждый подкласс является подтипом своего родителя). Это нормально, если вы не пытаетесь также смоделировать и проблемную область с помощью дерева наследования. Обычно литература по ООП рекомендует сделать это, когда речь идёт о проблемно-ориентированном проектировании.
Типажи укрепляют отношение «наследование есть специализация», ослабляя отношение «наследование есть повторное использование» (если обе суперкатегории производят одно и то же свойство экземпляра, то ни одно из них не наследуется и вы должны прописать его самостоятельно). Это нормально, если вы не пытаетесь также рассматривать подклассы как ковариантные подтипы своих суперклассов, но обычно литература по ООП рекомендует сделать это, упоминая принцип подстановки Барбары Лисков и то, что тип в сигнатуре метода означает этот тип или любой подкласс.
Я считаю, что в литературе должно быть написано следующее: «вот три типа наследования, сосредоточьтесь на одном из них». Я также считаю, что языки должны поддерживать это (очевидно, Smalltalk, Ruby и их друзья поддерживают это за счёт отсутствия каких-либо ограничений типов).
- Если я использую наследование для совместного использования кода, не следует предполагать, что мои подклассы одновременно являются подтипами.
- Если я использую подтипы для укрепления интерфейсных контрактов, мне должны не только позволить помечать класс в любом месте дерева как подтип другого класса в любом месте дерева, но обязать делать это. Опять же, не следует предполагать, что мои подклассы одновременно являются подтипами.
- Если мне требуется указать концептуальную специализацию через классы, то это также не должно предполагать соблюдение дерева наследования. Мне должны не только позволить помечать класс в любом месте дерева как подмножество другого класса в любом месте дерева, но обязать делать это. Опять же, не следует предполагать, что мои подклассы одновременно являются специализациями.
Ваша модель области распределения — это не объектная модель. Это не модель абстрактного типа данных. И объектная модель — не модель абстрактного типа данных.
Вот теперь наследование опять стало простым.