пятница, 13 июня 2014 г.

Расширенное делегирование: Протокол освобождения объекта и контроль ссылок

В этом коротком сообщении пойдёт речь о некоторых особенностях работы с агрегированными объектами.

Проблема

Пусть есть класс TClient, содержащий в списке своих полей среди прочего ссылку (свойство Server) на некоторый экземпляр класса TServer, существующий независимо от него.
Под независимостью понимается, что TClient не управляет жизненным циклом TServer. TServer может быть создан независимо от экземпляра TClient и, также независимо от него может быть освобождён.
Иногда в таких случаях говорят, что TClient и TServer состоят в отношении использования, в котором TClient использует TServer.
Связи между объектами могут быть и двунаправленными, например, TServer содержит в себе список экземпляров TClient, которые также могут существовать независимо.
В таких отношениях между объектами часто возникает проблема "провисших ссылок". Действительно, поскольку TServer может быть в любой момент освобождён, как экземпляр TClient "узнает" об этом? Излишне напоминать, что в описанном сценарии при попытке обращения к свойству Server в TClient произойдёт Access Violation.

Примеры ассоциативных связей

  • Потомки TDataSet и соответствующие им соединения - потомки TCustomConnection.
    При освобождении соединения экземпляры DataSet остаются, но информирование их об освобождении соединения требует дополнительных усилий в коде.
  • В аналогичных отношениях состоят TDataSource и потомки TDataSet.
    Освобождение DataSet должно приводить к деактивации TDataSource и связанных с ним элементов управления.
  • TDBEdit содержит ссылку на TDataSource и перекрывает виртуальный метод Notification для того, чтобы отслеживать освобождение DataSet.
  • Вообще, длинный список примеров можно получить, выполнив поиск строки ".Notification" среди исходных текстов, поставляемых вместе с Delphi, поскольку виртуальный метод Notification в TComponent предназначен, в частности, для обеспечения возможности отслеживать освобождение объекта, на который содержится ссылка. Т.е. если в перекрытии Notification отрабатывается операция opRemove и в качестве реакции устанавливается в nil некоторое поле данных - скорее всего, обрабатывается обсуждаемая ситуация.

ev_Terminate

ED предоставляет унифицированный способ получить управление до освобождения объекта-потомка TSBaseClass.
Для этого достаточно установить обработчик на событие ev_Terminate, рассылаемое после того, как принято решение освободить объект, но ещё до того, как управление будет передано в деструктор объекта.
Таким образом, на момент рассылки ev_Terminate объект, в контексте которого происходит это событие, полностью функционален - его разрушение ещё не началось.
Это соглашение даёт возможность как актуализировать ссылку на разрушаемый объект и выполнить соответствующие действия (вроде тех, что выполняются в Notification в случае Operation = opRemove), так освободить объект, содержащий ссылку на освобождаемый.
Например, вполне легален такой код:
ПоказатьСкрыть
constructor TSomeClass.Create(AOwner: TSBaseClass);
begin
  inherited Create;
  InstallEventHandler(AOwner, ev_Terminate, CloseSelf); // при освобождении AOwner будет вызван освобождён экземпляр TSomeClass
end;

TSafeRef: безопасная ссылка

Наличие ev_Terminate в протоколе освобождения экземпляров классов даёт возможность получить общее решение проблемы "провисших ссылок" - для отслеживания ассоциаций в Sonar.Core.BaseClass содержится класс TSafeRef, который можно параметризовать требуемым типом ссылки.
Определение TSafeRef:
ПоказатьСкрыть
type
  { TSafeRef: Контроль за доступностью агрегированного объекта }
  TSafeRef<T: TSBaseClass> = class(TSBaseClass)
  protected
    FInstance: T;
    FOwned: Boolean;
    function get_Instance: T;
    procedure set_Instance(AInstance: T);
    procedure hnd_CloseRef(var Event: TEvent); virtual;
    procedure SetObserve(Instance: T; TurnOn: Boolean);
  public
    property Instance: T read get_Instance write set_Instance;
    property InstanceRef: T read FInstance write set_Instance;
    property Owned: Boolean read FOwned write FOwned;
    constructor Create(AInstance: T; AOwned: Boolean = False); overload;
    destructor Destroy; override;
    function Assigned: Boolean;
    procedure Assign(AInstance: T; AOwned: Boolean = False);
  end;
Идея применения этого класса состоит в том, что вместо прямого обращения к объектной ссылке используется обращение к соответствующему свойству TSafeRef.
Например:
ПоказатьСкрыть
// прямое обращение к объекту по ссылке:
var
  dataSet_SETTINGS: TDataSet;
  ... ... ...
  dataSet_SETTINGS := AForm.DataSource.DataSet;
  ... ... ...
  dataSet_SETTINGS.First;
  ... ... ...

// обращение к объекту опосредованно SafeRef
// ВАЖНО! Это сработает только в случае, если TDataSet является потомком TSBaseClass.
var
  safeRef_SETTINGS: TSafeRef<TDataSet>;
  ... ... ...
  // AForm.DataSource.DataSet передаётся TSafeRef для отслеживания
  safeRef_SETTINGS := TSafeRef.Create(AForm.DataSource.DataSet);
  ... ... ...
  safeRef_SETTINGS.Instance.First;
  ... ... ...
Если с момента создания экземпляра TSafeRef и до момента обращения к его свойству Instance объект, отслеживаемый TSafeRef будет разрушен - SafeRef инициализирует ссылку на него и, при обращении к свойству Instance возбудит исключение ESafeRef_UsingNullInstance, а не Access Violation, как в случае непосредственного обращения к функциональности объекта по ссылке.
В конструктор класса передаётся объект, за которым следует наблюдать, и признак: следует ли этот объект освободить при освобождении экземпляра TSafeRef. В последнем случае уместного говорить уже о композиции.
Дополнительно, в TSafeRef есть булевская функция Assigned, возвращающая True, если ссылка на объект отлична от nil, и False в противном случае. Также есть свойство InstanceRef, как и Instance возвращающее ссылку на объект, но в отличие от использования Instance не приводящее к исключению, если ссылка равна nil.

Актуальность ссылочных полей объектов при агрегации

При агрегации для ссылок на объекты вполне можно использовать TSafeRef.
Тем не менее, из-за ограничений языка это не всегда удобно.
В частности, вот так написать не получится:
ПоказатьСкрыть
type
  TMyClass = class
  private
    FReference: TSafeRef<TDataSet>;
    ... ... ...
  public
    // следующую строку компилятор не пропустит
    property DataSet: TDataSet read FReference.Instance write FReference.Instance;
  ... ... ...
Если агрегированный объект должен быть представлен свойством, можно воспользоваться процедурой AssignObjectRef.
Определение AssignObjectRef:
ПоказатьСкрыть
{ Эта процедура предназначена для "наблюдения" за объектом Observed, ссылка на
  который содержится в поле данных, обозначенном здесь InstanceField, объекта
  Instance. В таком случае, вызов AssignObjectRef(Instance, Instance.FField,
  Observed) приведёт к следующему эффекту:
  * в Instance.FField будет занесена ссылка на Observed
  * в случае освобождения Observed в Instance.FField будет занесен nil }
procedure AssignObjectRef(Instance: TSBaseClass; var InstanceField; Observed: TSBaseClass);
Использование этой функции можно проиллюстрировать следующим примером.
Пример:
ПоказатьСкрыть
// В этом фрагменте кода подразумевается, что TDataSet является потомком TSBaseClass
type
  TMyClass = class
  private
    FDataSet: TDataSet;
    procedure set_DataSet(ADataSet: TDataSet);
    ... ... ...
  public
    property DataSet: TDataSet read FReference write set_Reference;
    constructor Create;
    ... ... ...
  end;
... ... ...
constructor TMyClass.Create;
begin
  ... ... ...
  { если DataSet пкркдавать в конструктор, то соответственно, его можно было бы передать в set_DataSet }
  set_DataSet(nil);
end;
... ... ...
procedure TMyClass.set_DataSet(ADataSet: TDataSet);
begin
  ... ... ...
  AssignObjectRef(Self, FDataSet, ADataSet);
  ... ... ...
end;
... ... ...
Вызов AssignObjectRef приводит к появлению обработчика, отслеживающего освобождение Observed (третий параметр AssignObjectRef, ADataSet в примере) и управляющего значением поля InstanceField (второй параметр, FDataSet - в примере). Обработчик запоминает адрес этого поля и установит его в nil в случае, если Observed освобождается. Разумеется, освобождение Instance (первый параметр AssignObjectRef, Self в примере) будет освобождён и обработчик.
Отслеживание объектов (Instance и Observed) производится обработкой события ev_Terminate, происходящего в их контексте.

Некоторые замечания относительно сказанного выше

Обозначенные способы контроля ссылок на агрегированные объекты весьма удобны и интенсивно используются. Но объекты, участвующие в агрегировании должны быть потомками TSBaseClass.
Это может показаться серьёзным ограничением общности, поскольку, например использованный для иллюстрации TDataSet изначально таковым не является, а для того, чтобы сделать его потомком TSBaseClass придётся вносить изменения в стандартные модули.
Вместе с тем, забегая вперёд хотелось бы отметить, что расширенное делегирование проектировалось так, чтобы ничему не противоречить и предъявлять минимальные требования к своему использованию.
Простейший и самый прямой путь добавить в VCL возможности, предоставляемые ED, это унаследовать от TSBaseClass класс TPersistent. Уже только это действие приведёт к возможности широкого применения ev_Terminate и безопасных ссылок для любых компонентов, а также возможность перехватывать сообщения, передаваемые в них посредством метода Dispatch, в обработчиках события ev_Dispatch.
Всё имеет свою цену, но вероятно, это тот самый случай, когда её стоит заплатить...

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

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