Інтерфейси як вирішення проблем множинного
успадкування h2>
Євген Каратаєв p>
В
цій роботі розбирається проблема множинного спадкоємства в мові
програмування С + + і можливе її вирішення шляхом застосування абстракцій
інтерфейсів. p>
Множинні
спадкуванням є утворення класу шляхом успадкування одночасно
декількох базових класів. Штука корисна і одночасно з цим проблемна.
Розглянемо приклад, в якому з'являється множинне успадкування, що приводить до
проблеми. p>
Класичним
завданням для починаючого програміста є завдання написати класи,
реалізують ієрархію Людина - Студент - Співробітник. Зазвичай першим же рішенням
є утворення трьох класів у вигляді: p>
class
Людина (... ); p>
class
Співробітник: public Людина (... ); p>
class
Студент: public Людина (... ); p>
В
класі Людина декларуються кілька віртуальних і, можливо, абстрактних,
функцій, які перевизначаються/реалізуються в класах-спадкоємців. Схема на
перший погляд, цілком очевидна і практично ні в кого не викликає
підозр. Схема реалізується в програмі і програма здається в роботу. p>
Проблема
виникає пізніше, коли оператор приходить і говорить: p>
--
У мене є людина, яка одночасно і співробітник і студент. Що мені
робити? p>
Реалізована
схема, взагалі кажучи, не передбачає такого варіанту - можуть бути або
співробітник, або студент. Але щось робити треба. У цей момент приходить на
допомога множинне успадкування. Програміст, не довго думаючи, створює ще
один клас, утворений спадкуванням і від Співробітник і від Студент: p>
class
СтудентСотруднік: public Студент, public Співробітник (...}; p>
На
перший погляд все гаразд, на другому - повний бардак. Справа в тому, що
клас Співробітник, як він був декларував, містить у собі повну копію класу
Людина. Те ж саме відноситься і до класу Студент. Таким чином, клас
СтудентСотруднік буде містити в собі вже 2 копії класу Людина. При цьому
функції класу Співробітник будуть працювати зі своїм екземпляром класу Людина, а
функції класу Студент - зі своїм. У результаті коректної поведінки домогтися
практично дуже важко. У класі СтудентСотруднік доведеться перевизначати все
функції базових класів і викликати відповідні функції базових класів,
щоб модифікації обох копій класу Людина пройшли когерентно. p>
Виявивши
таку ситуацію шляхом важкої налагодження, програміст приходить до необхідності
застосування віртуального наслідування для виключення дублювання класу
Людина. Проблема полягає в тому, що віртуальне наслідування вимагає
модифікації графа успадкування базових класів. Необхідна схема має вигляд: p>
class
Людина (... ); p>
class
Студент: virtual public Людина (... ); p>
class
Співробітник: virtual public Людина (... ); p>
class
СтудентСотруднік: public Студент, public Співробітник (... p>
); p>
В
цьому варіанті вирішена проблема однозначної входимо класу Людина в усі
класи. Але залишається питання - чи не виникне такої ж проблеми і далі з
отриманим класом СтудентСотруднік? І чи буде можливість виробити
модифікацію вже працюючого коду? У такій ситуації руки можуть опуститися --
слід або погодитися з існуванням проблемного коду або дійсно
йти на повну переробку програми. p>
Тим
не менш елегантне рішення існує. Це реалізація базових класів з
принципом інтерфейсів. Мова С + + не містить мовної підтримки інтерфейсів в
явному вигляді, тому будемо їх емулювати. Принцип інтерфейсу полягає в тому, що
його завданням є не стільки реалізація класу, скільки його декларація.
Нормалізуючи вихідну завдання: p>
class
БитьЧеловеком (... ); p>
class
БитьСтудентом (... ); p>
class
БитьСотрудніком (... ); p>
Виходячи
з нормалізованого безлічі класів, отримаємо доповнення: p>
class
Людина: public БитьЧеловеком (... ); p>
class
); p>
class
Студент: public БитьЧеловеком, public БитьСтудентом (...}; p>
class
СтудентСотруднік: public БитьЧеловеком, public БитьСтудентом, p>
public БитьСотрудніком (... ); p>
Формально
кажучи, така схема побудови класів цілком працездатна за винятком
того, що в багатьох випадках програмісти відносяться до інтерфейсів надто вже
буквально - залишають у них тільки абстрактні функції і реалізують ці функції
тільки в класах-спадкоємців. В результаті повністю вихолощується ідея
повторного використання коду. Підставою для нереалізації функцій у
інтерфейсних класах зазвичай служить те, що в класі - інтерфейсі немає
"ядра" об'єкта. У нашому випадку ядром об'єкта або класом, які реалізують
можливість існування об'єкта, може виступати клас БитьЧеловеком. p>
Можливим
вирішенням проблеми є передача конструктору інтерфейсного класу покажчика
на конструюються об'єкт з тим, щоб його запам'ятати в своєму приватному поле даних
і використовувати при реалізації функцій інтерфейсу. Приблизно за схемою: p>
class
БитьСтудентом p>
( p>
БитьЧеловеком & m_БитьЧеловеком; p>
public: p>
БитьСтудентом (БитьЧеловеком & init) p>
: m_БитьЧеловеком (init) p>
(... ); p>
); p>
class
Студент: public БитьЧеловеком, public БитьСтудентом p>
( p>
public: p>
Студент () p>
: БитьЧеловеком (), БитьСтудентом (* this) p>
(...}; p>
); p>
В
цією схемою, відповідно до стандарту, також є проблема - стандарт не гарантує
ініціалізації конструкторів, зазначених у списку ініціалізації, в тому порядку, в
якому вони перераховані в цьому списку. Тому ми, передаючи * this як аргумент
конструктора базового класу, отримуємо посилання на Негарантований певний
об'єкт. Вийти з цієї ситуації можна, якщо декларувати конструктор без
аргументів і створити додаткову функцію ініціалізації, що залежить від * this.
Але дублювання посилань, що зберігаються в інтерфейсних класах, тим не менше,
зберігається і це є негарно. p>
Для
вирішення цього завдання є надзвичайно гарне, на мій погляд, рішення. Рішення
полягає в тому, щоб не зберігати посилання на ядро об'єкта, а отримувати її
динамічно. Для цього застосовується оператор приведення типу dynamic_cast,
застосовується не до класу, а до об'єкта в процесі роботи програми. Приклад: p>
class
БитьСтудентом p>
( p>
public: p>
БитьСтудентом (){}; p>
virtual void Func (void); p>
// приклад функції, що звертається до ядра
об'єкта p>
( p>
БитьЧеловеком * ptr = dynamic_cast <
БитьЧеловеком *> (this); p>
if (ptr) p>
( p>
// використовуємо ядро p>
) p>
); p>
); p>
На
перший погляд, приведення типу БитьСтудентом до типу БитьЧеловеком неможливо,
оскільки ніхто з цих класів ні від кого не наслідувати. Але справа в тому, що
оператор dynamic_cast визначений не для класів, а для об'єктів. І якщо при
виконанні коду Func реальний об'єкт, для якого ця функція виконується,
имееет клас, успадкування від БитьЧеловеком, то оператор поверне правильне
значення. Відповідно до стандарту, оператор приведення типу dynamic_cast має два
виду поводження якщо приведення неможливо - повернути нульове значення або
порушити виняткову ситуацію. Обидва варіанти нас повністю влаштовують. p>
Я
вважаю, що в моделі застосування інтерфейсних класів для вирішення проблем
множинного спадкування буде також гарно побудувати інтерфейсні класи з
конструкторами, що не вимагають звернення до ядра об'єкта. Втім, це вже з
галузі філософії перешкодостійкого програмування. p>
Список літератури h2>
Для
підготовки даної роботи були використані матеріали з сайту http://karataev.nm.ru/
p>