Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the best practice for using an object implementing two interfaces?

Tags:

delphi

This may be an extension of my previous question.

I have understood that an interface-based variable can not be defined as its original type, otherwise the reference count does not work properly for automatic release.

But if a class implements two interfaces, then what type should be defined when make an instance of it?

Consider following code:

program Project2;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  SysUtils, Classes;

type
  ITestInterface = interface(IInvokable)
    ['{A7BDD122-7DC6-4F23-93A2-B686571AB2C8}']
    procedure TestMethod;
  end;

  IAnotherInterface = interface(IInvokable)
    ['{15FEC4A7-E361-41D0-9D52-170AFAD1794B}']
    procedure AnotherMethod;
  end;

  TTestObj = class(TInterfacedObject, ITestInterface, IAnotherInterface)
    constructor Create;
    destructor Destroy; override;
  private
    FData: TStrings;
  public
    procedure TestMethod;
    procedure AnotherMethod;
  end;

{ TTestObj }

constructor TTestObj.Create;
begin
  FData := TStringList.Create;
end;

destructor TTestObj.Destroy;
begin
  Writeln('Destroy');
  FData.Free;

  inherited;
end;

procedure TTestObj.TestMethod;
begin
  FData.Text := 'TestMethod';
  Writeln(FData.Strings[0]);
end;

procedure TTestObj.AnotherMethod;
begin
  FData.Text := 'AnotherMethod';
  Writeln(FData.Strings[0]);
end;

{ Main }

function CreateObj: TTestObj;
begin
  Result := TTestObj.Create;
end;

function CreateObj_i1: ITestInterface;
begin
  Result := TTestObj.Create;
end;

function CreateObj_i2: IAnotherInterface;
begin
  Result := TTestObj.Create;
end;

procedure Main;
var
  TestObj: ITestInterface;  // It must be declared as an interface type, or it won't be freed correctly.
  AnotherObj: IAnotherInterface;
  NaturalObj: TTestObj;
begin
  { 1st way: The syntax is a bit natural, but easily lead to memory leaks. }
  CreateObj;                // memory leak !
  TestObj := CreateObj;
  TestObj.TestMethod;
  AnotherObj := CreateObj;
  AnotherObj.AnotherMethod;

  TestObj := nil;
  AnotherObj := nil;
  Writeln('----------');

  { 2nd way: The syntax is a bit messy, you should do type conversion carefully. }
  CreateObj_i1;             // object freed correctly.
  TestObj := TTestObj(CreateObj_i2);    // Using ITestInterface(CreateObj_i2) is wrong.
  TestObj.TestMethod;
  AnotherObj := TTestObj(CreateObj_i1); // Using IAnotherInterface(CreateObj_i1) is wrong.
  AnotherObj.AnotherMethod;

  TestObj := nil;           // useless, it won't be be freed until the procedure returns.
  AnotherObj := nil;        // as above.
  Writeln('----------');

  { 3rd way: The syntax is a bit natural, but it's easily lead to access violation if pass the `NaturalObj` out of the procedure. }
  NaturalObj := TTestObj(CreateObj_i1); // Using TTestObj(CreateObj_i2) is okay too.
  NaturalObj.TestMethod;
  NaturalObj.AnotherMethod;
end;

begin
  Writeln('Program start!');
  Main;
  Writeln('Program end.');
  Readln;
end.

So which way is your preferred? Or any other advice? Thanks in advance.

like image 317
DDGG Avatar asked Feb 15 '26 22:02

DDGG


1 Answers

There is a lot of confusion and complexity here. Rather than trying to dissect what you have, I'll show you how I would do it.

First of all remove all variables of type TTestObj. You should be using interface references only. You'll want a variable for each one.

var
  TestIntf: ITestInterface; 
  AnotherIntf: IAnotherInterface;

Note that I have changed the name of these variables, replacing the Obj suffix with Intf. This reflects that they are interface references rather than object references.

Then you can simply do this:

TestIntf := TTestObj.Create;
AnotherIntf := TestIntf as IAnotherInterface;

Now you have two interface variables, one for each of your interfaces. It so happens that the implementing object behind both of these references is the same object, which is presumably what you want.

You could equally have reversed the logic:

AnotherIntf := TTestObj.Create;
TestIntf := AnotherIntf as ITestInterface;

This achieves exactly the same effect, you can do it either way.

If you want a different instance behind the variables then that is easy enough:

TestIntf := TTestObj.Create;
AnotherIntf := TTestObj.Create;

The key points here are:

  1. Don't mix interfaces and objects. Once you start using an interface don't access the implementing object behind it.
  2. When an object implements multiple interfaces, use the as operator to obtain the other interfaces.
like image 57
David Heffernan Avatar answered Feb 18 '26 21:02

David Heffernan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!