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