Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

Понятие интерфейса

Итак, что такое интерфейс? Если вдуматься в звучание этого слова, то это что-то inter face, т.е. находящееся между взаимодействующими субъектами. И это действительно так. Интерфейс есть спецификация способа взаимодействия двух сущностей понятным для них обеих образом. (И всегда - двух и только двух! Пару могут составлять всё время разные объекты в разном сочетании, но спецификация распространяется только на inter face...)

Концепция интерфейса(а это именно концепция) является абстракцией не меньшего, а может даже - и большего, уровня, чем абстракция объекта. Она требуется, когда мы от взаимодействия объектов переходим к конструкции какого-то одного из них. В таком случае интерфейс выступает, как существенные исключительно для взаимодействия стороны второго, не рассматриваемого в данный момент, объекта. Например, во взаимодействии телефонного аппарата и телефонной станции интерфейсом, очевидно, является вид и последовательность сигналов, которыми обмениваются аппарат и станция. Станцию не интересует цвет, вес, форма и прочие свойства аппарата. Станция считает телефонным аппаратом любой объект, который в состоянии "по проводам" передавать сигналы установленной спецификации. Аналогичная картина наблюдается и со стороны телефонного аппарата - для него станция тоже приводится только к сигналам, которые он получает из линии, а где она расположена, какое у нее здание и что у нее есть еще - аппарат не знает, да ему это и не нужно.

Другой пример интерфейса показывает обычная пара - штепсельная вилка и штепсельная розетка. Вместе они составляют "разъёмное соединение", но на стороне каждого из взаимодействующих объектов имеется своя спецификация - штыри или гнезда определенного размера и на определенном расстоянии. Розетка вполне может считать вилкой любой предмет, который имеет штыри определенного диаметра, расположенные на определённом расстоянии, а вилка может считать розеткой любые гнезда, способные вместить её штыри.

Из этих примеров должно быть понятно, что интерфейс является вполне точной категорией, но не имеет никакого особенного вещного выражения. Весь интерфейс выражается только в спецификации, как должны взаимодействовать объекты и что они должны иметь и делать для этого взаимодействия.

Концепция интерфейса замечательна тем, что она позволяет разделить способность объекта к взаимодействию и другие свойства данного объекта. Если вернуться к примеру телефонного аппарата, то его весьма специфическое восприятие телефонной станцией приводит к тому, что и факсимильная машина и модем и вообще всё, что угодно, способное произвести в линию заданную последовательность сигналов требуемого уровня и вида будут способны воспользоваться услугами телефонной станции. А это даёт определенную свободу в конструировании этих устройств без переделки конструкции телефонной станции. Либо, напротив - свободу в переделке конструкции телефонной станции без потери ею способности обслуживать телефонные соединения.

Интерфейсами пропитана вся наша окружающая жизнь. Мы используем это понятие как совершенно естественное, часто не отдавая себе сознательного отчета в том, что оно называется "интерфейс". Скажем, многие ли вспомнят, что обычный телефонный аппарат имеет и второй интерфейс? Интерфейс между человеком и аппаратом! Этот интерфейс выражается в приемопередающей звукопреобразующей аппаратуре (микрофон и телефон), устройстве, генерирующем вызывные сигналы (номеронабиратель), а ещё - в последовательности действий, которые нужно произвести, чтобы установить соединение с удалённым корреспондентом. Если не будет хотя бы чего-то одного, если последовательность действий будет нарушена - соединение не состоится. А это - это и есть самый настоящий интерфейс, поскольку "что у аппарата внутри" в данном случае никак не влияет на возможность установления связи.

С другой стороны, интерфейс - действительно философское понятие. То что, является самостоятельным объектом на одном уровне рассмотрения, на другом уровне вполне может оказаться только лишь другим интерфейсом, а то и его частью. Например, когда вы звоните в билетную кассу, чтобы заказать билет в театр, то "весь телефон" для вас окажется просто частью бОльшего интерфейса между вами и билетным кассиром с той стороны. И у вас будет "способ взаимодействия", в котором "вхождение в телефонную связь" будет рассматриваться только как часть общего алгоритма взаимодействия. И не состояться это взаимодействие может по совсем другим причинам, например, кассир с той стороны говорит на языке, которого вы не понимаете, хотя - в связь при помощи телефона вы оба входите совершенно правильно.

Поскольку интерфейс - категория философская, то и в программировании можно найти его примеры на каждом шагу. Хотя они и не называются так, тем не менее, ничем, кроме интерфейса они и не являются. Самый распространённый пример называется "прототип функции". Если припомнить как описывается разбиение программы на "вызывающую" и "вызываемую" функции, то там фигурируют такие понятия, как "вызов функции", "определение функции" и "описание прототипа функции". При этом дело обстоит так, что для компиляции вызова функции не требуется её определения, достаточно "описания прототипа". Прототип так же полезно показать компилятору и при определении функции - тогда компилятор будет способен заметить расхождения между "прототипом" и фактическим определением. Что же есть в данном случае одна строчка "прототип функции"? Да интерфейс же! Ведь он ничего не делает, кроме как описывает существенные для взаимодействия с этой функцией свойства - имя функции, тип возвращаемого значения, порядок следования и типы аргументов.

Следует заметить, что при описании взаимодействия в computer sciencesразделяют два понятия - "интерфейс" и "протокол". Первое из них обозначает статику этого взаимодействия, т.е. что именно, в каком формате и на каком месте должен предоставлять объект. Второе обычно обозначает динамику взаимодействия - чем именно и в какой последовательности должны обмениваться взаимодействующие объекты. Это разделение существует, оно традиционно, хотя… хотя и интерфейс и протокол относятся к одному и тому же явлению. И когда их иногда смешивают в одну сущность (которую называют то "интерфейс", то "протокол") в этом нет особенной ошибки. В самом деле, чем "спецификация в пространстве" (интерфейс) отличается в своей сущности от "спецификации во времени" (протокол)?

Итак, первым китом компонентного программирования является понятие объекта. Понятие явно определенного интерфейса - второй его кит.

Что такое интерфейс в его программном исполнении? Ответ на него может быть и длинным и коротким - мы видели, что даже объявление прототипа функции уже можно считать интерфейсом. И программирование пользуется этим очень давно.

В действительности же интерфейс стали называть отдельным специальным словом тогда, когда стало очевидно - сложность программных конструкций стала такой, что программист уже не может удержать во внимании оба взаимодействующих объекта. Пока это удавалось "объявление функции" выполняло страхующую роль - а вдруг где забыто чего? Но программист не чувствовал ни малейших неудобств в произвольном изменении списка аргументов функции - если по ходу развития программы требовалось этот список изменить, то он менял его в определении функции и в её объявлении. После чего запускал компилятор и тот показывал ему список мест, где нужно поправить.

Проблемы начинались тогда, когда, образно говоря, "задачи стали занимать более десяти тысяч строк". Эти задачи уже не помещались в один "программный проект". Классическим же примером этого опять является "клиент-сервер" - его наиболее удобная реализация как раз и выглядит в виде двух самостоятельных программных проектов - проекта клиента и проекта сервера. А границу между проектами компилятор преодолевать не умеет…

Это, конечно, случилось совсем не вдруг - разработчики языков программирования видели эту проблему и пытались предложить для нее адекватное своему времени решение. Например, в языке C++есть понятиечисто абстрактного класса, которое совершенно явно вводит понятие интерфейса между объектами. Оно в самом буквальном смысле явилось предтечей интерфейсаCOMи на нём стОит остановиться подробнее. Давайте кратко рассмотрим откуда возникла эта проблема и в чём она состоит.

Рассмотрим класс (статический тип), описывающий объект сервера:

class Foo

{

int a;

float b;

};

и рассмотрим код клиента:

Foo Cls;

Cls.a = 12;

Cls.b = 13.2;

Это, как теперь канонически признаётся - нехороший код. Нехорошо иметь прямой доступ к данным класса помимо самого класса. Что вот будет, если нам придётся переименовать a- уж больно это невнятный идентификатор? "По науке" нам нужно делать только так:

classFoo

{

public:

void SetA(int i)

{

a = i;

}

void SetB(float f)

{

b = f;

}

void SetAB(int i, float f)

{

a = i;

b = f;

}

private:

inta;

floatb;

};

Теперь исполнить предложение в теле клиента:

Cls.a = 12;

не даст компилятор - данные-то у нас - private. Клиент придётся переписать:

Foo Cls;

Cls.SetA(12);

Cls.SetB(13.2);

В таком случае клиент наш может ничего и не знать о том, что такие переменные, как aиbв классе есть. Но для того, чтобы клиент мог быть откомпилирован, чтобы компилятор сумел правильно организовать вызов методовSetA иSetB , при его компиляции нам всё равно придется подавать на вход компилятору определение класса. Допустим, что мы поместили его в заголовочный файлmyobj.h, который мы будем подавать компилятору и при компиляции кода сервера и, естественно, при компиляции кода клиента. Теперь у нас есть три файла исходного текста:

Первый:

//myobj.h - определение объекта Foo

class Foo

{

public:

void SetA(int i);

void SetB(float f);

void SetAB(int i, float f);

private:

int a;

float b;

};

Второй:

//myobj.cpp - реализация методов объекта сервера

#include "myobj.h"

void Foo::SetA(int i)

{

a = i;

}

void Foo::SetB(float f)

{

b = f;

}

void Foo::SetAB(int i, float f)

{

a = i;

b = f;

}

Третий:

//myclient.cpp - реализация кода клиента

#include "myobj.h"

Foo Cls;

Cls.SetA(12);

Cls.SetB(13.2);

Видите ли вы здесь неудобство, которое при этом возникает? А неудобство-то вот какое - при компиляции сервера нам действительно нужно знать всё про класс - и про его данныеи проего методы. А вот при компиляции клиента нас интересуюттолько методы- данные-то сервера нам недоступны. И если в процессе развития сервера мы, скажем, добавили в класс еще одну, сугубо внутреннюю, переменную или внутренний метод, то понятно, что нам нужно будет перекомпилировать сервер. Но ведь нам также придется и перекомпилировать клиент - файлmyobj.hведь изменился! А вот сам клиент - не изменялся. А перекомпилировать клиент - придётся... Здесь это неудобство маленькое, но вот если и клиент и сервер - большие проекты, то эти сугубо локальные изменения могут вылиться и всегда выливаются в очень большие глобальные затраты. Ведь при измененииодногосервера приходится перекомпилироватьвсехего клиентов. Которые, при каком-то ощутимом их размере, уже не помещаются в единый проект и должны быть распределены по нескольким. Как быть? По сути, нам всего-то надо - разделить описания методов и данных, причем так, чтобы описание методов влияло и на клиент и на сервер, но полное описание класса - не влияло на клиент. Ведь из клиента мы видим только методы!

Это можно сделать воспользовавшись аппаратом абстрактных классов С++. Этот аппарат специально для этой цели и был сконструирован. Абстрактный класс, это класс который вводит только точные описания методов, причём их реализация откладывается до тех пор, пока от абстрактного класса кто-то не унаследуется. Вот тогда-то абстрактный класс заставит наследника в точности эти методы и реализовать! Попробуем усовершенствовать наше творение:

//myobjint.h - описание методов Foo

classFooInterface

{

public:

void SetA(int i) = 0;

void SetB(float f) = 0;

void SetAB(int i, float f) = 0;

};

Далее, переписываем наши файлы:

Первый:

//myobj.h - определение объекта Foo

#include "myobjint.h"

//включили описание от которого наследуемся

classFoo :publicFooInterface

{

public:

void SetA(int i);

void SetB(float f);

void SetAB(int i, float f);

private:

int a;

float b;

};

Второй:

//myobj.cpp - реализация методов объекта сервера

#include"myobj.h"

//включили описание всего объекта

void Foo::SetA(int i)

{

a = i;

}

void Foo::SetB(float f)

{

b = f;

}

void Foo::SetAB(int i, float f)

{

a = i;

b = f;

}

Третий:

//myclient.cpp - реализация кода клиента

#include "myobjint.h"

//включили только описание как вызывать объект

Foo Cls;

Cls.SetA(12);

Cls.SetB(13.2);

Мы запускаем третий наш файл myclient.cppна компиляцию и... файл не компилируется! Компилятор сообщает, что классFooкомпилятору неизвестен. Верно. Нет у нас такого класса. У нас вместо него теперь -FooInterface, заменяем, компилируем. Стало ещё хуже - компилятор заявляет, что он вообще не может создать объект такого класса, т.к. класс... абстрактный... Это - очень интересное заявление! В чём же дело?

А дело вот в чём. FooInterface- действительно абстрактный класс. У него нет ничего, кроме объявлениякаквызывать методы. Методы чьи? Да наследника же, своих-то нет! Поэтому мы и не можем создать объект абстрактного типа - нет в нем ничего,чтовызывать - неизвестно. Зато - совершенно точно описано -каквызывать. И, если мы получим каким-то образом указатель на наследника, то пользуясь спецификацией "как вызывать" предоставляемой абстрактным классом, мы все сделаем отлично:

//myclient.cpp - реализация кода клиента

#include"myobjint.h"

//включили только описание как вызывать объект

FooInterface *pCls;

//здесь нужно как-то получить значение для указателя pCls

pCls->SetA(12);

pCls->SetB(13.2);

И теперь, как бы ни развивался объект сервера, если спецификация абстрактного класса не изменялась нам ничего не нужно в клиенте изменять - ни в тексте, ни в его коде. Всё будет работать! Здорово? Осталось только-то получить указатель. Например, можно использовать оператор new:

pCls = newFoo();

Но... статический тип Fooнам на стороне клиента неизвестен. И пока мы не включим в состав своего клиента описаниеmyobj.hнам не исполнить этогоnew. А ведь именно включения этого файла в код на стороне клиента мы и хотели избежать. Выходит, хотели как лучше, а получилось - как всегда? И что делать?

Прежде чем продолжить, я хочу особо отметить - описанное обстоятельство "свернуло шею" не одному программисту! Разнести описания клиента и сервера по разным файлам - очевиднейшее решение, оно недостойно даже особого упоминания. Вот только что делать с этим потом?

А вот этот вопрос не имеет прямого и однозначного ответа! Например, если наш код клиента - функция, то можно передать этот указатель как аргумент при вызове, только ведь где-то этот указатель первично получать всё равно придётся. Что вообще можно сделать? К сожалению, нужно признать, что в данном случае и сделать ничего нельзя. Причина этого - исключительно философская. Причина в том, что объект сервера Fooмы пытаемся создать в коде клиента - ведьnewисполнятся на клиентской стороне? А поэтому абстрактные классы здесь нам помогут очень слабо - где-то, где будет выполнятьсяnew, всё равно потребуется иметь описание статического типаFoo, т.е. если тотальной перекомпиляции всех файлов проекта(-ов) ещё можно избежать, то вот тотальной перелинковки - никогда.

Именно поэтому аппарат абстрактных классов, введённый в C++, и не стал действительно аппаратом абстракции. Он - очень полезен при проектировании большой иерархии классов, но его власть над реализацией всё равно не может выйти за пределы одного проекта.

Вот если бы, сохранив преимущества абстрактного класса - точное описание "как вызывать", ещё и как-то избавиться от его недостатков - от необходимости на клиентской стороне знать "что вызывать" мы бы получили… мы бы получили интерфейс. Ведь абсолютное безразличие к "что", но с точным определением "как" это и есть интерфейс!

Мы обнаружили - абстрактный класс очень хорошо, просто замечательно, описывает технологию взаимодействия с объектом любого производного статического типа, выстроенного на базе этого абстрактного класса. Да только вот всё равно - для полноценного обращения с объектом знания абстрактного класса недостаточно, поскольку нам обязательно нужно знать ещё и полное описание этого класса.

Как избавиться от этого недостатка? Давайте подумаем. И подумаем не так, как принято в стандартном "по-проектном мышлении", а - именно с позиций подхода "клиент-сервер".

Во-первых, очевидно, что абстрактный класс нисколько не мешает обращаться к объекту, какого бы типа он ни был. Нужно просто иметь указатель на объект (это будет "что") и пользуясь абстрактным классом вызывать его методы (это будет "как"). Поскольку получить указатель - совершенно рядовая операция, трудность находится не в указателе. А как раз в том, на что он указывает. Мы не можем сделать объект оператором newпотому, что операторnewдолжен знать не абстрактный базовый класс, а полное описание. Стоп! "...не можем..." где? Мы не можем это сделать на стороне клиента, т.к. мы и хотим избежать на стороне клиента именно полного описания класса! Но ведь на стороне сервера мы и не собирались этого избегать... Нас вполне устраивает, что неполное описание класса на стороне клиента, позволяет клиенту только лишь с объектом взаимодействовать. Вот, если бы еще сервер сам и делал этот объект, а потом передавал клиенту только указатель на него?

Это - правильная идея. Вся наша проблема только и заключается в том, что мы не можем породить объект сервера на стороне клиента. Но ведь это не означает, что мы не можем из клиента попросить сервер сделать нам объект указанного типа? Делаем в составе сервера специальную особую функцию, передаем ей тип объекта, который мы хотим получить. Сервер - заведомо знает все объекты, которые он реализует. Следовательно, сервер запрошенный объект создаст, передаст указатель клиенту, клиент применит к нему правила обращения, описываемые абстрактным классом - и проблема-то решена. Верно?

Нет! В C++статический тип исчерпывающе описывается классом. А абстрактный класс - "не тот тип", иначе его бы не отказался использовать и сам операторnew. Аnew- отказывается. Поэтому для "функции сервера, умеющей создавать объекты" абстрактный класс будет всё так же неприменим, как и для "функции клиента".

Но и это - одолимая беда. Сервер же не реализует бесконечное множество статических типов объектов? Перенумеровать их - и всего-то. И запросы ставить к нему - "сделать объект типа номер один", "сделать объект типа номер три"… Только здесь возникает вот какое обстоятельство - чтобы корректно использовать "перенумерование" мы, фактически, должны использовать пары "статический тип на стороне сервера - абстрактный тип на стороне клиента" и перенумеровывать уже пары. А это - еще хуже, поскольку малейший сбой в соответствии частей останется незамеченным никем, но в большом проекте приведет к тому, что будет непонятно даже где ошибку и искать... Тупик?

Пройдемся ещё раз - по шагам. Мы не можем создать объект даже "внутри сервера" потому, что "внутри клиента" не знаем описания его статического типа ... "внутри сервера". Это мы так захотели - мы не хотим в клиенте об объекте сервера знать больше, чем тот абстрактный тип, который позволяет с этим объектом взаимодействовать. Меньше знаешь - реже перекомпилируешься. И отказываться от этого требования мы не собираемся. Но, с другой стороны, сервер-то как раз знает и полное описание статического типа, и тотабстрактный тип, на базе которого этот статический тип построен. Почему бы нам не отправлять на сервер запросы примерно такого вида - "создать объект статического типа номер четыре, построенный на базе абстрактного типа XXX"? У сервера эта информация есть. Пусть не мы на клиентской стороне, но сам сервер отслеживает соответствует ли номер статического типа абстрактному типу и может ли он такой объект создать. Поэтому, похоже, выход находится здесь - на стороне клиента нумеровать (именовать не описанием классов) статические типы и отправлять на сервер запросы из пар "номер объекта - абстрактный тип". Когда сервер "сделает из номера" настоящий объект и передаст указатель на него клиенту, то клиенту будет уже всё равно, какой там у этого статического типа был номер, он будет применять при доступе по этому указателю исключительно абстрактный тип, который он заказывал. Эврика? Эврика...

Всё бы хорошо, да вот... Абстрактный класс - понятие времени компиляции. А создание экземпляра статического типа - понятие времени выполнения, когда никакого абстрактного класса уже и в помине нет. Способ борьбы понятен - перенумеровать и абстрактные типы так же, как перенумеруются сами статические типы, и передавать серверу пару "номер статического типа - номер абстрактного типа". Только способ этот не сработает - именно потому, что абстрактный класс есть понятие времени компиляции. В откомпилированном модуле нет никаких классов, а есть "данные + код". Пока связывание было "ранним" - на этапе компиляции и линковки, такой способ годился. Потому, что все описания классов приводились компилятором к таблицам смещений, которые он сам же и подставлял в код. А сейчас у нас связывание "позднее" и те самые таблицы компилятора уже давно не существуют... Иными словами наш славный абстрактный класс не годится только потому, что он существует лишь "в воображении компилятора" и не существует во время исполнения. И "номер абстрактного типа" - номер сущности, которой нет в тот момент, когда на неё ссылаются. А есть ли "абстрактные структуры", существующие и во время исполнения тоже?

Есть! Есть такие структуры и самое парадоксальное, что они как раз и были придуманы в языке C++для решения именно той самой проблемы, которую мы с таким трудом пытаемся решить - как сказать компилятору "как" не говоря "что". Как до поры разделить построение и использование объекта. Это - виртуальные функции.

С++- мощный язык. Мощный в том смысле, что он позволяет "по человечески естественно" выражать генетические отношения между статическими типами. Это - азбукаC++и на эту тему давно написано много, со вкусом, и в разных стилях. Так что повторять это здесь даже как-то и неудобно. И есть вC++одна интересная особенность - в иерархии наследования можно определить такую конструкцию, что программа, располагающая объектом базового класса во время исполнения будет вызывать методы не самого этого, но производного от него класса. Традиционно этот случай иллюстрируется примером, когда определяется класс "геометрическая фигура", от него наследуются классы "круг", "треугольник", "квадрат" и в базовом классе определяется метод "рисовать фигуру". Но - какую именно фигуру класс "геометрическая фигура" не знает, зато это знают класс "круг" и другие - ведь они-то себя как раз рисовать и умеют. Как сделать по человечески совершенно естественную вещь - сделать так, чтобы один и тот же метод "рисовать фигуру", принадлежащий классу "геометрическая фигура", ничего не знающему о том, какие классыещё только будутот него произведены, начал "рисовать фигуры" производных классов?

Столь сложная, на первый взгляд, проблема решается до неприличия просто - вместо простого внутреннего смещения метода "рисовать фигуру" в базовом классе компилятор генерирует настоящий указатель. И генерирует к нему пометочку - адрес метода производного класса поместить в этот указатель, принадлежащий базовому классу. Так что, когда от данного базового производится класс-потомок, компилятор видит, что адрес метода потомка нужно занести и в этот самый указатель тоже. Когда программа будет вызывать такой метод базового классаона, фактически, совершит косвенный переход по указателю на метод не базового класса, но - егопотомка. Это - классика, о ней подробно можно прочитать в любом учебнике поC++, куда желающие отсылаются за бОльшими подробностями. Для нас же важно понять, что компилятор генерирует указатель, т.е. структуру, которая в таком случае существует как раз во время исполнения. А это - именно то, что нам требовалось - если сделать абстрактный класс виртуальным, то мы получим нужную нам абстрактную структуру существующую и после окончания компиляции.

Поэтому общее решение возникшей перед нами проблемы как разделить сервер и клиент будет таким:

  • описывать интерфейс абстрактным виртуальным классом;

  • иметь в составе сервера специальную "внеклассовую" функцию, которая создавала бы объекты запрошенного типа с возвращением клиенту только указателя на них;

  • передавать этой функции запросы, состоящие из пар "номер статического типа - номер интерфейса";

  • взаимодействовать на клиентской стороне с объектом сервера только по ссылке на указатель.

Это - очень важное обстоятельство. Это - фундамент нулевого уровня на котором построен COM.