10 мая 2020

Доступ к protected членам класса

 Я когда-то читал книгу еврейского писателя Шолом-Алейхема... У него там была занятная строка: "Если нельзя, но очень хочется, то можно"
 © Штирлиц "Семнадцать мгновений весны"

    Все члены класса обладают одним важным атрибутом – область видимости. Область видимости определяется специальными ключевыми словами private, protected, public, published и automated, которые называются модификаторами доступа. Сегодня я хочу сказать пару слов о модификаторе доступа protected. Члены класса, которые защищены им видны в любом классе являющимся его наследником и в том модуле, где описан класс. Эту область видимости можно сузить, если к модификатору "protected" добавить слово "strict", тогда эти члены класса увидят только его наследники.
    Зачем это надо? Например, за protected, а лучше за strict protected можно скрыть абстрактный метод. Это избавит программиста, который будет использовать класс, от желания вызвать этот метод. Но иногда авторы классов делают обычные методы protected. Самое печальное, что таким образом они скрывают много полезного. Зачем? Разумного объяснения этому я пока не слышал. Некоторые говорят, что причина в реализации одного из основных принципов ООП – инкапсуляции. Но инкапсуляция подразумевает под собой скрытие членов класса от посторонних глаз. А о какой инкапсуляции может вестись речь, если protected члены класса можно легко увидеть и использовать?
    Давайте посмотрим ситуацию на примере. Например, в модуле uXyz у нас есть класс:
unit uXyz;

interface

type
  TXyz = class
  strict protected
    procedure StrictProtectedProc;
  protected
    procedure ProtectedProc;
    procedure DoSomething; virtual; abstract;
  public
  end;

implementation

{ TXyz }

procedure TXyz.ProtectedProc;
begin
  Writeln('ProtectedProc');
end;

procedure TXyz.StrictProtectedProc;
begin
  Writeln('StrictProtectedProc');
end;

// --------------------------

procedure Test;
var
  xyz: TXyz;
begin
  xyz := TXyz.Create;
  xyz.ProtectedProc;
  xyz.Free;
end;

end.
Любая процедура, в модуле uXyz имеет доступ к protected методу ProtectedProc. Так, где тут инкапсуляция? А вот обращение в процедуре Test к методу StrictProtectedProc приведет к ошибке при компиляции:
[dcc32 Error] uXyz.pas(35): E2362 Cannot access protected symbol TXyz.StrictProtectedProc
Теперь перенесем процедуру Test в другой модуль:
program TestXyz;

{$APPTYPE CONSOLE}

uses
  uXyz in 'uXyz.pas';

procedure Test;
var
  xyz: TXyz;
begin
  xyz := TXyz.Create;
  xyz.ProtectedProc;
  xyz.Free;
end;
Попытка компиляции программы приводит к ошибке:
[dcc32 Error] TestXyz.dpr(13): E2362 Cannot access protected symbol TXyz.ProtectedProc
Это как раз пример того самого случая, когда автор класса спрятал нужный нам метод за модификатором protected. Этим часто грешат некоторые разработчики библиотек для Delphi. Конечно, при наличии исходных кодов библиотеки этот метод спокойно можно перенести в public. А, что делать если исходных кодов нет? Воспользуемся тем, что protected члены класса видят его наследники и объявим класс "пустышку":
type
  TXyzHack = class(TXyz);
через который вызовем спрятанный от нас метод ProtectedProc:
procedure Test;
var
  xyz: TXyzHack;
begin
  xyz := TXyzHack.Create;
  xyz.ProtectedProc;
  xyz.Free;
end;
или так
procedure Test;
var
  xyz: TXyz;
begin
  xyz := TXyz.Create;
  TXyzHack(xyz).ProtectedProc;
  xyz.Free;
end;
Вариант вызова в данном случае зависит от конкретной ситуации или еще кому как нравится.
    Попытка вызвать strict protected метод StrictProtectedProc таким же способом приведет к знакомой нам ошибке:
[dcc32 Error] TestXyz.dpr(16): E2362 Cannot access protected symbol TXyz.StrictProtectedProc
Но это случай тоже из разряда "если нельзя, но очень хочется, то можно". Немного усовершенствуем наш класс "пустышку" и переопределим в нем метод StrictProtectedProc из которого вызываем его родительскую реализацию:
type
  TXyzHackEx = class(TXyz)
  public
    procedure StrictProtectedProc;
  end;

procedure TXyzHackEx.StrictProtectedProc;
begin
  inherited;
end;
И спокойно вызовем strict protected метод:
procedure Test;
var
  xyz: TXyz;
begin
  xyz := TXyz.Create;
  TXyzHackEx(xyz).StrictProtectedProc;
  xyz.Free;
end;
    Таким образом модификаторы protected и strict protected легко обходятся и их нельзя назвать механизмом реализации инкапсуляции. По моему мнению, инкапсуляцию реализует только модификатор private, а точнее strict private.



В продолжение данной темы предлагаю ознакомиться с моей следующей статьей Доступ к private членам класса. В ней описаны другие способы, которые позволят получить доступ и к protected и к private членам класса.

7 комментариев:

  1. Не хочу Вас разочароывать, но если пользоваться грязными хаками типа

    TXyzHackEx(xyz).StrictProtectedProc;

    (это и есть нарушение OOP: обрашение к TXyz как к TXyzHackEx коим TXyz не является) то можно получить доступ и к приватныем полям и методам ...

    ОтветитьУдалить
  2. получить доступ к приватным полям и методам используя TXyzHack из статьи не получается :(

    ОтветитьУдалить
  3. @Max. Почему это грязный хак? Это использование того, что наследники класса видят protected методы. Так, что никакого хака, все на законных основаниях.

    @Анонимный. Завтра напишу, как вызвать приватный метод. Сегодня я уже спасть.

    ОтветитьУдалить
  4. Через новый rtti можно и к приватным получить доступ

    ОтветитьУдалить
  5. Приветный метод вызвать нельзя - он же приватный. Разработчики Delphi позаботились о том, чтобы в Delphi соблюдался принцип инкапсуляции

    ОтветитьУдалить
  6. Хм, мне казалось я объяснил почему это грязный хак, но видимо надо было объяснять подрбнее

    Возьмем например две перемменые AEdit: TEdit и AGrid: TStringGrid. Надеюсь никто не будет спорить что

    AGrid := TStringGrid(AEdit);

    это грязный, нарушающий ООП.

    После такого приведения по прежнему можно вызывать методы обшего предка (например методы TComponent) но вот что будет при вызове новых методов объявленных в TStrinGrid? В лучшем случае будет како-нибидь Access Violation, в худшем случае код отработает без ошибок, но он может повредить какие-нибудь данные и ваша программа может вылетать уже в других местах...

    PS Как сказал Бен Паркер "большая сила требует большой ответственности". Вот статью про большую силу вы написали, а вот про большую ответственность, которая ложиться на разработчика который пытается достучаться до приватных методов и данных указать забыли ...

    ОтветитьУдалить
  7. AGrid := TStringGrid(AEdit);
    это грязный, нарушающий ООП.

    Согласен. Но вы перегибаете. Я не призываю приводить всё ко всему и беспорядочно вызывать методы. Я показал, как вызвать метод родительского класса. Это не нарушение ООП.
    Кто же виноват, что разработчики бездумно прячут нужные методы/свойства от тех, кто работает с их классом? К тому же, этот трюк доступа к protected очень старый и я думаю, что все его знают и без меня.

    P.S. вот грязный метод:
    машина(велосипед).колесо.накачать - это грязный трюк, но работающий. Главное качать с умом, что бы камера не лопнула ;)

    ОтветитьУдалить