Delphi interface implements

Gryffe picture Gryffe · Apr 25, 2013 · Viewed 10.5k times · Source

I would expect that the reference counting should work on the outer aggregating object in an interface implementation. If I can refer to another example: Clarity in classes implementing multiple interfaces (alternative to delegation):

Here is a minimal reproduction of the behaviour:

program SO16210993;

{$APPTYPE CONSOLE}

type
  IFoo = interface
    procedure Foo;
  end;

  TFooImpl = class(TInterfacedObject, IFoo)
    procedure Foo;
  end;

  TContainer = class(TInterfacedObject, IFoo)
  private
    FFoo: IFoo;
  public
    constructor Create;
    destructor Destroy; override;
    property Foo: IFoo read FFoo implements IFoo;
  end;

procedure TFooImpl.Foo;
begin
  Writeln('TFooImpl.Foo called');
end;

constructor TContainer.Create;
begin
  inherited;
  FFoo := TFooImpl.Create;
end;

destructor TContainer.Destroy;
begin
  Writeln('TContainer.Destroy called');//this line never runs
  inherited;
end;

procedure Main;
var
  Foo : IFoo;
begin
  Foo := TContainer.Create;
  Foo.Foo;
end;

begin
  Main;
  Readln;
end.

If instead of using implements, I implement the interface in the TImplementor class then the destructor runs.

Answer

David Heffernan picture David Heffernan · Apr 25, 2013

What is happening here is that you call TContainer.Create and create an instance to an object. But you then assign that instance to an interface reference, the global variable Foo. Because that variable is of type IFoo, the interface delegation means that the implementing object is the instance of TFooImpl and not the instance of TContainer.

Hence nothing ever takes a reference to the instance of TContainer, its reference count is never increased, and so it is never destroyed.

I don't think there's a very easy way around this. You may be able to use TAggregatedObject but it may not solve your problem. It would force you to declare TContainer.FFoo to be of type TFooImpl which I imagine you do not want to do. Anyhow, here's what it looks like re-cast that way:

program SO16210993_TAggregatedObject;

{$APPTYPE CONSOLE}

type
  IFoo = interface
    procedure Foo;
  end;

  TFooImpl = class(TAggregatedObject, IFoo)
    procedure Foo;
  end;

  TContainer = class(TInterfacedObject, IFoo)
  private
    FFoo: TFooImpl;
    function GetFoo: IFoo;
  public
    destructor Destroy; override;
    property Foo: IFoo read GetFoo implements IFoo;
  end;

procedure TFooImpl.Foo;
begin
  Writeln('TFooImpl.Foo called');
end;

destructor TContainer.Destroy;
begin
  Writeln('TContainer.Destroy called');//this line does run
  FFoo.Free;
  inherited;
end;

function TContainer.GetFoo: IFoo;
begin
  if not Assigned(FFoo) then
    FFoo := TFooImpl.Create(Self);
  Result := FFoo;
end;

procedure Main;
var
  Foo : IFoo;
begin
  Foo := TContainer.Create;
  Foo.Foo;
end;

begin
  Main;
  Readln;
end.

The documentation does talk about this:

The class you use to implement the delegated interface should derive from TAggregationObject.

Initially I could not find any documentation for this TAggregationObject. And finally I realised that it's actually named TAggregatedObject and is documented.

TAggregatedObject provides the functionality for an inner object of an aggregate by implementing the IInterface methods to delegate to the controlling IInterface.

An aggregated object is an object composed of several interfaced objects. Each object implements its own behavior and interfaces, but all the objects share the same reference count, which is that of the controller object. In the container pattern, the controller is the container object.

TAggregatedObject does not itself support any interfaces. However, as is typical of an aggregate, it does implement the methods of IInterface, which are used by the objects that descend from it. TAggregatedObject, therefore, serves as a base for classes that implement interfaces for creating objects that are part of an aggregate.

TAggregatedObject is used as a base for classes that create contained objects and connecting objects. Using TAggregatedObject ensures that calls to the IInterface methods delegate to the controlling IInterface of the aggregate.

The controlling IInterface is specified in the constructor for TAggregatedObject and is indicated by the Controller property.

In addition there is this from the source code comments:

TAggregatedObject and TContainedObject are suitable base classes for interfaced objects intended to be aggregated or contained in an outer controlling object. When using the "implements" syntax on an interface property in an outer object class declaration, use these types to implement the inner object.

Interfaces implemented by aggregated objects on behalf of the controller should not be distinguishable from other interfaces provided by the controller. Aggregated objects must not maintain their own reference count - they must have the same lifetime as their controller. To achieve this, aggregated objects reflect the reference count methods to the controller.

TAggregatedObject simply reflects QueryInterface calls to its controller. From such an aggregated object, one can obtain any interface that the controller supports, and only interfaces that the controller supports. This is useful for implementing a controller class that uses one or more internal objects to implement the interfaces declared on the controller class. Aggregation promotes implementation sharing across the object hierarchy.

TAggregatedObject is what most aggregate objects should inherit from, especially when used in conjunction with the "implements" syntax.