developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Откуда суть пошли интерфейсы программные?

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

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

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

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

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

class Foo{     int a;     float b; };

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

Foo Cls;   Cls.a = 12; Cls.b = 13.2;

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

class Foo private:     int a;     float b;   public:     void SetA(int i){a = i;}     void SetB(float f){b = f;}     void SetAB(int i, float f){a = i; = f;} };

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

Cls.a = 12;

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

Foo Cls;   Cls.SetA(12); Cls.SetB(13.2);

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

Первый:

//myobj.h - определение объекта Foo class Foo{ private:     int a;     float b;   public:     void SetA(int i);     void SetB(float f);     void SetAB(int i, float f); };

Второй:

//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 class FooInterface{ public:     void SetA(int i) = 0;     void SetB(float f) = 0;     void SetAB(int i, float f) = 0; };

для плохо знающих C++ - нотация <объявление функции> = 0; как раз и есть нотация объявления абстрактного метода. Она означает, что метод в данном классе не реализуется и компилятору об этом можно не беспокоиться. Но вот в каком-то производном классе компилятор должен найти реализацию этого метода - именно такого и именно с таким объявлением параметров.

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

Первый:

//myobj.h - определение объекта Foo #include <myobjint.h> //включили описание от которого наследуемся   class Foo : public FooInterface{ private:     int a;     float b;    public:     void SetA(int i);     void SetB(float f);     void SetAB(int i, float f); };

Второй:

//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 = new Foo();

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

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

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

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

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

предыдущий выпускархив и оглавлениеследующий выпуск
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

разделы сайта

форум

технология COM

оптимизация сайтов


© 2000-2004 Клуб программистов developing.ru