среда, 7 июля 2010 г.

Хак №5: Доступ к private-полям

Это перевод Hack #5: Access to private fields. Автор: Hallvard Vassbotn.

Как вам говорила ваша мама, вам нужно защищать ваши личные данные и не трогать личные данные других людей. Это же применимо и в программировании. Инкапсуляция и скрытие информации - это два главных столпа в ООП, так что нам лучше бы не вмешиваться в это, точно? Точно. Но однажды при полной луне у меня возникла неодолимая жажда достучаться до private-полей другого класса (объявленного в другом модуле).

Когда возможен, ранее упомянутый хак является предпочтительным вариантом перед техникой, описанной в этой статье. Однако, иногда этот хак не практичен или невозможен. Использование RTTI - это относительно медленно (хотя вы можете применять кэширование), а private-поля могут быть и вовсе не раскрываться через свойства. Кроме того, если вам нужен доступ сразу ко всем private-полям, то решение, описываемое тут, также может быть более предпочтительным.

Как я упоминал ранее, концепция Delphi private данных (по сравнению со strict private в D2006 и выше) всё ещё позволяет другому коду в том же самом модуле обращаться с полным доступом к данным класса, но при этом исключает доступ из других модулей. Иными словами, все классы и подпрограммы в одном модуле непременно являются "друзьями" друг друга (в смысле C++).

Замечали ли вы как взаимодействуют некоторые VCL классы друг с другом? Например, абстрактный базовый класс для системы персистентности компонент (TFiler) определяет несколько private-полей. Но в действительности, с этими полями работают другие классы, объявленные в том же модуле: TReader и TWriter (оба, впрочем, наследники TFiler). Оба эти класса объявлены в этом же модуле и грубо используют private-поля, определённые в TFiler (к примеру, самое-важное-поле FStream).

И если вы захотите написать свою собственную систему персистентности уровня приложения (скажем, со способностью писать в поток и авто-создавать при загрузке не только наследников TComponent), которая будет, как и стандартная, участвовать в системе TPersistent.DefineProperties, то вам нужно расширить классы TReader и TWriter. Вам может показаться, что эти классы были спроектированы для расширения - ведь они объявляют новые виртуальные методы. Но ваши собственные классы TMyReader и TMyWriter не имеют доступа ни к private-полям TFiler ни к private-полям TReader и TWriter. Это может(*) серьёзно ограничить ваши возможности по расширению.

Окей, хватит уже рассуждений - перейдём к главному блюду. Чтобы получить доступ к private-полям класса TFiler, объявите shadow-класс (иногда называемый dummy-класс), который в точности соответствует раскладке полей в TFiler из Classes.pas:
type   
  TFilerHack = class(TObject)   
  private     
    FStream: TStream; // интересующее нас свойство. 
    // Если бы оно шло не первым - мы должны были бы перечислить все поля до него _в точности_    

    // ... тут идут другие поля - они нас не интересуют, поэтому их можно опустить
  end;
Теперь, если у вас есть экземпляр TFiler, вы легко можете привести его тип к TFilerHack и получить доступ к полям:
procedure Foo(Filer: TFiler); 
var   
  Stream: TStream; 
begin   
  Stream := TFilerHack(Filer).FStream; 
end;
Заметьте, что я использовал директиву видимости private в объявлении поля в классе TFilerHack - это всё равно даст вам доступ к полю для кода в том же модуле. Обычно, хак-классы подобного толка должны объявляться в секции implementation модуля.

Конечно же, этот хак чрезвычайно чувствителен к изменениям в исходном классе. Сравните это с другим решением: вычислением "волшебного смещения" поля от начала класса. Это решение имеет то преимущество, что всю работу по вычислению смещения вы перекладываете на компилятор. Кроме того, при этом получается, что вы защищены от изменений в предках хакаемого класса (в случае TFiler это только TObject). Если вам где-то придётся использовать этот хак, то убедитесь, что вы использовали {$IFDEF} или {$IF} для защиты кода от изменений в компиляторе, RTL или сторонней библиотеке.

(*) Несколько лет назад (сидя на D5), я вообще-то реализовал свою собственную систему персистентности, написанием своих собственных классов TReader и TWriter и используя хаки, похожие на описываемые в этой статье. С тех пор TFiler, TReader и TWriter были улучшены и в частности улучшены для расширения - к примеру, предоставляя protected-доступ к некоторым бывшим private-полям (все дружно сказали "спасибо" Borland CodeGear!). Так что вполне возможно, что эта система может быть переписана без хаков на D7, но я это не проверял. Я просто буду использовать TFiler как пример для иллюстрации принципа.

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

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

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

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

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

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

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