пятница, 5 августа 2011 г.

Как добавить published свойство без нарушения совместимости DCU

Это перевод How to add a "published" property without breaking DCU compatibility. Автор: Allen Bauer.

Во-первых, если вы пурист ООП или фреймворков и имеете слабый желудок - лучше закройте эту страницу прямо сейчас.

В ожидаемом релизе Delphi 2007 for Win32 мы взяли беспрецедентный подход к расширению функциональности VCL. Если вы использовали Delphi более нескольких её версий, то вы очень хорошо знаете, что такое смена версии - это необходимость получить новые версии всех компонентов. Я не буду сейчас обсуждать причины этого, поскольку я уже неоднократно покрыл это в деталях за всё это время. В основном это сводится к тому, что нам нужна гибкость для внесения изменений в существующие классы и сам компилятор по своему усмотрению, чтобы обеспечить лучшее общее впечатление, какое только мы можем. Таким образом, для всех вас, кто собирается делать возмущённые комментарии по поводу "почему бы вам не делать это в каждом выпуске!?" - если бы мы делали это, то вы бы никогда не имели новых возможностей компилятора - пакетов, интерфейсов, классовых переменных, встроенных (inline) функций, а также генериков (пост-Delphi 2007). Просто просмотрите некоторые из моих прошлых постов для более подробной информации.

И несмотря на всё вышесказанное, мы решили следовать иному подходу при выпуске Delphi 2007. D2007 предназначено быть "non-breaking" выпуском. Что в точности это означает? В корне это означает, что вы можете просто взять большинство своих компонентов и модулей, собранных в BDS2006, и установить их в D2007. Я говорю "большинство" потому что, конечно же, будут какие-то краевые случаи, когда это не будет возможным. К примеру, если компонент работает с какими-то внутренними вещами в обход общедоступного/документированного интерфейса, а эти "вещи за сценой" изменены - такое поведение не является целью для "non-breaking" изменений. До тех пор, пока ваш (или сторонний) код придерживается общепринятого контракта - он будет работать без перекомпиляции.

Эмм... а что-ж тогда нового?? Это правда, что мы изменили много кода, и эти изменения были сделаны только в секции implementation - так что мы не меняли интерфейс и не нарушили совместимость двоичных DCU. Но в новостях объявлено о некоторых изменениях, которые проявляются либо в новых компонентах, либо в новых published свойствах! Правила по совместимости типов компилятора Delphi обманчиво просты. В принципе, для любого структурированного типа (класса, записи, объекта и т.п.) "версия" этого типа или символа выводится от всех входящих в его определение типов и символов. Так что вы не можете модифицировать существующий тип класса или объявление метода без смены "версии". Это приведёт к появлению сообщения об ошибке "xxx was compiled with a different version of yyy”.

Так как же мы сделали это?

Проницательные читатели, которые читали мой блог, уже, вероятно, забежали вперёд и подумали про... конечно же, class helper-ы! Ну, да, вы были бы правы на этот счет, но только до определённой степени. Я сказал, что новое свойство "опубликовано" (published) - в том смысле, что это означает проявление свойства в Object Inspector в IDE. Так что тут есть немного больше, чем только class helper. Как выяснилось, используя class helper, мы можем сделать это новое свойство видимым для исходного кода пользователя - видимым как свойство TCustomForm, что и является желаемым эффектом. Так что этим мы просто добавили свойство, фактически, не изменяя объявление типа TCustomForm. Но остаётся вопрос, где же хранить это свойство, если в классе не появляются новые поля. Здесь вы должны отвести ваши глаза в сторону, если вас тошнит от хаков ;-) Мы просто берём и перегружаем дополнительным смыслом уже существующее, но мало используемое 32-битное private поле в классе TCustomForm, и создаём внутреннюю (только в секции implementation) запись, содержащую само старое поле, которое мы заместили, а также любые другие данные, которые нам вздумается хранить. После этого мы проходимся по всему коду в Forms.pas, который обращается к замещаемому полю, и меняем код так, чтобы он использовал новое поле по указателю на нашу запись. Поскольку это private поле, то к нему не обращается никто за рамками модуля Forms.pas. Это хороший пример, когда один из основополагающих принципов ООП (инкапсуляции) помог нам добиться этого. Мы смогли сделать эти изменения, и никакой другой код или классы не пострадали.

Этот трюк позаботится о хранилище для данных под капотом интерфейса. Поговорим теперь о сериализации этих данных - не говорил ли я, что это должно быть published свойство? Это как раз то место, где нам помогли наши прошлые улучшения VCL/RTL. TCustomForm уже имеет переопределённый метод DefineProperties, так что нам нужно было просто добавить в него немного больше логики. Здесь снова пригодились class helper-ы, чтобы поместить в класс новые методы чтения/записи - опять же, без изменения самого класса TCustomForm. Теперь у нас есть новое свойство в классе TCustomForm, включенное в механизм сериализации и доступное любому коду - как свойство класса TCustomForm и любых его потомков. Заключительный штрих заключается в отображении свойства в Инспекторе объектов (Object Inspector).

Тут на сцену выходит интерфейс ISelectionPropertyFilter. Вы можете помнить или не знать, что этот небольшой интерфейс (всего из одного метода) был введён в BDS2006 в модуле DesignIntf. Для тех из вас, кто сейчас работает в BDS2006 - вы можете открыть модуль DesignIntf.pas и прочитать все объявления и пояснения сами, а для всех остальных я приведу их здесь:
  { ISelectionPropertyFilter
    This optional interface is implemented on the same class that implements
    ISelectionEditor.  If this interface is implemented, when the property list
    is constructed for a given selection, it is also passed through all the various
    implementations of this interface on the selected selection editors.  From here
    the list of properties can be modified to add or remove properties from the list.
    If properties are added, then it is the responsibility of the implementor to
    properly construct an appropriate implementation of the IProperty interface.
    Since an added "property" will typically *not* be available via the normal RTTI
    mechanisms, it is the implementor’s responsibility to make sure that the property
    editor overrides those methods that would normally access the RTTI for the
    selected objects.

      FilterProperties
        Once the list of properties has been gathered and before they are sent to the
        Object Inspector, this method is called with the list of properties.  You may
        manupulate this list in any way you see fit, however, remember that another
        selection editor *may* have already modified the list.  You are not guaranteed
        to have the original list.
  }

  ISelectionPropertyFilter = interface
    ['{0B424EF6-2F2F-41AB-A082-831292FA91A5}']
    procedure FilterProperties(const ASelection: IDesignerSelections; const ASelectionProperties: IInterfaceList);
  end;
Итак, базовым трюком тут является регистрация редактора выбора (selection editor) для TCustomForm, который реализовывал бы этот интерфейс. Когда вызывается FilterProperties, мы можем манипулировать списком интерфейсов IProperty в параметре ASelectionProperties. Вы можете вручную добавлять и удалять свойства. К примеру, когда вы бросаете на форму TFlowPanel, а затем бросаете на неё другие компоненты - то вы заметите, что у них исчезают свойства Left и Top. Это не потому, что их нет у компонента, а потому что они бесполезны в design-time (положением компонентов управляет панель), поэтому они удаляются из списка ASelectionProperties. В случае с TCustomForm мы просто хотим добавить свойство. Так что мы ищем в списке свойств точку вставки и создаём класс, реализующий IProperty. Теперь ваше "published" свойство показывается в Инспекторе объектов!

Хотя всё это кажется огромной кучей работы, просто для того, чтобы сохранить обратную совместимость, хорошей новостью является то, что после выхода Delphi 2007, в новой версии Delphi мы можем свернуть все эти изменения обратно в класс TCustomForm, удалив class helper-ы и ISelectionPropertyFilter, а существующие DFM файлы будут работать без изменений. Равно как и весь ваш код, работающий с этим новым свойством. В общем, это является интересной вещью - proof-of-concept того, как можно расширять функциональность существующего кода без ломания совместимости DCU/компонентов/пакетов. Вполне может быть, что все последующие релизы будут "breaking"-релизами, но всё равно это было полезно - просто некоторые вещи, которые нужно иметь в виду в будущем.

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

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

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

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

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

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

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