Создание экземпляра реализации интерфейса во время выполнения - PullRequest
10 голосов
/ 17 ноября 2011

Сначала небольшое объяснение моей ситуации:

У меня есть пример интерфейса, который реализован различными классами, и у этих классов не всегда может быть общий предок:

  IMyInterface = interface
    ['{1BD8F7E3-2C8B-4138-841B-28686708DA4D}']
    procedure DoSomething;
  end;

  TMyImpl = class(TInterfacedPersistent, IMyInterface)
    procedure DoSomething;
  end;

  TMyImp2 = class(TInterfacedObject, IMyInterface)
    procedure DoSomething;
  end;

У меня также есть метод фабрики, который должен создать экземпляр объекта, который реализует мой интерфейс. Мой фабричный метод получает имя класса в качестве параметра:

  function GetImplementation(const AClassName: string): IMyInterface;

Я попробовал два подхода для реализации этого фабричного метода, первый использовал расширенный RTTI:

var
  ctx : TRttiContext;
  t : TRttiInstanceType;
begin
  t := ctx.FindType(AClassName).AsInstance;
  if Assigned(t) then
    Result := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsInterface as IMyInterface;
end;

В этом подходе я вызываю конструктор по умолчанию, что хорошо в моем сценарии. Проблема в том, что во время выполнения я получаю сообщение об ошибке, говорящее, что объект не поддерживает IMyInterface. Более того, созданный объект не привязан к переменной интерфейса; следовательно, это будет утечка. Я также попытался вернуть значение с помощью метода TValue.AsType, но он дает мне нарушение прав доступа:

function GetImplementation(const AClassName: string): IMyInterface;
var
  ctx : TRttiContext;
  rt : TRttiInstanceType;
  V : TValue;
begin
  rt := ctx.FindType(AClassName).AsInstance;
  if Assigned(rt) then
  begin
    V := rt.GetMethod('Create').Invoke(rt.MetaclassType, []);
    Result := V.AsType<IMyInterface>;
  end;
end;

.

Второй подход, который я попробовал, состоял в том, чтобы использовать общий словарь для хранения пар и обеспечения регистрации, методов отмены регистрации:

  TRepository = class
  private
    FDictionary : TDictionary<string, TClass>;
  public
    constructor Create;
    destructor Destroy; override;
    function GetImplementation(const AClassName: string): IMyInterface;
    procedure RegisterClass(AClass: TClass);
    procedure UnregisterClass(AClass: TClass);
  end;

Здесь я реализовал метод GetImplementation так:

function TRepository.GetImplementation(const AClassName: string): IMyInterface;
var
  Obj : TObject;
begin
  if FDictionary.ContainsKey(AClassName) then
  begin
    Obj := FDictionary[AClassName].Create;
    Obj.GetInterface(IMyInterface, Result);
  end;
end;

Это работает нормально, и я могу вызвать метод DoSomething, используя возвращенное значение GetImplementation, но у него все еще есть проблема утечки памяти; Объект, созданный здесь, не привязан ни к какой переменной интерфейса; следовательно, он не подсчитывается, а протекает.

.

Теперь мой актуальный вопрос:

Итак, мой вопрос: как я могу безопасно создать экземпляр класса, который реализует мой интерфейс во время выполнения? Я видел Delphi Spring Framework, и он предоставляет такую ​​функциональность в своем модуле Spring.Services, но у него есть свои собственные процедуры отражения и модели управления временем жизни. Я ищу легкое решение, а не целый сторонний фреймворк, чтобы сделать это для меня.

Привет

Ответы [ 3 ]

11 голосов
/ 17 ноября 2011

Первый случай использования RTTI дает вам нарушение прав доступа, поскольку TRttiContext.FindType(AClassName) не может найти информацию Rtti для классов, которые не зарегистрированы или не используются в приложении.

Таким образом, вы можете изменить свой код на

function GetImplementation(AClass: TClass): IMyInterface;
var
  ctx : TRttiContext;
  t : TRttiInstanceType;
begin
  t := ctx.GetType(AClass).AsInstance;
  if Assigned(t) then
    Result := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsInterface As IMyInterface;
end;

и вызвать таким образом

AClass:=GetImplementation(TMyImp2);

Теперь, если вы хотите использовать имя класса для вызова класса,использование списка (например, вашего класса TRepository) для регистрации классов является допустимым подходом.Что касается утечки памяти, я почти уверен, что это вызвано тем, что класс TMyImpl является производным от TInterfacedPersistent, который не реализует подсчет ссылок напрямую, как TInterfacedObject.

Эта реализация TRepository Должно работать нормально.

constructor TRepository.Create;
begin
  FDictionary:=TDictionary<string,TClass>.Create;
end;

destructor TRepository.Destroy;
begin
  FDictionary.Free;
  inherited;
end;

function TRepository.GetImplementation(const AClassName: string): IMyInterface;
var
  Obj : TObject;
begin
  if FDictionary.ContainsKey(AClassName) then
  begin
    Obj := FDictionary[AClassName].Create;
    Obj.GetInterface(IMyInterface, Result);
  end;
end;

{
or using the RTTI
var
  ctx : TRttiContext;
  t : TRttiInstanceType;
begin
  t := ctx.GetType(FDictionary[AClassName]).AsInstance;
  if Assigned(t) then
    Result := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsInterface As IMyInterface;
end;
}

procedure TRepository.RegisterClass(AClass: TClass);
begin
  FDictionary.Add(AClass.ClassName,AClass);
end;

procedure TRepository.UnregisterClass(AClass: TClass);
begin
  FDictionary.Remove(AClass.ClassName);
end;
4 голосов
/ 17 ноября 2011

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

Но в обоих предложенных вами вариантах вы заявляете, что

объект, который здесь создается, не назначен ни одной интерфейсной переменной

Это просто неправда. В обоих случаях вы присваиваете Result, который имеет тип IMyInterface. Если у вас есть утечка памяти, она вызвана каким-то другим кодом, а не этим кодом.

И @RRUZ обнаружил причину утечки, а именно - с помощью TInterfacedPersistent, которая не реализует управление временем жизни с подсчетом ссылок. Ваш код не будет течь для TInterfacedObject.

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

if FDictionary.TryGetValue(AClassName, MyClass) then
  Result := MyClass.Create as IMyInterface;
2 голосов
/ 17 ноября 2011

Вы можете сделать это, используя расширенный RTTI и метод GetInterface TObject:

function GetImplementation(const AClassName: string): IMyInterface;
var
  ctx: TRttiContext;
  t : TRttiInstanceType;
  obj: TObject;
begin
  Result := nil;
  t := ctx.FindType(AClassName).AsInstance;
  if Assigned(t) then begin
    obj := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsObject;
    obj.GetInterface(IMyInterface, Result)
  end;
end;

Не будет работать, если объект переопределяет QueryInterface для пользовательской обработки, но оба TInterfacedPersistent и TInterfacedObject полагаются на GetInterface.

...