Pacific Blue Software Logo

Interface Delegation in Delphi

Delegating an Interface to a Property (the implements keyword)

Delphi lets a class satisfy an interface without writing the interface's methods itself. Put the implements keyword on a property, and the interface is served by that property's value instead. It is composition instead of inheritance: the real logic lives in a separate object the class merely holds a reference to.


The Three Pieces


Complete Example

type
  { The contract }
  IWorker = interface
    ['{A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}']
    procedure Do_Work;
  end;

  { The class that actually does the work }
  TWorker_Impl = class (TInterfacedObject, IWorker)
  public
    procedure Do_Work;
  end;

  { The host - claims IWorker, delegates to the Worker property }
  TCompany = class (TInterfacedObject, IWorker)
  private
    FWorker : IWorker;
  public
    property Worker : IWorker read FWorker write FWorker implements IWorker;
  end;

procedure TWorker_Impl.Do_Work;
begin
  Writeln ('The worker is doing the actual labor!');
end;

Using it:

var
  LCompany : TCompany;
  LWorker  : IWorker;
begin
  LCompany        := TCompany.Create;
  LCompany.Worker := TWorker_Impl.Create;

  { Do_Work is not a method of TCompany - it is reached through the IWorker
    that TCompany hands out on behalf of its Worker property }
  if LCompany.GetInterface (IWorker, LWorker)
  then LWorker.Do_Work;
  LWorker := nil;

  LCompany.Free;
end;

What Is Happening Under the Hood

Adding implements IWorker to the property tells the compiler that TCompany does not implement IWorker's methods itself. The IWorker entry in TCompany's interface table points at the property instead. When IWorker is requested from TCompany, the property is read and the interface it holds - the worker's own IWorker - is handed back. From then on calls go straight to the worker; TCompany is not involved. The effect is the same as a hand-written pass-through:

procedure TCompany.Do_Work;
begin
  FWorker.Do_Work;
end;

with one important difference: the IWorker you receive belongs to the worker, not to TCompany - see Identity below.


Key Technical Rules

Type Match

The property must be of the interface type being delegated, or of a class type that implements that interface's methods. Both of these are valid:

property Worker : IWorker      read FWorker      write FWorker      implements IWorker;
property Worker : TWorker_Impl read FWorker_Impl write FWorker_Impl implements IWorker;

Reference Counting

With an interface-typed property, Delphi's automatic reference counting applies to the delegate. Assigning to LCompany.Worker increments the new value's reference count and releases whatever the property held before.

A class-typed property gets no reference counting - the field is a plain object reference. If that class descends from TInterfacedObject, its count is driven only by the interface references handed out through the host; when the last one is released the delegate destroys itself while the host still holds the field, leaving a dangling reference. A class-type delegate should not count its own references: descend it from TAggregatedObject or TContainedObject, and have the host create and free it.

Identity

Because the IWorker handed out by an interface-typed property is the worker's own, its QueryInterface, _AddRef and _Release are the worker's too. Asking that IWorker for any other interface TCompany supports fails, and the reference counting lands on the worker, not on TCompany. When the delegate must behave as part of the host, use a class-typed property and descend the delegate from TAggregatedObject, which forwards QueryInterface and reference counting to the host, or TContainedObject, which forwards reference counting only.

Overriding

Mixing host and delegate methods works only with a class-typed property. The delegate class and its ancestors are searched first; any method it does not provide is then taken from the host. A method on the host fills a gap - it does not take precedence over one the delegate already provides.

With an interface-typed property, as in the example above, the whole interface comes from the property; a Do_Work declared on TCompany is not used for calls made through IWorker. To run code before or after the worker, drop implements and write the pass-through method by hand.

Properties, Not Fields

implements is a property specifier - it goes only on a property declaration (after the read specifier; a read specifier is required). It cannot be attached directly to a field declaration.


When Should You Use This?


Combining Creation and Assignment

The two lines (with LWorker declared as IWorker)

LWorker := TWorker_Impl.Create;
LCompany.Worker := LWorker;

can be written as one:

LCompany.Worker := TWorker_Impl.Create;

TWorker_Impl.Create returns a class instance; because TWorker_Impl implements IWorker, the compiler implicitly converts it to an interface reference for the assignment.

Lifetime Management

Because TWorker_Impl descends from TInterfacedObject, it is reference counted. The moment it's assigned to LCompany.Worker its count becomes 1; TCompany effectively owns it. Assign a different worker (or nil) later, or free TCompany itself, and the count drops to zero - the worker is destroyed automatically. Don't hold it in a separate class-typed variable and don't call .Free on it yourself.

{ No try/finally needed for the worker - it cleans itself up when
  LCompany is freed or LCompany.Worker is reassigned }
LCompany.Worker := TWorker_Impl.Create;

Critical warning: this only works safely if the implementation class is reference counted - descended from TInterfacedObject, or implementing counting in its own _AddRef and _Release. A class declared directly against TObject won't compile, as it lacks QueryInterface, _AddRef and _Release; one that implements them without counting (TNoRefCountObject, for example) compiles, but nothing ever frees it - the object leaks.

LCompany in the examples above is held in a plain class-typed variable, not an interface variable, so it still needs an explicit LCompany.Free; only the delegate it holds is reference counted. That is safe here because every IWorker taken from TCompany is the worker's own, so TCompany's count is never touched. If anything obtains an interface on TCompany itself, such as an IInterface reference, its count rises to 1 and falls back to 0, TCompany destroys itself, and the later LCompany.Free frees it a second time.


Requirements


Download Source Code

PBS Delphi - Interface Delegation with implements


Back to Articles for Developers
Back to Articles on Delphi
PBS Delphi Logger - Thread-safe logging with multiple outputs
Input Sanitization and Validation for Delphi

If you found this useful, then please consider making a donation.

paypal
QR Code for donation Please donate if helpful