Выбрать главу

class IFastString {

public:

// faux version 1.0

// фиктивная версия 1.0

virtual void Delete(void) = 0;

virtual int Length(void) = 0;

virtual int Find(const char *psz) = 0;

// faux version 2.0

// фиктивная версия 2.0

virtual int FindN(const char *psz, int n) = 0;

};

Это решение почти работает. Те клиенты, у которых оттранслирована исходная версия интерфейса, остаются в счастливом неведении относительно всех составляющих таблицы vtbl, кроме первых трех. Когда старые клиенты получают обновленные объекты, имеющие в vtbl вход для FindN, они продолжают нормально работать. Проблема возникает, когда новым клиентам, ожидающим, что IFastString имеет четыре метода, случится столкнуться с устаревшими объектами, где метод FindN не реализуется. Когда клиент вызовет FindN на объект, странслированный с исходным описанием интерфейса, результаты будут вполне определенными. Программа прервет работу.

В этой методике проблема заключается в том, что она нарушает инкапсуляцию объекта, изменяя открытый интерфейс. Подобно тому, как изменение открытого интерфейса в классе C++ может вызвать ошибки на этапе трансляции, когда происходит перестройка клиентского кода, так и изменение двоичного описания интерфейса вызовет ошибки на этапе выполнения, когда клиентская программа перезапущена. Это означает, что интерфейсы должны быть неизменяемыми с момента первой редакции. Решение этой проблемы заключается в том, чтобы разрешить классу реализации выставлять более чем один интерфейс. Этого можно достигнуть, если предусмотреть, что один интерфейс порождается от другого, связанного с ним интерфейса. А можно сделать так, чтобы класс реализации наследовал от нескольких несвязанных классов интерфейса. В любом случае клиент мог бы использовать имеющуюся в C++ возможность определения типа на этапе выполнения – идентификацию Runtime Type Identification – RTTI, чтобы динамически опросить объект и убедиться в том, что его требуемая функциональность действительно поддерживается уже работающим объектом.

Рассмотрим простой случай интерфейса, расширяющего другой интерфейс. Чтобы добавить в IFastString операцию FindN, позволяющую находить n–е вхождение подстроки, необходимо породить второй интерфейс от IFastString и добавить в него новое описание метода:

class IFastString2 : public IFastString {

public: // real version 2.0

// настоящая версия 2.0

virtual int FindN(const char *psz, int n) = 0;

};

Клиенты могут с уверенностью динамически опрашивать объект с помощью оператора C++ dynamic_cast, чтобы определить, является ли он совместимым с IFastString2

int Find10thBob(IFastString *pfs) {

IFastString2 *pfs2 = dynamic_cast<IFastString2*>(pfs);

if(pfs2)

// the object derives from IFastString2

// объект порожден от IFastString2

return pfs2->FindN(«Bob», 10);

else {

// object doesn't derive from IFastString2

// объект не порожден от IFastString2

error(«Cannot find 10th occurrence of Bob»);

return -1;

}

Если объект порожден от расширенного интерфейса, то оператор dynamic_cast возвращает указатель на вариант объекта, совместимый с IFastString2, и клиент может вызвать расширенный метод объекта. Если же объект не порожден от расширенного интерфейса, то оператор dynamic_cast возвратит пустой (null) указатель. В этом случае клиент может или выбрать другой способ реализации, зарегистрировав сообщение об ошибке, или молча продолжить без расширенной операции. Эта способность назначенного клиентом постепенного сокращения возможностей очень важна при создании гибких динамических систем, которые могут обеспечить со временем расширенные функциональные возможности.

Иногда требуется раскрыть еще один аспект функциональности объекта, тогда разворачивается еще более интересный сценарий. Обсудим, что следует предпринять, чтобы добавить постоянства, или персистентности (persistence), классу реализации IFastString. Хотя, вероятно, можно добавить методы Load и Save к расширенной версии IFastString, другие типы объектов, не совместимые с IFastString, могут тоже быть постоянными. Простое создание нового интерфейса, который расширяет IFastString:

class IPersistentObject : public IFastString

{

public: virtual bool Load(const char *pszFileName) = 0;

virtual bool Save(const char *pszFileName) = 0;

};

требует, чтобы все постоянные объекты поддерживали также операции Length и Find. Для некоторого, весьма малого подмножества объектов это могло бы иметь смысл. Однако для того, чтобы сделать интерфейс IPersistentObject возможно более общим, он должен быть своим собственным интерфейсом, а не порождаться от IFastString:

class IPersistentObject

{

public: virtual void Delete(void) = 0;

virtual bool Load(const char *pszFileName) = 0;

virtual bool Save(const char *pszFileName) = 0;

};

Это не мешает реализации FastString стать постоянной; это просто означает, что постоянная версия FastString должна поддерживать оба интерфейса: и IFastString, и IPersistentObject:

class FastString : public IFastString, public IPersistentObject

{

int m_cch;

// count of characters

// счетчик символов

char *m_psz;

public: FastString(const char *psz);

~FastString(void);

// Common methods

// Общие методы

void Delete(void);

// deletes this instance

// уничтожает этот экземпляр

// IFastString methods

// методы IFastString

int Length(void) const;

// returns # of characters

// возвращает число символов

int Find(const char *psz) const;

// returns offset

// возвращает смещение

// IPersistentObject methods

// методы IPersistentObject

bool Load(const char *pszFileName);

bool Save(const char *pszFileName);

};

Чтобы записать FastString на диск, пользователю достаточно с помощью RTTI связать указатель с интерфейсом IPerststentObject, который выставляется объектом:

bool SaveString(IFastString *pfs, const char *pszFN)

{

bool bResult = false;

IPersistentObject *ppo = dynamic_cast<IPersistentObject*>(pfs);

if (ppo) bResult = ppo->Save(pszFN);

return bResult;

}

Эта методика работает, поскольку транслятор имеет достаточно информации о представлении и иерархии типов класса реализации, чтобы динамически проверить объект для выяснения того, действительно ли он порожден от IPersistentObject. Но здесь есть одна проблема.

RTTI – особенность, сильно зависящая от транслятора. В свою очередь, DWP передает синтаксис и семантику RTTI, но каждая реализация RTTI разработчиком транслятора уникальна и запатентована. Это обстоятельство серьезно подрывает независимость от транслятора, которая была достигнута путем использования абстрактных базовых классов как интерфейсов. Это является неприемлемым для архитектуры компонентов, не зависимой от разработчиков. Удачным решением было бы упорядочение семантики dynamic_cast без использования свойств языка, зависящих от транслятора. Явное выставление хорошо известного метода из каждого интерфейса, представляющего семантический эквивалент dynamic_cast, позволяет достичь желаемого эффекта, не требуя, чтобы все объекты использовали тот же самый транслятор C++:

class IPersistentObject

{

public: virtual void *Dynamic_Cast(const char *pszType) = 0;

virtual void Delete(void) = 0;

virtual bool Load(const char *pszFileName) = 0;

virtual bool Save(const char *pszFileName) = 0;

};

class IFastString

{