Сама статья "Data Abstraction and Hierarchy (OOPSLA ‘87 Addendum to the Proceedings)", раздел "6.2. Multiple Implementations" (стр.16):
It is often useful to have multiple implementations of the same type. For example, for some matrices we use a sparse representation and for others a nonsparse representation. Furthermore, it is sometimes desirable to use objects of the same type but different representations within the same program.
Object-oriented languages appear to allow users to simulate multiple implementations with inheritance. Each implementation would be a subclass of another class that implements the type. This latter class would probably be virtual; for example, there would be a virtual class implementing matrices, and subclasses implementing sparse and nonsparse matrices.
Using inheritance in this way allows us to have several implementations of the same type in use within the same program, but it interferes with type hierarchy. For example, suppose we invent a subtype of matrices called extended-matrices. We would like to implement extended-matrices with a class that inherits from matrices rather than from a particular implementation of matrices, since this would allow us to combine it with either matrix implementation. This is not possible, however. Instead, the extended-matrix class must explicitly state in its program text that it is a subclass of sparse or nonsparse matrices.
The problem arises because inheritance is being used for two different things: to implement a type and to indicate that one type is a subtype of another. These uses should be kept separate. Then we could have what we really want: two types (matrix and extended-matrix), one a subtype of the other, each having several implementations, and the ability to combine the implementations of the subtype with those of the supertype in various ways.
Что в моем вольном переводе на русском языке звучит так:
Часто бывает необходимо иметь несколько реализаций одного типа. Например, для некоторых матриц мы используем разреженное представление, а для других обычное. Более того, иногда бывает нужно использовать в программе объекты одного типа, но с разными реализациями.
Кажется, что объектно-ориентированные языки позволяют пользовать иметь множественные реализации за счет наследования. Каждая конкретная реализация будет подклассом другого класса, выступающего в качестве типа. Последний, вероятно, будет виртуальным классом. Например, может быть виртуальный класс описывающий тип "матрица", а его наследники будут реализовывать разреженные и обычные матрицы.
Использование наследования подобным образом позволяет нам иметь несколько реализаций одного типа в той же самой программ, но это пересекается с иерархией типов. Например, представим что мы придумали новый тип "расширенная матрица". Нам бы хотелось реализовать "расширенную матрицу" посредством класса, который наследуется от "матрица", а не от конкретной реализации типа "матрица", т.к. это позволило бы нам комбинировать наш новый тип матрицы с уже имеющимися реализациями. Однако это невозможно. Вместо этого класс "расширенная матрица" в исходном коде программы должен явно указать, наследуется ли он от "разреженной" или "обычной" матрицы.
Проблема возникает из-за того, что наследование используется для выражения срезу двух вещей: и для реализации типа, и для указания того, что один тип является подтипом другого. Но эти вещи следует разделять. Тогда мы сможем иметь то, что хотим: два типа ("матрица" и "расширенная матрица") где один является подтипом другого, и возможность произвольно комбинировать реализации подтипов там, где ожидается супертип.
Так вот, я не понимаю почему Барбара Лисков сделала утверждение, что в рамках ООЯ мы не можем отнаследовать "расширенную матрицу" просто от класса "матрица" забив на существование подклассов "разреженная матрица" и "обычная матрицы", которые также наследуются от класса "матрица".
Очень смущает, что статья от 1987-го года, и там в списке литературы нет ни одной ссылки на описание таких языков программирования, как Eiffel (1986) и C++ (1985). Есть упоминание разве что SmallTalk, но в SmallTalk, насколько я помню, вообще не было понятия "абстрактный базовый тип" (или "интерфейс" в Java-подобных языках). Тогда как и в Eiffel, и в C++ понятие "абстрактного (базового) типа" как раз есть и в Eiffel/C++ мы запросто можем решить озвученную проблему имея просто абстрактный тип matrix и отнаследованный от него абстрактный тип extened_matrix.
И, как мне кажется, появление в Eiffel/C++ (а потом и в их наследниках) понятия "абстрактный (базовый) тип" как раз и позволило получить то самое разделение, о котором и говорила Барбара: отношение наследования абстрактных базовых типов как раз определяет отношение "тип является подтипом другого", тогда как наследник, реализующий абстрактный базовый тип, предоставляет реализацию. И таких реализаций может быть несколько. И они могут комбинироваться в произвольных вариациях до тех пор, пока мы манипулируем именно базовыми абстрактными типами.
В общем, требуется пояснительная бригада. Желательно с ссылками на публикации, в которых этот тезис Барбары Лисков разбирается (может быть даже самой Барбарой в последующих работах).
Как я понял, ключевая проблема вот в этой фразе "... and the ability to combine the implementations of the subtype with those of the supertype in various ways."
ОтветитьУдалитьТо есть хочется сделать реализацию расширенной матрицы ExtendedSparseMatrix, воспользовавшись реализацией ExtendedMatrix
При такой иерархии:
class IMatrix { ... }; // интерфейс (type) матрицы
class IExtendedMatrix : public IMatrix { ... }; // расширение (subtype) матрицы
class SparseMatrix : public IMatrix {...}; // конкретная реализация типа матрицы
class ExtendedSparseMatrix : public IExtendedMatrix {...}; // конкретная реализация расширения типа матрицы
наследование не даёт удобного механизма переиспользовать SparseMatrix для реализации ExtendedSparseMatrix. Прямолинейный подоход отнаследовать ExtendedSparseMatrix от SparseMatrix не подойдёт.
@Константин
ОтветитьУдалитьНу вот я не уверен, что хочется сделать именно реализацию ExtendedSparseMatrix. Речь вроде бы идет о том, что у нас есть IMatrix и пара наследников: SparseMatrix и NonsparseMatrix. А потом у нас появляется надобность в IExtendedMatrix.
Так вот для реализации IExtendedMatrix нам в принципе не нужно вообще иметь дела с уже имеющимися SparseMatrix и NonsparseMatrix. Мы запросто может сделать совсем новую реализацию.
Ведь у той фразы, которую вы процитировали есть очень важное начало: "We would like to implement extended-matrices with a class that inherits from matrices rather than from a particular implementation of matrices, since this would allow us to combine it with either matrix implementation"
Акцент на "with a class that inherits from matrices rather than from a particular implementation of"
PS. В блоге включена премодерация комментариев, поэтому они могут появляться сильно не сразу их отправки.
Как я понял, и интерфейс, и классы обычной и разреженной матрицы являются типами с выстроенной иерархией, тогда как расширенная матрица - тоже должна быть типом, а значит она может наследоваться и от обычной матрицы, и от разреженной. Ведь расширенная матрица не занимается делами обычной и разреженной, но предполагает необходимость этого функционала для имплементаций. Вы правильно говорите, что нужно расширенную матрицу убрать из иерархии наследования, но в таком случае типами становятся только интерфейсы, а обычная и разреженная матрицы не могут выступать обобщёнными типами для других подтипов.
ОтветитьУдалитьПохожая ситуация возникает ещё когда нам нужно убрать часть функционала из наследника. Например, квадрат должен быть подтипом прямоугольника, но для хранения длины сторон ему нужно одно значение, а не два.
@XX
ОтветитьУдалитьЯ не очень понял откуда в исходной статье взялись отдельные понятия "типа" и "интерфейса".
Ну и про "обобщенные типы" в 1987-ом году речь вряд ли могла идти в контексте разговора про наследование.