среда, 16 февраля 2011 г.

Правила интерфейсов COM существуют не просто так

Это перевод The COM interface contract rules exist for a reason. Автор: Реймонд Чен.

Некоторые люди считают, что правила COM по интерфейсам неоправданно строги. Но для таких правил есть причина.

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

Но вспомните, что если вы меняете интерфейс, то вам нужно сгенерировать новый IID (идентификатор интерфейса). Потому что идентификатор интерфейса уникально идентифицирует интерфейс (в конце концов, именно это означает его название - "идентификатор").

И это применимо к любым интерфейсам - в том числе и внутренним.

Предположим, что вы решили нарушить это правило и использовать тот же IID для немного изменённого интерфейса в версии N+1 вашей программы. Поскольку это внутренний интерфейс, то вы не постесняетесь это сделать.

До тех пор пока вам не придётся написать дополнение (патч), обслуживающий обе версии.

Теперь у этого патча есть проблемы. Он может вызвать IUnknown.QueryInterface и запросить интерфейс по этому IID, и он что-то получит в ответ. Только вот он не знает, это ему вернули версию N интерфейса, или же версию N+1. А если вы не в курсе, что такое могло произойти, то ваш патч просто предположит, что это версия интерфейса N+1 - и если в действительности ему вернули версию N, то начнут происходить всякие забавные вещи.

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

Заметьте, что эта зависимость может быть скрыта за другими интерфейсами. Посмотрите:
type
  IColorInfo = interface
  {ABC}
    function GetBackgroundColor: TColorRef; safecall;
    ...
  end;

  IGraphicImage = interface
  {XYZ}
    ...
    function GetColorInfo: IColorInfo; safecall;
  end;
Предположим, что вы хотите добавить новый метод в интерфейс IColorInfo:
type
  IColorInfo = interface
  {DEF}
    function GetBackgroundColor: TColorRef; safecall;
    ...
    procedure AdjustColor(const clrOld, clrNew: TColorRef); safecall;
  end;

  IGraphicImage = interface
  {XYZ}
    ...
    function GetColorInfo: IColorInfo; safecall;
  end;
Вы изменили интерфейс, но вы также поменяли и IID, так что всё должно быть в порядке, да?

Вообще-то - нет.

Интерфейс IGraphicImage зависит от интерфейса IColorInfo. Когда вы изменили интерфейс IColorInfo, вы неявно изменили и метод IGraphicImage.GetColorInfo - поскольку его возвращаемое значение стало теперь другим: интерфейсом IColorInfo версии N+1.

Посмотрите на такой код, написанный с заголовочными файлами версии N+1:
procedure AdjustGraphicColorInfo(pgi: IGraphicImage; const clrOld, clrNew: TColorRef);
var
  pci: IColorInfo;
begin
  pci := pgi.GetColorCount(pci);
  pci.AdjustColor(clrOld, clrNew);
end;
Если этот код запускается на версии N, то вызов IGraphicImage.GetColorCount вернёт IColorInfo версии N, а у этой версии нет метода IColorInfo.AdjustColor. Но вы всё равно его вызываете. Результат: проходим до конца таблицы методов интерфейса и вызываем мусор, который лежит за ней.

Быстрое решение проблемы - изменение IID для IGraphicImage, чтобы учесть изменения в IColorInfo:
type
  IGraphicImage = interface
  {UVW}
    ...
    function GetColorInfo: IColorInfo; safecall;
  end;
Более надёжным решением было бы изменение метода IGraphicImage.GetColorInfo так, чтобы вы могли указывать, какой интерфейс ы хотите получить:
type
  IGraphicImage = interface
  {RST}
    ...
    procedure GetColorInfo(const ARIID: REFIID; out ppv); safecall;
  end;
Этот вариант позволяет интерфейсам, от которых зависи IGraphicImage меняться как угодно, без необходимости менять сам интерфейс IGraphicImage. Конечно же, реализации метода GetColorInfo всё ещё нужно меняться, чтобы учитывать новые варианты интерфейса IColorInfo. Но теперь вызывающий может быть спокоен, зная, что когда он запрашивает интерфейс, он получит именно его, а не что-то другое, случайно имеющее тот же идентификатор/имя.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.