При закрытии подписчики были переданы в рассылку "Мастерская программиста" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Что такое "технология COM" и как с ней бороться? №13
Откуда суть пошли интерфейсы программные?
Итак, мы продолжаем наше изложение. Что такое интерфейс в его программном исполнении? Ответ на него может быть и длинным и коротким - мы видели, что даже объявление прототипа функции уже можно считать интерфейсом. И программирование пользуется этим очень давно.
В действительности же интерфейс стали называть отдельным специальным словом тогда, когда стало очевидно - сложность программных конструкций стала такой, что программист уже не может удержать во внимании оба взаимодействующих объекта. Пока это удавалось "объявление функции" выполняло страхующую роль - а вдруг где забыто чего? Но программист не чувствовал ни малейших неудобств в произвольном изменении списка аргументов функции - если по ходу развития программы требовалось этот список изменить, то он менял его в определении функции и в её объявлении. После чего запускал компилятор и тот показывал ему список мест, где нужно поправить.
Проблемы начинались тогда, когда, образно говоря, "задачи стали занимать более десяти тысяч строк". Эти задачи уже не помещались в один "программный проект". Классическим же примером этого опять является "клиент-сервер" - его наиболее удобная реализация как раз и выглядит в виде двух самостоятельных программных проектов - проекта клиента и проекта сервера. А границу между проектами компилятор преодолевать не умеет…
Это, конечно, случилось совсем не вдруг - разработчики языков программирования видели эту проблему и пытались предложить для нее адекватное своему времени решение. Например, в языке C++ есть понятие чисто абстрактного класса, которое совершенно явно вводит понятие интерфейса между объектами. Оно в самом буквальном смысле явилось предтечей интерфейса COM и на нём стóит остановиться подробнее. Давайте кратко рассмотрим откуда возникла эта проблема и в чём она состоит.
Рассмотрим класс (статический тип), описывающий объект сервера:
class Foo{
int a;
float b;
};
и рассмотрим код клиента:
Foo Cls;
Cls.a = 12;
Cls.b = 13.2;
Это, как теперь канонически признаётся - нехороший код. Нехорошо иметь прямой доступ к данным класса помимо самого класса. Что вот будет, если нам придётся переименовать a - уж больно это невнятный идентификатор? "По науке" нам нужно делать только так:
classFoo{
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; b = f;}
};
Теперь исполнить предложение в теле клиента:
Cls.a = 12;
не даст компилятор - данные-то у нас - private. Клиент придётся переписать:
Foo Cls;
Cls.SetA(12);
Cls.SetB(13.2);
В таком случае клиент наш может ничего и не знать о том, что такие переменные, как a и b в классе есть. Но для того, чтобы клиент мог быть откомпилирован, чтобы компилятор сумел правильно организовать вызов методов SetA и SetB, при его компиляции нам всё равно придется подавать на вход компилятору определение класса. Допустим, что мы поместили его в заголовочный файл myobj.h, который мы будем подавать компилятору и при компиляции кода сервера и, естественно, при компиляции кода клиента. Теперь у нас есть три файла исходного текста:
Первый:
//myobj.h - определение объекта Foo
classFoo{
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
classFooInterface{
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, М. Безверхов
Публикация требует разрешения автора.
http://subscribe.ru/
E-mail: ask@subscribe.ru |
В избранное | ||