Истинное наследование или агрегация
Один из ключевых "столпов" ООП - наследование. Основная идея в наследовании интерфейсов или реализации базового класса. Я в своей жизни сталкивался с двумя идеями реализации наследования:
Итак, наследование и агрегация.
У каждого из них есть достоинства и недостатки. Наследование обычно прямо поддерживаеться языковыми средствами (сам язык, IDE, отладчики) и естественно воспринимается разрабочиками, а агрегирование приходится "привязывать" сбоку (хотя в расширяемых языках, например Лисп, эту задачу можно решать только один раз) и, что хуже, - объяснять идею многим разработчикам. В двух словах, идею агрегации можно свести к созданию обертки вокруг класса, в каждом методе которой мы вызываем соответствующий метод наследуемого класса, "обернув" его дополнительным кодом, или подменяем своей реализацией (подробнее читать в MSDN: Containment, Aggregation). Все дальнейшие рассуждения я привожу по-отношению к строго типизированным, компилирующим языкам (C#, C++, Delphi, Java, etc).
Мощность (предоставляемые возможности)
Как наследование, так и агрегирование, позволяет переопределять унаследованую от родительского класса ф-сть. При наследовании для этого используется механизм виртуальных методов, а потому мы можем переопределить только ту ф-сть, которую разработчик поместил в виртуальные методы. Невиртуальные методы обычно можно скрыть новым невиртуальным методом, но они по-прежнему доступны с приведением типов.
Механизм агрегаций не так требователен к базовому классу. В нем можно переопределить любой метод, но это переопределение не столь сильно как при наследовании. Слабость в том, что переопредение не действует на сам базовый класс. То есть, когда он вызывает собственный метод, который мы переопределили в обертке, он вызывает изначальную версию - непереопределенную. Этого можно избежать, если базовый класс будет знать о том что его обернули, и в этом случае вызывать обертку, а не напрямую (подумайте о варианте, когда наследник в своем коде вызывает базовую реализацию). Примерно такой подход избрали разработчики COM.
Скорость работы
Переопределение метода при наследовании, это не что иное, как замена одного указателя в таблице виртуальных методов другим. Скорость вызова при этом постоянна, независимо от количества переопрелений конкретной ф-ии. Конечно, если новая ф-ия вызывает старую реализацию в своем коде, то время выполнения суммируется + время поиска в таблице виртуальных методов.
Переопределение при агрегации - замена одного класса другим! Там где, при наследовании, требуется поиск в таблице виртуальных методов, при агрегации происходит просто вызов ф-ии по-указателю на нее. Это для переопределяемых методов.
Мы помним, что не все ф-ии виртуальные и и при наследовании у нас остается какое-то количество непереопределенных ф-ий. Что с ними? При неследовании, это просто вызов ф-ии по ее адресу, независимо от количества наследников. Скорость постоянна.
При агрегации мы обязанны переопределить все ф-ии, пусть даже только для того, чтобы вызвать старую реализацию. То есть, для непереопределяемых ф-ий каждый раз мы имеем дополнительный коссвенный вызов.
- Истинное наследование (в стиле C++, Delphi)
- Агрегация или включение (в стиле Visual Basic и COM вообще)
Итак, наследование и агрегация.
У каждого из них есть достоинства и недостатки. Наследование обычно прямо поддерживаеться языковыми средствами (сам язык, IDE, отладчики) и естественно воспринимается разрабочиками, а агрегирование приходится "привязывать" сбоку (хотя в расширяемых языках, например Лисп, эту задачу можно решать только один раз) и, что хуже, - объяснять идею многим разработчикам. В двух словах, идею агрегации можно свести к созданию обертки вокруг класса, в каждом методе которой мы вызываем соответствующий метод наследуемого класса, "обернув" его дополнительным кодом, или подменяем своей реализацией (подробнее читать в MSDN: Containment, Aggregation). Все дальнейшие рассуждения я привожу по-отношению к строго типизированным, компилирующим языкам (C#, C++, Delphi, Java, etc).
Мощность (предоставляемые возможности)
Как наследование, так и агрегирование, позволяет переопределять унаследованую от родительского класса ф-сть. При наследовании для этого используется механизм виртуальных методов, а потому мы можем переопределить только ту ф-сть, которую разработчик поместил в виртуальные методы. Невиртуальные методы обычно можно скрыть новым невиртуальным методом, но они по-прежнему доступны с приведением типов.
Механизм агрегаций не так требователен к базовому классу. В нем можно переопределить любой метод, но это переопределение не столь сильно как при наследовании. Слабость в том, что переопредение не действует на сам базовый класс. То есть, когда он вызывает собственный метод, который мы переопределили в обертке, он вызывает изначальную версию - непереопределенную. Этого можно избежать, если базовый класс будет знать о том что его обернули, и в этом случае вызывать обертку, а не напрямую (подумайте о варианте, когда наследник в своем коде вызывает базовую реализацию). Примерно такой подход избрали разработчики COM.
Скорость работы
Переопределение метода при наследовании, это не что иное, как замена одного указателя в таблице виртуальных методов другим. Скорость вызова при этом постоянна, независимо от количества переопрелений конкретной ф-ии. Конечно, если новая ф-ия вызывает старую реализацию в своем коде, то время выполнения суммируется + время поиска в таблице виртуальных методов.
Переопределение при агрегации - замена одного класса другим! Там где, при наследовании, требуется поиск в таблице виртуальных методов, при агрегации происходит просто вызов ф-ии по-указателю на нее. Это для переопределяемых методов.
Мы помним, что не все ф-ии виртуальные и и при наследовании у нас остается какое-то количество непереопределенных ф-ий. Что с ними? При неследовании, это просто вызов ф-ии по ее адресу, независимо от количества наследников. Скорость постоянна.
При агрегации мы обязанны переопределить все ф-ии, пусть даже только для того, чтобы вызвать старую реализацию. То есть, для непереопределяемых ф-ий каждый раз мы имеем дополнительный коссвенный вызов.
Коментарі
А для некоторых методов нужно создать специальную таблицу, и писать подмены в нее, оставив в качестве метода класса функцию, которая подберет наиболее удачную реализацию из тех,
которые описаны в доп. таблице....
вобщем, дальше мы получаем позднее связывание (как оно есть на самом деле, но скрывается от программиста) и фактическое вынесение метода за пределы класса...
Если у тебя в руках молоток, то все проблемы кажутся гвоздями. Как ни крути, но все сводится к упаковке параметров, определению адреса функции и ее вызову. Только выразить это можно разными словами. Синтаксис C полон частностей, которые казались важными 30 лет назад, C++ их перенял из маркетинговых соображений (абсолютно не конструктивно) и сделал ООП пляской с бубном: подразумеваемая логика (которую вставляет в компилятор) сложна и не может быть адаптирована, решения становятся излишне сложными а шаблоны проектирования превращаю маленькую задачу, в проблему.
В языках типа C#, где нету законной возможности пролочить компилятору череп, все гораздо хуже: вместо решения пославленой задачи, решаем задачу поиска нужной комбинации шаблонов, которая дает минимальное падение производительности.
--
Pilya.
Мне кажется, что сегодня есть необходимость строить "динамические", "адаптивные", "легко-разборные" конструкции (программы, библиотеки, системы), и для этого нужно выбирать адекватные средства.
делема между наследованием и агрегацией, это только маленький кусочек большой проблемы.
Что стот за интерфейсами: форма или семантика?
Что применять: интерфейс или протокол?
Если вместо явного вызова конструкторов использовать фабрику, то не появится ли фабрика фабрик?
ну и т.д.
--
Pilya.
MyObject a = new MyOjbects(a, b, c);
и
MyObject a = MyObjectFactory.Create(a, b, b);
логически эквивалентны с разумным допуском. Не думаю, что различный синтаксис есть проблемой (китайцы все едят палочками и считают это правильным, а европейцы успешно осваивают целых 2 инструмента - вилку и ложку и точно также успешно страдают от ожирения). В любом(!) современном языке легко(!) решаються все(!) проблемы, встающие перед программистом. Просто в одном языке это ложиться в уже существующие языковые конструкции, а в другом порождает новые (как минимум - новые библиотеки).
Quote:
Мне кажется, что сегодня есть необходимость строить "динамические", "адаптивные", "легко-разборные" конструкции (программы, библиотеки, системы), и для этого нужно выбирать адекватные средства.
Вы будете смеяться, но в первой книге по ООП, которую мне довелось прочесть, именно такими эпитетами описывалось выгодное отличие ООП от функционального программирования. О чем это говорит? Мне кажеться, о том, что нет "серебряной пули". Нет такого средства, которое дает ответы на все вопросы, да еще и удобно. Именно поэтому, считаю правильным разумное совмещение различных подходов. В то же время, "динамические, легкие и легко-разборные" - это не только о программировании. Люди стремятся к таким решениям во всех сферах своей деятельности. Но надо помнить, что хотя у боллида Формулы-1 можно произвести замену колес и двигателя в чистом поле за 5 минут, у Mercedes 500 SLK для той же операции нужно несколько дней и хорошо оборудованая станция. И именно SLK является "коробочным" продуктом, а боллид - разовым.
Quote:
Что стот за интерфейсами: форма или семантика?
Что применять: интерфейс или протокол?
Если вместо явного вызова конструкторов использовать фабрику, то не появиться ли фабрика фабрик?
Из этих вопросов, только на последний можно ответить однозначно в широком смысле - появиться! Ответы на первые 2 вопроса нужно искать применительно к задаче, опыту и личным предпочтениям.
И последнее, основная заметка была приведена для объяснения разницы между наследованием и агрегацией, а не для разжигания религиозных войн. Не будем уподобляться бородатому анекдоту о строении блохи.
Экзамен по зоологии на ветеринарном факультете. Один из студентов выучил только один вопрос - "Строение блохи".
Удачно вытащил соответствующий вопрос, но получил 2 дополнительных вопроса: "Строение собаки" и "Строение рыбы". Ответы:
1. Собака - это млекопитающее. У нее есть шерсть. В шерсти водятся блохи. Итак, строение блохи...
2. Рыба. У рыбы есть чешуя. Шерсти у нее нет. А вот если бы была, то там водились бы блохи. Строение блохи...
Речь не о том, что шаблоны плохие или код должен быть хорошим, а продукт выпущен.
Когда-то проводили эксперимент с мартышками (может и не проводили): в клетке с десятком мартышек положили банан, но как только кто-то пытался его взять, остальных тут же поливали холодной водой. Мартыхи научились, что брать бананы нельзя, и как только кто-то таки пытался брать банан, остальные нарушителя били. Водой уже не поливали.
Потом начали по одной мартыхе менять. Деды их быстро отвадили от бананов. В какой-то момент, уже не осталось ни одной обезьяны, которую когда-либо поливали водой из шланга, но в клетке бытовало табу на банан, и всех новеньких этому табу учили.
Короче, если "менять языковые конструкции" - это табу, то можно на нем строить церковь, держать жрецов, воспитывать адептов и даже вести религиозные войны.
С точки зрения "догматически-ограниченного программирования" мои слова звучат как ересь. Смерть еретикам! :-)
--
Pilya, Еретик.