Інтерфейси як вирішення проблем множинного спадкування

[ виправити ] текст може містити помилки, будь ласка перевіряйте перш ніж використовувати.

скачати

Євген Каратаєв

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

Множинним спадкуванням є утворення класу шляхом успадкування одночасно декількох базових класів. Штука корисна і одночасно з цим проблемна. Розберемо приклад, в якому з'являється множинне успадкування, що приводить до проблеми.

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

class Людина {... };

class Співробітник: public Людина {... };

class Студент: public Людина {... };

У класі Людина декларуються кілька віртуальних та, можливо, абстрактних, функцій, які перевизначаються / реалізуються в класах-спадкоємців. Схема на перший погляд абсолютно очевидна і практично ні в кого не викликає підозр. Схема реалізується в програмі і програма здається в роботу.

Проблема виникає пізніше, коли оператор приходить і каже:

- У мене є людина, який одночасно і співробітник і студент. Що мені робити?

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

class СтудентСотруднік: public Студент, public Співробітник {...};

На перший погляд все гаразд, на другий - повний бардак. Справа в тому, що клас Співробітник, як він був декларовано, містить в собі повну копію класу Людина. Те ж саме відноситься і до класу Студент. Таким чином, клас СтудентСотруднік буде містити в собі вже 2 копії класу Людина. При цьому функції класу Співробітник будуть працювати зі своїм екземпляром класу Людина, а функції класу Студент - зі своїм. У результаті коректної поведінки домогтися практично дуже важко. У класі СтудентСотруднік доведеться перевизначати всі функції базових класів і викликати відповідні функції базових класів, щоб модифікації обох копій класу Людина пройшли когерентно.

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

class Людина {... };

class Студент: virtual public Людина {... };

class Співробітник: virtual public Людина {... };

class СтудентСотруднік: public Студент, public Співробітник {...

};

У цьому варіанті вирішена проблема однозначної входимость класу Людина в усі класи. Але залишається питання - чи не виникне такий ж проблеми і далі з отриманим класом СтудентСотруднік? І чи буде можливість провести модифікацію вже працюючого коду? У такій ситуації руки можуть опуститися - слід або погодитися з існуванням проблемного коду або дійсно йти на повну переробку програми.

Тим не менш елегантне рішення існує. Це реалізація базових класів за принципом інтерфейсів. Мова С + + не містить мовної підтримки інтерфейсів в явному вигляді, тому будемо їх емулювати. Принцип інтерфейсу полягає в тому, що його завданням є не стільки реалізація класу, скільки його декларація. Нормалізуємо вихідну завдання:

class БитьЧеловеком {... };

class БитьСтудентом {... };

class БитьСотрудніком {... };

Виходячи з нормалізованого безлічі класів, одержимо додаток:

class Людина: public БитьЧеловеком {... };

class Співробітник: public БитьЧеловеком, public БитьСотрудніком {... };

class Студент: public БитьЧеловеком, public БитьСтудентом {...};

class СтудентСотруднік: public БитьЧеловеком, public БитьСтудентом,

public БитьСотрудніком {... };

Формально кажучи, така схема побудови класів цілком працездатна за винятком того, що в багатьох випадках програмісти відносяться до інтерфейсів надто вже буквально - залишають у них тільки абстрактні функції і реалізують ці функції тільки в класах-спадкоємців. У результаті повністю вихолощується ідея повторного використання коду. Підставою для нереалізації функцій в інтерфейсних класах зазвичай служить те, що в класі - інтерфейсі немає "ядра" об'єкта. У нашому випадку ядром об'єкта або класом, що реалізує можливість існування об'єкта, може виступати клас БитьЧеловеком.

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

class БитьСтудентом

{

БитьЧеловеком & m_БитьЧеловеком;

public:

БитьСтудентом (БитьЧеловеком & init)

: M_БитьЧеловеком (init)

{... };

};

class Студент: public БитьЧеловеком, public БитьСтудентом

{

public:

Студент ()

: БитьЧеловеком (), БитьСтудентом (* this)

{...};

};

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

Для вирішення цього завдання є надзвичайно красиве, на мій погляд, рішення. Рішення полягає в тому, щоб не зберігати посилання на ядро ​​об'єкта, а отримувати її динамічно. Для цього застосовується оператор приведення типу dynamic_cast, застосовується не до класу, а до об'єкта в процесі роботи програми. Приклад:

class БитьСтудентом

{

public:

БитьСтудентом (){};

virtual void Func (void);

/ / Приклад функції, що звертається до ядра об'єкта

{

БитьЧеловеком * ptr = dynamic_cast <БитьЧеловеком *> (this);

if (ptr)

{

/ / Використовуємо ядро

}

};

};

На перший погляд, приведення типу БитьСтудентом до типу БитьЧеловеком неможливо, бо ніхто їх цих класів ні від кого не успадкований. Але справа в тому, що оператор dynamic_cast визначений не для класів, а для об'єктів. І якщо при виконанні коду Func реальний об'єкт, для якого ця функція виконується, имееет клас, успадкування від БитьЧеловеком, то оператор поверне правильне значення. Відповідно до стандарту, оператор приведення типу dynamic_cast має два види поведінки якщо приведення неможливо - повернути нульове значення або порушити виняткову ситуацію. Обидва варіанти нас повністю влаштовують.

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

Додати в блог або на сайт

Цей текст може містити помилки.

Програмування, комп'ютери, інформатика і кібернетика | Доповідь
13.8кб. | скачати


Схожі роботи:
Вирішення проблем друку в OS 2
Шляхи вирішення проблем в електроенергетиці
Клонування вирішення проблем чи нова проблема
Технології вирішення проблем підвищення якості
Основні шляхи вирішення проблем в області інформатизації
Достоєвський ф. м. - Релігія як спосіб вирішення моральних проблем
Особливості підходів до вирішення проблем літніх людей
Шляхи вирішення проблем сучасної історичної освіти
Вирішення проблем безпеки праці користувачів ПК в різних країнах св
© Усі права захищені
написати до нас