Возможно, одним из наиболее интересных применений этой методики является возможность для клиента динамически выбирать между различными реализациями одного и того же интерфейса. Если описание интерфейса IFastString дано как общедоступное (publicly available), то ничто не препятствует как исходному конструктору (implementor) FastString, так и любым сторонним средствам реализации порождать дополнительные классы реализации от того же самого интерфейса. Подобно исходной реализации класса FastString, эти новые реализации будут иметь такое двоичное представление, что будут совместимы на двоичном уровне с исходным классом интерфейса. Все, что должен сделать пользователь, чтобы добиться полностью совместимых («plug-compatible») реализаций, – это определить правильное имя файла для желаемой реализации DLL.
   Чтобы понять, как применить эту методику, предположим, что исходная реализация IFastString выполняла поиск слева направо. Это прекрасно для языков, анализируемых слева направо (например, английский, французский, немецкий). Для языков, анализируемых справа налево, предпочтительней вторая реализация IFastString, осуществляющая поиск справа налево. Эта альтернативная реализация может быть построена как вторая DLL с характерным именем (например, FastStringRL.DLL). Пусть обе DLL установлены на машине конечного пользователя, тогда он может выбрать нужный вариант IFastString простой загрузкой требуемой DLL на этапе выполнения:
 
   IFastString * CallCreateFastString(const char *psz, bool bLeftToRight = true)
   {
   static IFastString * (*pfnlr)(const char *) = 0;
   static IFastString * (*pfnrl)(const char *) = 0;
   IFastString *(**ppfn) (const char *) = &pfnlr;
   const TCHAR *pszDll = TEXT(«FastString.DLL»);
   if (!bLeftToRight) { pszDll = TEXT(«FastStringRL.DLL»);
   ppfn = &pfnrl; }
   if (!(*ppfn)) {
   // init ptr 1st time through
   // первое появление ptr
   const char szFn[] = «CreateFastString»;
   HINSTANCE h = LoadLibrary(pszDll);
   if (h) *(FARPROC*)ppfn = GetProcAddress(h, szFn); }
   return (*ppfn) ? (*ppfn)(psz) : 0;
   }
 
   Когда клиент вызывает функцию без второго параметра,
 
   pfs = CallCreateFastString(«Hi Bob!»);
   n = pfs->Find(«ob»);
 
   то загружается исходная DLL FastString, и поиск идет слева направо. Если же клиент указывает, что строка написана на разговорном языке, анализируемом справа налево:
 
   pfs = CallCreateFastString(«Hi Bob!», false);
   n = pfs->Find(«ob»);
 
   то загружается альтернативная версия DLL (FastStringRL.DLL ), и поиск будет начинаться с крайней правой позиции строки. Главное здесь то, что вызывающие операторы CallCreateFastString не заботятся о том, какая из DLL используется для реализации методов объекта. Существенно лишь то, что указатель на совместимый с IFastString vptr возвращается функцией и что vptr обеспечивает успешное и семантически корректное функционирование. Эта форма полиморфизма на этапе выполнения чрезвычайно полезна при создании системы, динамически скомпонованной из двоичных компонентов.
 

Расширяемость объекта

   Описанные до сих пор методики позволяют клиентам выбирать и динамически загружать двоичные компоненты, что дает возможность изменять с течением времени двоичное представление их реализации без необходимости повторной трансляции клиента. Это само по себе чрезвычайно полезно при построении динамически компонуемых систем. Существует, однако, один аспект объекта, который не может изменяться во времени, – это его интерфейс. Это связано с тем, что пользователь осуществляет трансляцию с определенной сигнатурой класса интерфейса, и любые изменения в описании интерфейса требуют повторной трансляции клиента для учета этих изменений. Хуже того, изменение описания интерфейса полностью нарушает инкапсуляцию объекта (так как его открытый интерфейс изменился) и может испортить программы всех существующих клиентов. Даже самое безобидное изменение, такое как изменение семантики метода с сохранением его сигнатуры, делает бесполезной всю установленную клиентскую базу. Это означает, что интерфейсы являются постоянными двоичными и семантическими контрактами (contracts), которые никогда не должны изменяться. Эта неизменяемость требует стабильной и предсказуемой среды на этапе выполнения.
   Несмотря на неизменяемость интерфейсов, часто возникает необходимость добавить дополнительные функциональные возможности, которые не могли быть предусмотрены в период первоначального составления интерфейса. Хотелось бы, например, использовать знание двоичного представления таблицы vtbl и просто добавлять новые методы в конец существующего описания интерфейса. Рассмотрим исходную версию IFastString:
 
   class IFastString {
   public:
   virtual void Delete(void) = 0;
   virtual int Length(void) = 0;
   virtual int Find(const char *psz) = 0;
   };
 
   Простое изменение класса интерфейса путем объявлений добавочных виртуальных функций после объявлений существующих методов имело бы следствием двоичный формат таблицы vtbl, который является надмножеством исходной версии по мере того, как появятся какие-либо новые элементы vtbl после тех, которые соответствуют исходным методам. У реализации объектов, которые транслируются с новым описанием интерфейса, все новые методы будут добавляться к исходному размещению vtbl:
 
   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
   {
   public: virtual void *Dynamic_Cast(const char *pszType) = 0;
   virtual void Delete(void) = 0;
   virtual int Length(void) = 0;
   virtual int Find(const char *psz) = 0;
   };
 
   Так как всем интерфейсам необходимо выставить этот метод вдобавок к уже имеющемуся методу Delete, имеет большой смысл включить общее подмножество методов в базовый интерфейс, из которого могли бы порождаться все последующие интерфейсы:
 
   class IExtensibleObject { public: virtual void *Dynamic_Cast(const char* pszType) = 0; virtual void Delete(void) = 0; }; class IPersistentObject : public IExtensibleObject { public: virtual bool Load(const char *pszFileName) = 0; virtual bool Save(const char *pszFileName) = 0; }; class IFastString : public IExtensibleObject { public: virtual int Length(void) = 0; virtual int Find(const char *psz) = 0; };
 
   Имея такую иерархию типов, пользователь может динамически запросить объект о данном интерфейсе с помощью следующей не зависящей от транслятора конструкции:
 
   bool SaveString(IFastString *pfs, const char *pszFN) { boot bResult = false; IPersistentObject *ppo = (IPersistentObject) pfs->Dynamic_Cast(«IPers1stentObject»); if (ppo) bResult = ppo->Save(pszFN); return bResult; }
 
   В только что приведенном примере клиентского использования присутствуют требуемая семантика и механизм для определения типа, но каждый класс реализации должен выполнять это функциональное назначение самолично:
 
   class FastString : public IFastString, public IPersistentObject
   {
   int m_cсh;
   // count of characters
   // счетчик символов
   char *m_psz;
   public:
   FastString(const char *psz);
   ~FastString(void);
   // IExtensibleObject methods
   // методы IExtensibleObject
   void *Dynamic_Cast(const char *pszType);
   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);
   };
 
   Реализации Dynamic_Cast необходимо имитировать действия RTTI путем управления иерархией типов объекта. Рисунок 1.8 иллюстрирует иерархию типов для только что показанного класса FastString. Поскольку класс реализации порождается из каждого интерфейса, который он выставляет, реализация Dynamic_Cast в FastString может просто использовать явные статические приведения типа (explicit static casts), чтобы ограничить область действия указателя this, основанного на подтипе, который запрашивается клиентом:
 
   void *FastString::Dynam1c_Cast(const char *pszType)
   {
   if (strcmp(pszType, «IFastString») == 0) return static_cast<IFastString*>(this);
   else if (strcmp(pszType, «IPersistentObject») == 0) return static_cast<IPersistentObject*>(this);
   else if (strcmp(pszType, «IExtensibleObject») == 0) return static_cast<IFastString*>(this);
   else return 0;
   // request for unsupported interface
   // запрос на неподдерживаемый интерфейс
   }
 
 
 
   Так как объект порождается от типа, используемого в этом преобразовании, оттранслированные версии операторов преобразования просто добавляют определенное смещение к указателю объекта this, чтобы найти начало представления базового класса.
   Отметим, что после запроса на общий базовый интерфейс IExtensibleObject реализация статически преобразуется в IFastString. Это происходит потому, что интуитивная версия (intuitive version) оператора
   return static_cast<IExtensibleObject*>(this);
   неоднозначна, так как и IFastString, и IPersistentObject порождены от IExtensibleObject. Если бы IExtensibleObject был виртуальным базовым классом как для IFastString, так и для IPersistentObject, то данное преобразование не было бы неоднозначным и оператор бы оттранслировался. Тем не менее, применение виртуальных базовых классов добавляет на этапе выполнения ненужную сложность в результирующий объект и к тому же вносит зависимость от транслятора. Дело в том, что виртуальные базовые классы являются всего лишь особенностями языка C++, которые имеют несколько специфических реализации.
 

Управление ресурсами

   Еще одна проблема поддержки нескольких интерфейсов из одного объекта становится яснее, если исследовать схему использования клиентом метода DynamicCast. Рассмотрим следующую клиентскую программу:
 
   void f(void)
   {
   IFastString *pfs = 0;
   IPersistentObject *ppo = 0;
   pfs = CreateFastString(«Feed BOB»);
   if (pfs) {
   ppo = (IPersistentObject *) pfs->DynamicCast(«IPersistentObject»);
   if (!ppo) pfs->Delete();
   else { ppo->Save(«C:\\autoexec.bat»);
   ppo->Delete(); }
   }
   }
 
   Хотя вначале объект был связан через свой интерфейс IFastString , клиентский код вызывает метод Delete через интерфейс IPersistentObject. С использованием свойства C++ о множественном наследовании это вполне допустимо, так как все таблицы vtbl , порожденные классом IExtensibleObject, укажут на единственную реализацию метода Delete . Теперь, однако, пользователь должен хранить информацию о том, какие указатели связаны с какими объектами, и вызывать Delete только один раз на объект. В случае простого кода, приведенного выше, это не слишком тяжелое бремя. Для более сложных клиентских кодов управление этими связями становится делом весьма сложным и чреватым ошибками. Одним из способов упрощения задачи пользователя является возложение ответственности за управление жизненным циклом объекта на реализацию. Кроме того, разрешение клиенту явно удалять объект вскрывает еще одну деталь реализации: тот факт, что объект находится в динамически распределяемой памяти (в «куче», on the heap).
   Простейшее решение этой проблемы – ввести в каждый объект счетчик ссылок, который увеличивается, когда указатель интерфейса дублируется, и уменьшается, когда указатель интерфейса уничтожается. Это предполагает изменение определения IExtensibleObject с
 
   class IExtensibleObject
   {
   public:
   virtual void *DynamicCast (const char* pszType) =0;
   virtual void Delete(void) = 0;
   };
 
   на
 
   class IExtensibleObject
   {
   public:
   virtual void *DynamicCast(const char* pszType) = 0;
   virtual void DuplicatePointer(void) = 0;
   virtual void DestroyPointer(void) = 0;
   };
 
   Разместив эти методы, все пользователи IExtensibleObject должны теперь придерживаться следующих двух соображений:
   1) Когда указатель интерфейса дублируется, требуется вызов DuplicatePointer.
   2) Когда указатель интерфейса более не используется, следует вызвать DestroyPointer.
   Эти методы могут быть реализованы в каждом объекте: нужно просто фиксировать количество действующих указателей и уничтожать объект, когда невыполненных указателей не осталось:
 
   class FastString : public IFastString,
   public IPersistentObject
   {
   int mcPtrs;
   // count of outstanding ptrs
   // счетчик невыполненных указателей
   public:
   // initialize pointer count to zero
   // сбросить счетчик указателя в нуль
   FastString(const char *psz) : mcPtrs(0) { }
   void DuplicatePointer(void)
   {
   // note duplication of pointer
   // отметить дублирование указателя
   ++mcPtrs;
   }
   void DestroyPointer(void)
   {
   // destroy object when last pointer destroyed
   // уничтожить объект, когда уничтожен последний указатель
   if (-mcPtrs == 0) delete this;
   }
   : : :
   };
 
   Этот совершенно стандартный код мог бы просто быть включен в базовый класс или в макрос С-препроцессора, чтобы его могли использовать все реализации.
   Чтобы поддерживать эти методы, все программы, которые манипулируют или управляют указателями интерфейса, должны придерживаться двух простых правил DuplicatePointer/DestroyPointer. Для реализации FastString это означает модификацию двух функций. Функция CreateFastString берет начальный указатель, возвращаемый новым оператором C++, и копирует его в стек для возврата клиенту. Следовательно, необходим вызов DuplicatePointer:
 
   IFastString* CreateFastString(const char *psz)
   {
   IFastString *pfsResult = new FastString(psz);
   if (pfsResult) pfsResult->DuplicatePointer();
   return pfsResult;
   }
 
   Реализация копирует указатель и в другом месте – в методе Dynamic_Cast:
 
   void *FastString::Dynamic_Cast(const char *pszType)
   {
   void *pvResult = 0;
   if (strcmp(pszType, «IFastString») == 0) pvResult = static_cast<IFastString*>(this);
   else if (strcmp(pszType, «IPersistentObject») == 0) pvResult = static_cast<IPersistentObject*>(this); 
   else if (strcmp(pszType, «IExtensibleObject») == 0) pvResult = static_cast<IFastString*>(this);
   else return 0;
   // request for unsupported interface
   // запрос на неподдерживаемый интерфейс
   // pvResult now contains a duplicated pointer, so
   // we must call DuplicatePointer prior to returning
   // теперь pvResult содержит скопированный указатель,
   // поэтому нужно перед возвратом вызвать DuplicatePointer
   ((IExtensibleObject*)pvResult)->DuplicatePo1nter();
   return pvResult;
   }
   С этими двумя усовершенствованиями соответствующий код пользователя становится значительно более однородным и прозрачным:
 
   void f(void)
   {
   IFastString *pfs = 0;
   IPersistentObject *ppo = 0;
   pfs = CreateFastString(«Feed BOB»);
   if (pts) {
   рро = (IPersistentObject *) pfs->DynamicCast(«IPersistentObject»);
   if (ppo) { ppo->Save(«C:\\autoexec.bat»);
   ppo->DestroyPointer(); }
   pfs->DestroyPointer(); }
   }
 
   Поскольку каждый указатель теперь трактуется как автономный объект с точки зрения времени жизни, клиенту можно не интересоваться тем, какой указатель соответствует какому объекту. Вместо этого клиент просто придерживается двух простых правил и предоставляет объектам самим управлять своим временем жизни. При желании способ вызова DuplicatePointer и DestroyPointer можно легко скрыть за интеллектуальным указателем (smart pointer) C++.
   Использование этой схемы вычисления ссылок позволяет объекту весьма единообразно выставлять множественные интерфейсы. Возможность выставления нескольких интерфейсов из одного класса реализации позволяет типу данных участвовать в различных контекстах. Например, новая постоянная подсистема могла бы определить собственный интерфейс для управления автозагрузкой и автозаписью объектов на некоторый специализированный носитель. Класс FastString мог бы добавить поддержку этих возможностей простым наследованием от постоянного интерфейса этой подсистемы. Добавление этой поддержки никак не повлияет на уже установленные базы клиентов, которые, может быть, используют прежний постоянный интерфейс для записи и загрузки строки на диск. Механизм согласования интерфейсов на этапе выполнения может служить краеугольным камнем для построения динамической системы из компонентов, которые могут изменяться со временем.
 

Где мы находимся?

   Мы начали эту главу с простого класса C++ и рассмотрели проблемы, связанные с объявлением этого класса как двоичного компонента повторного использования. Первым шагом было употребление этого класса в качестве библиотеки Dynamic Link Library (DLL) для отделения физической упаковки этого класса от упаковок его клиентов. Затем мы использовали понятие интерфейсов и реализации для инкапсуляции элементов реализации типов данных за двоичной защитой, что позволило изменять двоичные представления объектов без необходимости перетрансляции клиентами. Затем, используя для определения интерфейсов подход абстрактного базового класса, эта защита приобрела форму указателя vptr и таблицы vtbl. Далее мы исследовали приемы для динамического выбора различных полиморфных реализаций данного интерфейса на этапе выполнения с использованием LoadLibrary и GetProcAddress. Наконец, мы использовали RTTI-подобную структуру для динамического опроса объекта с целью определить, действительно ли он использует нужный интерфейс. Эта структура предоставила нам методику расширения существующих версий интерфейса, а также возможность выставления нескольких несвязанных интерфейсов из одного объекта.
   Короче, мы только что создали модель компонентных объектов (Component Object Model – СОМ).
 

Глава 2. Интерфейсы

   void *pv = malloc(sizeof(int));
   int *pi = (int*)pv;
   (*pi)++;
   free(pv);
Аноним,1982

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

Снова об интерфейсах и реализациях

   Цель отделения интерфейса от реализации заключалась в сокрытии от клиента всех деталей внутренней работы объекта. Этот фундаментальный принцип предусматривал уровень косвенности, или изоляции (level of indirection), который позволял изменяться количеству или порядку элементов данных в реализации класса без перекомпиляции клиента. Кроме того, этот принцип позволял клиентам обнаруживать расширенную функциональность путем опроса объекта на этапе выполнения. И, наконец, этот принцип позволяет сделать библиотеку DLL независимой от транслятора C++, который используется клиентом.
   Хотя этот последний аспект и полезен, он далеко не достаточен для обеспечения универсальной основы для двоичных компонентов. Важно отметить, что хотя клиенты могут использовать любой выбранный ими транслятор C++, в конечном счете это будет всего лишь транслятор C++. Приемы, описанные в предыдущей главе, обеспечивают независимость от транслятора. В конце концов, главное, что необходимо для создания действительно универсальной основы для двоичных компонентов, – это независимость от языка. А чтобы достичь независимости от языка, принцип отделения интерфейса от реализации должен быть применен еще раз.
   Рассмотрим определения интерфейса, использованные в предыдущей главе. Каждое определение интерфейса принимало форму определения абстрактного базового класса C++ в заголовочном файле C++. Тот факт, что определение интерфейса постоянно находится в файле, читаемом только на одном языке, вскрывает один остаточный признак реализации этого объекта – язык, на котором он был написан. Но, в конечном счете, объект должен быть доступен для любого языка, а не только для того, который выбрал разработчик объекта. Предусматривая только совместимое с C++ определение интерфейса, разработчик объекта тем самым вынуждает всех использующих этот объект также работать на C++.