Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Проектування інформаційних систем.doc
Скачиваний:
95
Добавлен:
21.09.2019
Размер:
28.77 Mб
Скачать

15.4.3. Успадкування

Приклади. Космічні зонди, що перебувають у польоті, відсилають на наземні станції інформацію про стан своїх основних систем (наприклад, джерел енергопостачання й двигунів) і вимір датчиків (таких як датчики радіації, мас-спектрометри, телекамери, фіксатори зіткнень із мікрометеоритами й т.д.). Вся сукупність переданої інформації називається телеметричними даними. Як правило, вони передаються у вигляді потоку даних, що складає із заголовка (який включає тимчасові мітки й ключі для ідентифікації наступних даних) і декількох пакетів даних від підсистем і датчиків. Все це виглядає як простий набір різнотипних даних, тому для опису кожного типу даних телеметрії напрошуються структури:

class Time...

struct ElectricalData {

Time timeStamp;

int id;

float fuelCell1Voltage, fuelCell2Voltage;

float fuelCell1Amperes, fuelCell2Amperes;

float currentPower;

};

Однак такий опис має ряд недоліків. По-перше, структура класу ElectricalData не захищена, тобто клієнт може викликати зміну такої важливої інформації, як timeStamp або currentPower (потужність, що розвивається обома електробатареями, яку можна обчислити зі струму й напруги). По-друге, ця структура є повністю відкритою, тобто її модифікація (додавання нових елементів у структуру або зміна типу існуючих елементів) впливають на клієнтів. Як мінімум, доводиться заново компілювати всі описи, зв'язані яким-небудь чином із цією структурою. Ще важливіше, що внесення в структуру змін може порушити логіку відношень із клієнтами, а отже, логіку всієї програми. Крім того, наведений опис структури дуже складий для сприйняття. Стосовно такої структури можна виконати множину різних дій (пересилання даних, обчислення контрольної суми для визначення помилок і т.д.), але всі вони не будуть пов'язані з наведеною структурою логічно. Нарешті, припустимо, що аналіз вимог до системи обумовив наявність декількох сотень різновидів телеметричних даних, що включають показану вище структуру й інші електричні параметри в різних контрольних точках системи. Очевидно, що опис такої кількості додаткових структур буде надлишковим як через повторюваність структур, так і через наявність загальних функцій опрацювання.

Краще було б створити для кожного виду телеметричних даних окремий клас, що дозволить захистити дані в кожному класі й погодити їх з виконуваними операціями. Але цей підхід не вирішує проблему надмірності.

Значно краще побудувати ієрархію класів, в якій від загальних класів за допомогою успадкування утворяться спеціалізованіші класи; наприклад, у такий спосіб:

class TelemetryData {

public:

TelemetryData();

virtual ~TelemetryData();

virtual void transmit();

Time currentTime() const;

protected:

int id;

Time timeStamp;

};

У цьому прикладі введений клас, який має конструктор, деструктор (який нащадки можуть перевизначити) і функції transmit і currentTime, які видні всім клієнтів. Захищені елементи id і timeStamp трохи краще інкапсульовані - вони доступні тільки класу і його підкласам. Зауважте, що функція currentTime є відкритою, завдяки чому значення timeStamp можна читати (але не змінювати).

Тепер розглянемо ElectricalData:

class ElectricalData : public TelemetryData {

public:

ElectricalData(float v1, float v2, float a1, float a2);

virtual ~ElectricalData();

virtual void.transmit();

float currentPower() const;

protected:

float fuelCell1Voltage, fuelCell2Voltage;

float fuelCell1Amperes, fuelCell2Amperes;

};

Цей клас - спадкоємець класу TelemetryData, але вихідна структура доповнена (чотирма новими елементами), а поведінка - перевизначена (змінена функція transmit). Крім того, додана функція currentPower.

Одиночне успадкування. Успадкування - це таке відношення між класами, коли один клас повторює структуру й поведінку іншого класу (одиночне успадкування) або інших (множинне успадкування) класів. Клас, структура й поведінка якого успадковуються, називається суперкласом. Так, TelemetryData є суперкласом для ElectricalData. Похідний від суперкласу клас називається підкласом. Це означає, що успадкування встановлює між класами ієрархію загального й часткового. У цьому змісті ElectricalData є спеціалізованішим класом загальнішого класу TelemetryData. Ми вже бачили, що в підкласі структура й поведінка вихідного суперкласу доповнюються й перевизначаються. Наявність механізму успадкування відрізняє об’єктно-орієнтовані мови від об'єктних.

Підклас розширює або обмежує існуючу структуру й поведінку свого суперкласу. Наприклад, підклас GuardedQueue може додавати до поведінки суперкласу Queue операції, які захищають стан черги від одночасної зміни декількома незалежними потоками. Зворотний приклад: підклас UnselectableDisplayItem може обмежити поведінку свого суперкласу DisplayItem, заборонивши виділення об'єкта на екрані. Часто підкласи роблять і те, і інше.

Відношення одиночного успадкування від суперкласу TelemetryData показані на рис. 15.5. Стрілки позначають відношення загального до часткового. Зокрема, Cameradata - це різновид класу SensorData, який у свою чергу є різновидом класу TelemetryData. Такий же тип ієрархії характерний для семантичних мереж, які часто використовуються фахівцями з розпізнавання образів і штучного інтелекту для організації баз знань. У розділі 33 ми покажемо, що правильна організація ієрархії абстракцій - це питання логічної класифікації.

Рис. 15.5. Одиночне спадкування.

Для деяких класів на рис. 15.5 можна створити екземпляри, а для інших - ні. Найімовірніше утворення об'єктів самих спеціалізованих класів ElectricalData і SpectrometerData (такі класи називають конкретними класами, або листками ієрархічного дерева). Утворення об'єктів із класів, що займають проміжне положення (SensorData або TelemetryData), менш імовірно. Класи, екземпляри яких не створюються, називаються абстрактними. Підкласи абстрактних класів довизначають їх до життєздатної абстракції, наповнюючи клас змістом. У мові Smalltalk розробник може змусити підклас перевизначити метод, поміщаючи в реалізацію методу суперкласу виклик методу SubclassResponsibility. Якщо метод не перевизначений, то при спробі його виконання генерується помилка. Аналогічно, в C++ існує можливість повідомляти функції віртуальними. Якщо вони не перевизначені, екземпляр такого класу неможливо створити.

Самий загальний клас в ієрархії класів називається базовим. У більшості інформаційних систем базових класів буває декілька, і всі вони відбивають найзагальніші абстракції предметної області. Насправді, особливо в C++, добре зроблена структура класів - це скоріше ліс із дерев успадкування, ніж одна багатоповерхова структура успадкування з одним коренем. Але в деяких мовах програмування є базовий клас самого верхнього рівня, що є єдиним суперкласом для всіх інших класів. У мові Smalltalk цю роль грає клас object.

Клас зазвичай має два види клієнтів:

  • екземпляри;

  • підкласи.

Корисно мати для них різні інтерфейси. Зокрема, ми хочемо показати тільки зовні видиму поведінку для клієнтів-екземплярів, але нам потрібно відкрити службові функції й подання клієнтам-підкласам. Цим пояснюється наявність відкритої, захищеної і закритої частин опису класів в мові C++: розробник може чітко розділити, які елементи класу доступні для екземплярів, а які для підкласів. У мові Smalltalk ступінь такого поділу менша: дані видимі для підкласів, але не для екземплярів, а методи загальнодоступні (можна запровадити закриті методи, але мова не забезпечує їхній захист).

Є серйозні протиріччя між потребами успадкування й інкапсуляції. Значною мірою успадкування відкриває клас, що успадковує, деякі секрети. На практиці, щоб зрозуміти, як працює якийсь клас, часто треба вивчити всі його суперкласи в їх внутрішніх деталях.

Успадкування розуміє, що підкласи повторюють структури їх суперкласів. У попередньому прикладі екземпляри класу ElectricalData містять елементи структури суперкласу (id і timeStamp) і спеціалізованіші елементи (fuelCell1Voltage, fuelCell2Voltage, fuelCell1Amperes, fuelCell2Amperes).

Поведінка суперкласів також успадковується. Стосовно об'єктів класу ElectricalData можна використовувати операції currentTime (успадкована від суперкласу), currentPower (визначена в класі) і transmit (перевизначена в підкласі). У більшості мов допускається не тільки успадковувати методи суперкласу, але також додавати нові і перевизначати існуючі методи. В Smalltalk будь-який метод суперкласу можна перевизначити в підкласі.

В C++ ступінь контролю за цим трохи вища. Функція, оголошена віртуальною (функція transmit у попередньому прикладі), може бути в підкласі перевизначена, а інші (currentTime) - ні.

Одиночний поліморфізм. Нехай функція transmit класу TelemetryData реалізована в такий спосіб:

void TelemetryData::transmit()

{

// передати id

// передати timeStamp

};

У класі ElectricalData та ж функція перевизначена:

void ElectricalData::transmit()

{

TelemetryData::transmit();

// передати напругу

// передати силу струму

};

Ця функція спочатку викликає однойменну функцію суперкласу за допомогою її явно кваліфікованого імені TelemetryData::transmit(). Та передасть заголовок пакету (id і timeStamp), після чого в підкласі передаються його власні дані.

Визначимо тепер екземпляри двох описаних вище класів:

TelemetryData telemetry;

ElectricalData electrical(5.0, -5.0, 3.0, 7.0);

Тепер визначимо вільну процедуру:

void transmitFreshData (TelemetryData& d, const Time& t)

{

if (d.currentTime() >= t)

d.transmit();

);

Що відбудеться, якщо виконати такі два оператори?

transmitFreshData(telemetry, Time(60));

transmitFreshData(electrical, Time(120));

У першому операторі буде передано вже відомий нам заголовок. У другому буде переданий він же, плюс чотири числа у форматі із плаваючою крапкою, що містять результати вимірів електричних параметрів. Чому це так? Ніби то функція transmitFreshData нічого не знає про клас об’єкта, вона просто виконує d.transmit()! Це був приклад поліморфізму. Змінна d може позначати об’єкти різних класів. Ці класи мають спільний суперклас і вони, хоча й по різному, можуть реагувати на одне і те ж повідомлення, однаково розуміючи його зміст.

Традиційні типізовані мови типу Pascal засновані на тій ідеї, що функції й процедури, а отже, і операнди повинні мати певний тип. Ця властивість називається мономорфізмом, тобто кожна змінна й кожне значення ставляться до одного певного типу. На противагу мономорфізму поліморфізм допускає віднесення значень і змінних до декількох типів. Вперше ідею поліморфізму ad hoc описав Страчі, маючи на увазі можливість перевизначати зміст символів, таких, як "+", згідно потреби. У сучасному програмуванні ми називаємо це перевантаженням. Наприклад, в C++ можна визначити кілька функцій з тим самим іменем, і вони будуть автоматично відрізнятися за кількістю й типами своїх аргументів. Сукупність цих ознак називається сигнатурою функції; у мові Ada до цього списку додається тип значення, що повертається. Страчі говорив також про параметричний поліморфізм, який ми зараз називаємо просто поліморфізмом.

При відсутності поліморфізму код програми змушений містити багато операторів вибору case або switch. Наприклад, мовою Pascal неможливо утворити ієрархію класів телеметричних даних; замість цього прийдеться визначити один великий запис із варіантами, який включатиме всі різновиди даних. Для вибору варіанту потрібно перевірити мітку, що визначає тип запису. Мовою Pascal процедура TransmitFreshData може бути написана в таким чином:

const

Electrical = 1;

Propulsion = 2;

Spectrometer = 3;

Procedure Transmit_Presh_Data(TheData: Data; The_Time: Time);

begin

if (The_Data.Current_Time >= The_Time)

then

case TheData.Kind of

Electrical: Transmit_Electrical_Data(The_Data);

Propulsion: Transmit_Propulsion_Data(The_Data);

end;

end;

Щоб ввести новий тип телеметричних даних, потрібно модифікувати цей варіантний запис, додавши новий тип у кожний оператор case. У такій ситуації збільшується ймовірність помилок, і проект стає нестабільним.

Успадкування дозволяє розрізняти різновид абстракцій, і монолітні типи стають не потрібні. Поліморфізм найдоцільніший у тих випадках, коли кілька класів мають однакові протоколи. Поліморфізм дозволяє обійтися без операторів вибору, оскільки об'єкти самі знають свій тип.

Успадкування без поліморфізму можливе, але не дуже корисне. Це видно на прикладі Ada, де можна повідомляти похідні типи, але через мономорфізм мови операції жорстко задаються на стадії компіляції.

Поліморфізм тісно пов'язаний з механізмом пізнього зв'язування. При поліморфізмі зв'язок методу й імені визначається тільки в процесі виконання програм. В C++ програміст має можливість вибирати між раннім і пізнім зв'язуванням ім'я з операцією. Якщо функція віртуальна, зв'язування буде пізнім і, отже, функція поліморфна. Якщо ні, то зв'язування відбувається при компіляції й нічого змінити потім не можна.

Спадкування й типізація. Розглянемо ще раз перевизначення функції transmit:

void ElectricalData::transmit()

{

TelemetryData::transmit();

// передати напруга

// передати силу струму

};

У більшості об’єктно-орієнтованих мовах програмування при реалізації методу підкласу дозволяється викликати прямо метод якого-небудь суперкласу. Як видно із прикладу, це допускається й у тому випадку, якщо метод підкласу має таке ж ім'я й фактично перевизначає метод суперкласу. В Smalltalk метод вищестоящого класу викликають за допомогою ключового слова super, при цьому можна вказувати на самого себе за допомогою ключового слова self. В C++ метод будь-якого досяжного вищестоящого класу можна викликати, додаючи ім'я класу як префікс, формуючи кваліфіковане ім'я методу (як TelemetryData::transmit() у нашому прикладі). Об'єкт може посилатися на себе за допомогою визначеного показника this.

На практиці метод суперкласу викликається до або після додаткових дій. Метод підкласу уточнює або доповнює поведінку суперкласу.

Всі підкласи на рис. 15.5 є також підтипами вищестоящого класу. Зокрема, ElectricalData є підтипом TelemetryData. Система типів, що будується паралельно успадкуванню, звичайна для об’єктно-орієнтованих мов із сильною типізацією, включаючи C++. Для Smalltalk, яка не є типізованою, типи не мають значення.

Розглянемо приклад на C++:

TelenetryData telemetry;

ElectrycalData electrical(5.0, -5.0, 3.0, 7.0);

Наступний оператор присвоювання вірний:

telemetry = electrical; //electrical - це підтип telemetry

Хоча він формально правильний, він небезпечний: будь-які доповнення в стані підкласу в порівнянні зі станом суперкласу просто пропадуть. Таким чином, додаткові чотири параметри, визначені в підкласі electrical, будуть загублені під час копіювання, оскільки їх просто нікуди записати в об'єкті telemetry клacу TelemetryData.

Наступний оператор неправильний:

electrical = telemetry; //неправильно: telemetry - це не підтип electrical

Можна зробити висновок, що присвоєння об'єкту y значення об'єкта x припустиме, якщо тип об'єкта x збігається з типом об'єкта y або є його підтипом.

У більшості строго типізованих мовах програмування допускається перетворення значень із одного типу в інший, але тільки в тих випадках, коли між двома типами існує відношення клас/підклас. У мові C++ є оператор явного перетворення, який називається приведенням типів. Як правило, такі перетворення використовуються стосовно об'єкту спеціалізованого класу, щоб привласнити його значення об'єкту загальнішого класу. Таке приведення типів вважається безпечним, оскільки під час компіляції здійснюється семантичний контроль. Іноді необхідні операції приведення об'єктів загальнішого класу до спеціалізованих класів. Ці операції не є надійними з погляду строгої типізації, тому що під час виконання програми може виникнути невідповідність (несумісність) об'єкта, що приводиться, з новим типом. Однак такі перетворення досить часто використовуються в тих випадках, коли програміст добре уявляє собі всі типи об'єктів. Наприклад, якщо немає параметризованих типів, часто створюються класи set або bag, що представляють собою набори довільних об'єктів. Їх визначають для деякого базового класу (це набагато безпечніше, ніж використовувати ідіому void*, як ми робили, визначаючи клас Queue). Ітераційні операції, визначені для такого класу, вміють повертати тільки об'єкти цього базового класу. Всередині конкретної програми розробник може використовувати цей клас, створюючи об'єкти тільки якогось спеціалізованого підкласу, і, знаючи, що саме він збирається поміщати в цей клас, може написати відповідний перетворювач. Але вся ця струнка конструкція звалиться під час виконання, якщо в наборі зустрінеться який-небудь об'єкт неочікованого типу.

Більшість сильно типізованих мов дозволяють програмам оптимізувати техніку виклику методів, найчастіше зводячи пересилання повідомлення до простого виклику процедури. Якщо, як в C++, ієрархія типів збігається з ієрархією класів, така оптимізація очевидна. Але в неї є недоліки. Зміна структури або поведінки якого-небудь суперкласу може поставити поза законом його підкласи. Якщо правила утворення типів засновані на успадкуванні й ми переробляємо який-небудь клас так, що міняється його положення в ієрархії успадкування, клієнти цього класу можуть виявитися поза законом з погляду типів, незважаючи на те, що зовнішній інтерфейс класу залишається колишнім.

Тим самим ми підходимо до фундаментальних питань успадкування. Як було сказано вище, успадкування використовується у зв'язку з тим, що в об'єктів є щось спільне або між ними є змістовна асоціація. Виражаючи ту ж думку іншими словами: успадкування можна розглядати, як спосіб керування повторним використанням програм, тобто, як просте рішення розробника про запозичення корисного коду. У цьому випадку механіка успадкування повинна бути гнучкою й легкою. Інша точка зору: успадкування відбиває принципову спорідненість абстракцій, яку неможливо скасувати. В Smalltalk і CLOS ці два аспекти нероздільні. C++ гнучкіший. Зокрема, при визначенні класу його суперклас можна оголосити public (як ElectricalData у нашому прикладі). У цьому випадку підклас вважається також і підтипом, тобто зобов'язується виконувати всі зобов'язання суперкласу, зокрема забезпечуючи сумісну із суперкласом підмножину інтерфейсу й володіючи нерозрізненим з погляду клієнтів суперкласу поведінкою. Але якщо при визначенні класу оголосити його суперклас як private, це буде означати, що, успадковуючи структуру й поведінку суперкласу, підклас вже не буде його підтипом. Це означає, що відкриті й захищені члени суперкласу стануть закритими членами підкласу, і отже вони будуть недоступні підкласам нижчого рівня. Крім того, той факт, що підклас не буде підтипом, означає, що клас і суперклас мають несумісні (загалом кажучи) інтерфейси з погляду клієнта.

Визначимо новий клас:

class InternalElectricalData: private ElectricalData {

public:

InternalElectricalData(float v1, float v2, float a1, float a2);

virtual ~InternalElectricalData();

ElectricalData::currentPower;

};

Тут суперклас ElectricalData оголошений закритим. Отже, його методи, такі, наприклад, як transmit, недоступні клієнтам. Оскільки клас InternalElectricalData не є підтипом ElectricalData, ми вже не зможемо привласнювати екземпляри підкласу об'єктам суперкласу, як у випадку оголошення суперкласу в якості відкритого. Відзначимо, що функція currentPower є видимою за рахунок її явної кваліфікації. Інакше вона залишилася б закритою. Як можна було очікувати, правила C++ забороняють робити успадкований елемент у підкласі "відкритішим", ніж у суперкласі. Так, член timeStamp, оголошений у класі TelemetryData захищеним, не може бути зроблений у підкласі відкритим шляхом явного згадування (як це було зроблено для функції currentpower).

У мові Ada для досягнення аналогічного ефекту замість підтипів використовується механізм похідних типів. Визначення підтипу означає не появу нового типу, а лише обмеження існуючого. А ось визначення похідного типу самостійно створює новий тип, що має структуру, запозичену у вихідного типу.

У наступному розділі ми покажемо, що успадкування з метою повторного використання й агрегації певною мірою суперечать один одному.

Множинне успадкування. Ми розглянули питання, пов'язані з одиночним успадкуванням, тобто, коли підклас має рівно один суперклас. Однак, одиночне успадкування при всій своїй корисності часто змушує програміста вибирати між двома однаково привабливими класами. Це обмежує можливість повторного використання визначених класів і змушує дублювати вже наявні коди. Наприклад, не можна успадкувати графічний елемент, що був би одночасно колом й картинкою; доводиться успадковувати щось одне й додавати необхідне від іншого.

Множинне успадкування прямо підтримується в мовах C++ і CLOS, а також, певною мірою, в Smalltalk. Необхідність множинного успадкування в OOP залишається предметом гарячих суперечок. Множинне спадкування - як парашут: зазвичай, воно не потрібне, але, коли раптом воно знадобиться, буде шкода, якщо його не виявиться під рукою.

Уявіть собі, що нам треба організувати облік різних видів матеріального й нематеріального майна - банківських рахунків, нерухомості, акцій і облігацій. Банківські рахунки бувають поточні й ощадні. Акції й облігації можна віднести до цінних паперів, керування ними зовсім відмінно від банківських рахунків, але й рахунки й цінні папери - це різновиди майна.

Однак є багато інших корисних класифікацій тих же видів майна. У якомусь контексті треба буде відрізняти те, що можна застрахувати (нерухомість і, певною мірою, ощадні вклади). Інший аспект - здатність майна приносити дивіденди; ця загальна властивість банківських рахунків і цінних паперів.

Очевидно, одиночне спадкування у цьому випадку не відбиває реальності, так що прийдеться вдатися до множинного. Отримана структура класів показана на рис. 15.7. На ньому клас Security (цінні папери) успадковується одночасно від класів InterestBearingItem (джерело дивідендів) і Asset (майно). Подібним чином, BankAccount (банківський рахунок) успадковується відразу від трьох класів: InsurableItem і вже відомих Asset і InterestBearingItem.

От як це виражається на C++. Спочатку базові класи:

class Asset ...

class InsurableItem ...

class InterestBearingItem ...

Тепер проміжні класи; кожний успадковується від декількох суперкласів:

class BankAccount: public Asset,

public InsurableItem,

public InterestBearingItem ...

class RealEstate: public Asset,

public InsurableItem ...

class Security: public Asset,

public InterestBearingItem ...

Нарешті, листки:

class SavingsAccount: public BankAccount ...

class CheckingAccount: public BankAccount ...

class Stock: public Security ...

class Bond: public Security ...

Рис. 15.7. Множинне спадкування.

Проектування структур класів із множинним успадкуванням - важке завдання, розв'язуване шляхом послідовних наближень. Є дві специфічні для множинного спадкування проблеми - як дозволити конфлікти імен між суперкласами й що робити з повторним успадкуванням.

Конфлікт імен відбувається, коли у двох або більше суперкласах випадково виявляється елемент (змінна або метод) з однаковим іменем. Наприклад Asset і InsurableItem містять атрибут presentValue, що позначає поточну вартість. Оскільки клас RealEstate успадковує обидва ці класи, то як розуміти успадкування двох операцій з тим самим іменем? Це, насправді, головна проблема множинного успадкування: конфлікт імен може ввести двозначність у поведінці класу з декількома предками.

Боряться із цим конфліктом трьома способами. По-перше, можна вважати конфлікт імен помилкою й відкидати його під час компіляції (так роблять Smalltalk і Eiffel, хоча в Eiffel конфлікт можна дозволити, виправивши ім'я). По-друге, можна вважати, що однакові імена означають однаковий атрибут (так робить CLOS). По-третє, для усунення конфлікту дозволяється додати до імен префікси, що вказують імена класів, звідки вони прийшли. Такий підхід прийнятий в C++.

Друга проблема повторного успадкування: тонке ускладнення при використанні множинного успадкування зустрічається, коли один клас є спадкоємцем іншого по декількох лініях. Якщо в мові дозволене множинне успадкування, рано або пізно хто-небудь напише клас D, що успадковується від B і C, які, у свою чергу, успадковуються від A. Ця ситуація називається повторним успадкуванням, і з нею потрібно коректно поводитися. Розглянемо такий клас:

class MutualFund: public Stock, public Bond ...

який двічі успадковується від класу security.

Проблема повторного успадкування вирішується трьома способами. По-перше, можна його заборонити, відслідковуючи під час компіляції. Так зроблено в мовах Smalltalk і Eiffel (але в Eiffel, знов-таки допускається перейменування для усунення невизначеності). По-друге, можна явно розділити дві копії успадкованого елемента, додаючи до імен префікси у вигляді імені класу-джерела (це один із підходів, прийнятих в C++). По-третє, можна розглядати множинні посилання на клас, як такі, що позначають той самий клас. Так зроблено в C++, де повторюваний суперклас визначається як віртуальний базовий клас. Віртуальний базовий клас з'являється, коли який-небудь підклас іменує інший клас своїм суперкласом і відзначає цей суперклас як віртуальний, щоб показати, що це - загальний (shared) клас. Аналогічно, у мові CLOS повторно наслідувані класи "усуспільнюються" із використанням механізму, який називається списком слідування класів. Цей список заводять для кожного нового класу, поміщаючи в нього сам цей клас і всі його суперкласи без повторень на основі таких правил:

  • клас завжди передує своєму суперкласу;

  • кожний клас сам визначає порядок слідування своїх безпосередніх батьків.

У результаті граф успадкування виявляється плоским, дублювання усувається, і з'являється можливість розглядати отриману ієрархію як ієрархію з одиночним успадкуванням. Це нагадує топологічне сортування класів. Якщо вона дозволена, то повторне успадкування допускається. При цьому теоретично можуть існувати кілька рівноправних результатів сортування, але алгоритм так чи інакше видає якийсь один із них. Якщо ж сортування неможливе (наприклад, у структурі виникають цикли), то клас відкидається.

При множинному спадкуванні часто використовується прийом створення домішок (mixin). Ідея домішок походить із мови Flavors: можна комбінувати (змішувати) невеликі класи, щоб будувати класи із складнішою поведінкою. Домішка синтаксично нічим не відрізняється від класу, але призначення їх різне. Домішка не призначена для породження самостійно використовуваних екземплярів - вона змішується з іншими класами. На рис. 15.7 класи InsurableItem і interestBearingItem - це домішки. Жодний з них не може існувати сам по собі, вони використовуються для додавання змісту іншим класам. Таким чином, домішка - це клас, що виражає не поведінку, а одну якусь сторону, яку можна причіпити іншим класам через успадкування. Зазвичай ця сторона ортогональна власній поведінці класу, що її успадковує. Класи, сконструйовані цілком з домішок, називаються агрегатними.

Множинний поліморфізм. Повернемося до однієї з функцій-членів класу DisplayItem:

virtual void draw();

Ця операція зображає об'єкт на екрані в деякому контексті. Вона оголошена віртуальною, тобто поліморфною, переобумовленими підкласами. Коли цю операцію викликають для якогось об'єкта, програма визначає, що, саме їй виконувати. Це одиночний поліморфізм у тому розумінні, що зміст повідомлення залежить тільки від одного параметра, а саме, об'єкта, для якого викликається операція.

Насправді операція draw повинна б залежати від характеристик використовуваної системи відображення, зокрема, від графічного режиму. Наприклад, в одному випадку ми хочемо одержати зображення з високою якістю, а в іншому - швидко одержати чернове зображення. Можна ввести дві різні операції, скажемо, drawGraphic і drawText, але це не зовсім те, що хотілося б. Справа в тому, що щораз, коли потрібно врахувати новий вид пристрою, його треба провести за всією ієрархією надкласів для класу DisplayItem.

В CLOS є так звані мультиметоди. Вони поліморфні, тобто їхній зміст залежить від множини параметрів (наприклад, від графічного режиму й від об'єкта). В C++ мультиметодів немає, тому там використовується ідіома так званої подвійної диспетчеризації.

Наприклад, ми могли б ввести ієрархію пристроїв для відображення інформації від базового класу DisplayDevice, а потім визначити метод класу DisplayItem так:

virtual void draw(DisplayDevice&);

При реалізації цього методу ми викликаємо графічні операції, які поліморфні щодо переданого параметру типу DisplayItem, у такий спосіб відбувається подвійна диспетчеризація: draw спочатку демонструє поліморфну поведінку залежно від того, до якого підкласу класу DisplayItem належить об'єкт, а потім поліморфізм проявляється залежно від того, до якого підкласу класу DisplayDevice належить аргумент. Цю ідіому можна продовжити до множинної диспетчеризації.