Функції твої неминучі, ім'я твоє невідомо
Отже, рішення про те, що ще не вистачає нашому об'єкту сервера для того, щоб з ним міг нормально звертатися клієнт - знайдено. Залишилися деякі деталі оформлення цього рішення в точну програмну конструкцію. У попередній статті ми встановили, що нам потрібно апарат приведення типів покажчиків на інтерфейси і апарат підрахунку посилань на об'єкт, з вбудованим "самоліквідатором". Вони можуть бути доступні клієнту тільки за допомогою методів об'єкта, які викликає клієнт і ці самі методи повинні бути або в складі кожного інтерфейсу, реалізованого об'єктом, або - оформлені окремим спеціальним інтерфейсом. І абсолютно кожен об'єкт, якого б роду і племені він не був - зобов'язаний реалізовувати цю функціональність.
Оскільки експонувати ці функції можна тільки як інтерфейс, то і предмет обговорення: що краще - оформити їх у вигляді окремого інтерфейсу, обов'язок експонувати який ставити будь-якого об'єкта (в числі всіх інших інтерфейсів об'єкту) або ж - додавати цю функціональність до кожного інтерфейсу, експоновані об'єктом ?
Визначимося, про що ми говоримо. Перший метод буде називатися QueryInterface - він повинен приймати IID іншого інтерфейсу, що експонується даними ж об'єктом, і повертати покажчик на цей інтерфейс. Другий метод буде називатися AddRef - він не має параметрів, а кожен його виклик призводить до просування лічильника посилань об'єкта вперед на одиницю. Третій метод - Release. Його завдання - зворотна AddRef. а коли лічильник посилань досягне нуля Release ж викличе і delete this.
Чому замість одного методу з управління лічильником посилань ми придумали два? Хоча б тому, що код виклику методу без параметрів - коротше. Нехай на кілька байтів, але ці кілька байтів будуть в клієнті зустрічатися скрізь, де у нас розмножується покажчик. І сумарна добавка до коду може бути великий.
Отже, ці три методи:
ми можемо оформити в окремий інтерфейс. Або - ми можемо прописувати до складу кожного іншого інтерфейсу. Що краще? І чому?
Припустимо, ця функціональність виведена в абсолютно окремий інтерфейс X. а всі інші інтерфейси цього ж об'єкта її не мають. Що станеться? А станеться ось що - якщо при створенні об'єкта ми попросимо у сервера повернути нам покажчик на інтерфейс X. то, володіючи цим покажчиком, ми легко отримаємо покажчики і на всі інші інтерфейси об'єкта - QueryInterface же знаходиться в складі інтерфейсу X. Але от якщо ми у сервера попросимо повернути будь-який інший інтерфейс цього ж об'єкта, то так з цим інтерфейсом і залишимося - в цьому-то інтерфейсі немає QueryInterface. Це змушує щоразу виконувати не потрібний нам інтерфейс, а саме X. і потім з нього вже виробляти потрібний нам покажчик. У наявності подвійна робота на стороні клієнта - при отриманні покажчика на інтерфейс.
Якщо ж цю функціональність поміщати всередину всякого інтерфейсу, який експонує об'єкт, то у кожного інтерфейсу будуть займатися три додаткових осередки Vtbl. але зате нам не потрібно буде виконувати ніякої подвійної роботи на стороні клієнта, все управління об'єктом можна здійснити за допомогою його будь-якого інтерфейсу. І якось здається, що другий спосіб значно зручніше. з боку клієнта, зрозуміло - повторно-то використовується саме сервер.
Але ж так можна побудувати і такий об'єкт, у якого не буде жодного "корисного" інтерфейсу? Можна, можливо. А ось ця службова функціональність в будь-якому випадку повинна бути. І можна буде розмножувати покажчик, а потім - знищити об'єкт. А все що понад те - визначається виключно істотою розв'язуваної програмістом завдання.
Описана функціональність настільки фундаментальна, що без неї "взагалі нічого не працює" - ми ж і задумалися над нею тому, що в нашій реалізації компонентного взаємодії не вистачало дуже істотних фрагментів. Чи можна її реалізувати інакше? В деталях - так, по суті - ні. Адже причина наявності цієї функціональності в складі об'єкта - філософська. Якби у нас компілятор знав точний статичний тип об'єкту і тип цей був один і для клієнта і для сервера, то звичайно, компілятор міг би реалізувати і правильний виклик new і правильний виклик delete *. і сам компілятор міг би перетворювати покажчики на тип. Але, фактично, це означає, що і клієнт і сервер повинні розташовуватися в контексті одного і того ж проекту - а ми якраз маємо абсолютно зворотні "початкові умови". У нас і клієнт і сервер обов'язково повинні розташовуватися в різних проектах, в різних контекстах. У нас же - програмування з довічних компонент.
Саме через цю обставину нам спочатку знадобилося частина таблиць часу компіляції вбудувати в сам об'єкт, так щоб вони зберігалися і під час виконання (vtbl) (див. Статтю Введення в теорію компіляції. І виведення з неї.), А тепер нам потрібно навантажити об'єкт і такими функціями, як управління часом життя і приведення типу. І уникнути цього ми не можемо - або ми самі, або компілятор.
Забавно, що цього не розуміють критики відомого типу "Windows - мастдай, а COM - Суксь", коли вони посилаються на те, що, де, "COM - зайвий код", якого в "нормальному правильному проекті" немає. Є, є такий код в проекті, тільки тут ми його включаємо явно і "своїми руками", а там це робить компілятор, яким зовсім не обов'язково переносити вміст своїх таблиць в код часу виконання - частина такого коду може бути просто вставлена "за місцем виклику "і зовсім прозоро для програміста. COM дійсно включає в себе багато додаткового коду, але ж і проблеми, які вирішує COM хто не наважується жодним компілятором.
Потрібно особливо відмітити - що хоча ми і вивчаємо COM. то, що зараз сформульовано має філософську природу. А, значить, в тому чи іншому вигляді може бути знайдено і в реалізації CORBA і повинно бути взагалі в будь-який двійковій-компонентної технології. "Обгортка" цього може бути різною, а ось сутність - однакова.
В COM ця дійсно фундаментальна сутність називається "інтерфейс IUnknown", що у вільному перекладі може звучати як "невідомий інтерфейс" і викликає щонайменше деяке здивування - який же це невідомий інтерфейс, якщо його наявність гарантовано в будь-якому об'єкті? Однак, якщо це переводити, як "інтерфейс Невідомо Хто", - все встає на свої місця. Більш того, двійковий компонентний об'єкт буде об'єктом COM в тому і тільки в тому випадку, якщо він реалізує, щонайменше, інтерфейс IUnknown. Якщо такого інтерфейсу немає - це не об'єкт COM. хоча, як ми бачили в прикладі №1 об'єкт може бути "двійковій-компонентним" і без використання COM.
З цим інтерфейсом нам волею-неволею доведеться познайомитися дуже близько - він є "альфа і омега" всіх інтерфейсів, саме він визначає спеціальне поведінку об'єкта. А зараз поки що відзначимо - будь-який інтерфейс COM повинен бути успадкований від інтерфейсу IUnknown. Повинно бути зрозуміло чому - саме IUnknown в складі будь-якого інтерфейсу забезпечує управління часом життя об'єкта та приведення типу покажчика. І для цього не потрібно ніяких додаткових витрат клієнта.