![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Действительно, надо на время забросить расистский, шовинистический, антисемитский и всякий другой необщепринятый, хотя и небесполезный в нашем нетривиальном мире, дискурс, и попинать немножко ногами что-нидь из программирования. Пинать я буду не голословно, а приводя малоизвестные и интересные (очень надеюсь) примеры.
Самая мерзкая вещь, которую я только знаю в ООП - это так называемое "наследование". То, что наследование нахрен не нужно, можно понять, изучив COM, прекрасно себе работающий без всякого "наследования базовых классов", а лишь посредством имплементации интерфейсов (первейшее средство обеспечения полиморфизма, между прочим). Инкапсуляция и Полиморфизм_посредством_имплементации_интерфейсов_а_не_посредством_чего_нибудь_ещё - вот, что такое ООП. "Наследование" же затесалось в представление об ООП по чистому недоразумению.
Теперь к конкретным примерам.
Когда программист "наследует" класс A от класса Base, он как бы говорит как минимум две вещи сразу. Во-первых, он говорит, что класс A - это частный случай, разновидность, субкласс класса Base (как, к примеру, учитель - частный случай человека), всё, что можно сделать с классом Base, можно сделать и с классом A, везде, где используется класс Base, можно использовать и экземпляр класса А. Во-вторых, он говорит, что имплементация класса A использует имплементацию класса Base. Тоже немаловажно - уж кто из нас не расширял функциональность класса, наследуя от неё новую, более продвинутую функциональность, использующую и старую.
Так вот, всё это очень спорная практика.
Поскольку уже в самых основах можно видеть, что подобное "двойное" наследование в одну сторону вовсе не всегда то, что нужно. Рассмотрим, к примеру, в качестве такой "основы" тип действительных и комплексных чисел. Ясно совершенно, что действительное число - частный случай комплексного. Поэтому, если оформлять их в виде классов, то действительный тип должен наследовать комплексный. Именно так и не иначе. Комплексный тип - базовый. А вот с имплементацией всё ровно наоборот, разумеется, - комплексный тип, скорее уж, использует имплементацию действительного типа (что, впрочем, в данном случае разумнее оформить не через наследование имплементации "в другую сторону", но это частности).
Представление о том, что производный класс - разновидность базового и что именно из представления о том, что является разновидностью чего, следует исходить в ОО-дизайне, вполне укоренено. А между тем, всё куда сложнее и горше.
Хотя квадрат и является разновидностью прямоугольника (не говоря уже о том, что и ромба тоже), поспешно было бы наследовать квадрат от прямоугольника. И проблема тут даже не в том, что квадрату вовсе не нужно, как прямоугольнику, хранить и длину и ширину (мы уже обсудили абзацем выше, что не всегда направление наследования интерфейса и направление наследования имплементации совпадают). Проблема здесь в том, что прямоугольник в программировании и прямоугольник в математике вовсе не одно и то же. Как уже было сказано в прошлых постах ЖЖ, в математике "нет оператора присваивания", нет переменных. В програмировании же всё с точностью до наоборот. И если у нашего прямоугольника имеется метод
растягивающий его по ширине и высоте, то квадрат этот метод уже никак (адекватным образом) имплементировать не сможет. Неконстантный квадрат - вовсе не частный случай неконстантного прямоугольника.
[* Конец гона *]
А вот завтра пойду на работу и опять чего-нибудь унаследую.

Самая мерзкая вещь, которую я только знаю в ООП - это так называемое "наследование". То, что наследование нахрен не нужно, можно понять, изучив COM, прекрасно себе работающий без всякого "наследования базовых классов", а лишь посредством имплементации интерфейсов (первейшее средство обеспечения полиморфизма, между прочим). Инкапсуляция и Полиморфизм_посредством_имплементации_интерфейсов_а_не_посредством_чего_нибудь_ещё - вот, что такое ООП. "Наследование" же затесалось в представление об ООП по чистому недоразумению.
Теперь к конкретным примерам.
Когда программист "наследует" класс A от класса Base, он как бы говорит как минимум две вещи сразу. Во-первых, он говорит, что класс A - это частный случай, разновидность, субкласс класса Base (как, к примеру, учитель - частный случай человека), всё, что можно сделать с классом Base, можно сделать и с классом A, везде, где используется класс Base, можно использовать и экземпляр класса А. Во-вторых, он говорит, что имплементация класса A использует имплементацию класса Base. Тоже немаловажно - уж кто из нас не расширял функциональность класса, наследуя от неё новую, более продвинутую функциональность, использующую и старую.
Так вот, всё это очень спорная практика.
Поскольку уже в самых основах можно видеть, что подобное "двойное" наследование в одну сторону вовсе не всегда то, что нужно. Рассмотрим, к примеру, в качестве такой "основы" тип действительных и комплексных чисел. Ясно совершенно, что действительное число - частный случай комплексного. Поэтому, если оформлять их в виде классов, то действительный тип должен наследовать комплексный. Именно так и не иначе. Комплексный тип - базовый. А вот с имплементацией всё ровно наоборот, разумеется, - комплексный тип, скорее уж, использует имплементацию действительного типа (что, впрочем, в данном случае разумнее оформить не через наследование имплементации "в другую сторону", но это частности).
Представление о том, что производный класс - разновидность базового и что именно из представления о том, что является разновидностью чего, следует исходить в ОО-дизайне, вполне укоренено. А между тем, всё куда сложнее и горше.
Хотя квадрат и является разновидностью прямоугольника (не говоря уже о том, что и ромба тоже), поспешно было бы наследовать квадрат от прямоугольника. И проблема тут даже не в том, что квадрату вовсе не нужно, как прямоугольнику, хранить и длину и ширину (мы уже обсудили абзацем выше, что не всегда направление наследования интерфейса и направление наследования имплементации совпадают). Проблема здесь в том, что прямоугольник в программировании и прямоугольник в математике вовсе не одно и то же. Как уже было сказано в прошлых постах ЖЖ, в математике "нет оператора присваивания", нет переменных. В програмировании же всё с точностью до наоборот. И если у нашего прямоугольника имеется метод
void Scale(real factorX, real factorY);
, растягивающий его по ширине и высоте, то квадрат этот метод уже никак (адекватным образом) имплементировать не сможет. Неконстантный квадрат - вовсе не частный случай неконстантного прямоугольника.
[* Конец гона *]
А вот завтра пойду на работу и опять чего-нибудь унаследую.
Re: частичный ответ
Date: 2004-06-03 01:16 am (UTC)About rectangles and squares: I don't think "square" should even be a class. If you really need this functionality, the "rectangle" class can have a constructor that creates a square. Subclasses should not violate the contract of the superclass, this is a key requirement in OO design.
Numbers: I don't think real and complex numbers have a subclass-superclass relationship or vice versa. They're just separate entities, and you should use always explicit methods to convert between them - it makes for less confusion. From a set theory point of view, they definitely are related; but just think about the monstrous overloading of arithmetical operations. real*real = real; real*complex = complex; complex*complex can be real, but we can't tell it by checking the class, due to computational errors; etc.
This is, again, not the point of OO. OO starts to break when we consider binary operations. For a related example, consider the String class. It has a "substring" method. Now if we subclass String, an object of what class should "substring" return? What about matching a subclass of String with a subclass of Regexp? OO just isn't universal, live with it.
Re: частичный ответ
Date: 2004-06-05 04:01 am (UTC)Это верно, но по какому пункту нашей дискуссии или моего ответа Вы это говорите, я не смог понять.
> If you really need this functionality, the "rectangle" class can have a constructor that creates a square.
Вы позабыли про ромбы. Они тоже могут быть квадратами. Что до утверждения, что квадраты не должны быть классами, я бы тут не согласился. Вполне себе должны. Но, увы, не могут. По крайней мере, неконстантные.
> Subclasses should not violate the contract of the superclass, this is a key requirement in OO design.
Золотые слова. Но от них в данном случае не легче.
> Numbers: I don't think real and complex numbers have a subclass-superclass relationship or vice versa. They're just separate entities
Конечно же, нет. Любое действительное число определенно может быть использовано в качестве аргумента функции комплексного аргумента.
> complex*complex can be real, but we can't tell it by checking the class, due to computational errors
Прежде всего, мы вполне можем себе позволить не озаботиться данным случаем, то есть, считать, что complex*complex => complex. И тем не менее, желать иметь вменяемую систему иерархии численных типов. Это наше законное право.
Кроме того, система типов и точность вычислений - понятия ортогональные, мы вполне можем себе позволить рассмотреть систему типов при наличии воображаемой машины, осуществляющей точные вычисления.
Кроме того, мы можем рассмотреть систему "целых" комплексных чисел
Кроме того, мы можем реализовать ТОЧНУЮ рациональную арифметику и полностью избавиться от проблемы погрешности для некоторого класса вычислений даже на обычной машине.
> OO starts to break when we consider binary operations.
А бинарные операции здесь вообще не при чем. Проблема иерархии комплексных и действительных чисел вполне проявляет себя уже на унарных операциях.
> an object of what class should "substring" return
Я полагаю, что если есть желание, можно вернуть класс substring, не нарушив при этом инвариантов базового интерфейса. Разумеется, если имплементация такова, что возращается substring. Если же и производный класс возращает String - это еще проще, разумеется.
> What about matching a subclass of String with a subclass of Regexp
Непонятный пример.
> OO just isn't universal, live with it
И это тоже. Но некоторые из проблем, что я обсуждал, в рамках ОО вполне решаемы. Но не в рамказ парадигмы обычного наследования, что включает в себя одновременно как наследование интерфейса, так и наследование имплементации.