-1

I know this is old and I am using Delphi 5. Not sure if it is the same in later version of Delphi.

I found that if an object delegate its interface implementation to another object, the original object canonot call the interfaced method directly, which I expect what delegation interface implementation should be.

Here is my question

IFoo = interface
  procedure InterfaceMethod;
end;

TBar = class(TComponent, IFoo)
  FFoo: IFoo;
  procedure ObjectMethod;
  property Foo: IFoo read FFoo implements IFoo;
end;

TBar.Method2
begin
  InterfaceMethod;  // this will give compile error, Method1 not declared!
  (Self as IFoo).InterfaceMethod;  // compile error, saying operation not supported.
  FFoo.InterfaceMethod;  // this work, but meaningless, since this can be any object, why the need of interface!
end;

Similar if I do this outside of the TBar class. Say

Bar := TBar.Create;
Bar.InterfaceMethod;  // compile error
(Bar as IFoo).InterfaceMethod;  // compile error
Bar.Foo.Method1;  // this work, but...

Can anyone explain why is it this way or my understand is not correct. Or may be this will be correct after D5?

The following are sample code

unit iwSqlDbEngine;
//
// Database resource and helper funcions.
//
interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Db, ScktComp, ExtCtrls;

type

  ISqlMaker = interface
    ['{5A671E7B-957B-4A52-BB5E-87EEA8E5687B}']
    procedure SetSqlStatement(const value: string);
    function GetSqlStatement: string;
    // Insert and update
    function Insert(table: string): ISqlMaker;
    function Update(table: string): ISqlMaker;
    function AddField(fieldname: string; value: variant; isExpr: Boolean = False): ISqlMaker; overload;
    function AddField(field: TField): ISqlMaker; overload;
    // Delete
    function Delete(table: string): ISqlMaker;
    // Select
    function Select(table: string; columns: array of string): ISqlMaker;
    function Join(table: string; columns, matches: array of string): ISqlMaker;
    function LeftJoin(table: string; columns, matches: array of string): ISqlMaker;
    // Other clauses
    function Where(aExpr: string): ISqlMaker;
    function GroupBy(columns: array of string): ISqlMaker;
    function OrderBy(columns: array of string): ISqlMaker;
    function Having(vExprs: array of string): ISqlMaker;
    property SqlStatement: string read GetSqlStatement write SetSqlStatement;
  end;

  TSampleDbEngine = class(TComponent, ISqlMaker)
  private
    FSqlMaker: ISqlMaker;
  public
    procedure Execute;
    property SqlMaker: ISqlMaker read FSqlMaker write FSqlMaker implements ISqlMaker;
  end;

implementation

procedure TSampleDbEngine.Execute;
begin
  (Self as ISqlMaker).GetSqlStatement;
end;

end.

Same compile error as described above.

Gerald Me
  • 107
  • 1
  • 2
  • 8
  • Can you provide a [mcve] instead of this pseudo code – David Heffernan Apr 04 '17 at 15:34
  • The above are the minimal code which should able to compile. Sorry, Method1 refer to InterfaceMethod and Method2 is ObejctMethod. I use Method1 and Method2 in the first draft by think that name change is more clear. TBar.Method2 should be procedure TBar.ObjectMethod; – Gerald Me Apr 04 '17 at 18:23
  • No it is not. We don't know the types of variables and much of the syntax is made up. Please try harder. The question will get a good answer if it is fixed. – David Heffernan Apr 04 '17 at 18:27
  • Your sample code makes it worse. Making it clear that the original code was fake. Please follow the link in my first comment and read very carefully. A good [MCVE] would make a huge difference. – David Heffernan Apr 04 '17 at 18:53

2 Answers2

3

You get error "InterfaceMethod not declared" because your Bar variable is of type TBar, not IFoo and since the TBar doesn't have InterfaceMethod the compiler is right. Inside the TBar.Method2 the Self is of type TBar so thats why it doesn't work there too.

The idea to cast as IFoo is right, but it fails as the as operator requires that the interface have GUID which you'r IFoo doesn't have. Put caret right after IFoo = interface and hit Ctrl+G to generate a GUID for the interface.

I don't have access to Delphi 5, but I just tested with Delphi 7 which should be quite similar to the D5 with following code

  IFoo = interface
  ['{B6AFF17B-C239-414D-84DE-F8F2AE1FE4E0}']
    procedure Bar;
  end;

  TFoo = class(TComponent, IFoo)
  private
    FFoo: IFoo;
  public
    property Foo: IFoo read FFoo implements IFoo;
  end;

and now "outside the class" both

var f: TFoo;
begin
  f := TFoo.Create(nil);
  (f as IFoo).Bar;
end;

and

var f: IFoo;
begin
  f := TFoo.Create(nil);
  f.Bar;
end;

compile just fine.

ain
  • 22,394
  • 3
  • 54
  • 74
  • I just try by giving GUID to the interface and the results are the same. – Gerald Me Apr 04 '17 at 18:20
  • [Error] iwSqlDbEngine.pas(198): Operator not applicable to this operand type – Gerald Me Apr 04 '17 at 19:24
  • By the way, if I define a variable say x: IFoo, then x := Self, then use x to call the method. It can compile. But this does not work if outside of the class. That is I cannot declare an Interface variable and assign an object or type cast an object in assignment. Do I need to use QueryInterface? – Gerald Me Apr 04 '17 at 19:30
  • When outside the class, did you f := TBar.Create(nil)? Anyway, it does not work in D5. Thanks for confirming. I think it was an issue in D5. – Gerald Me Apr 05 '17 at 04:22
3

You get error InterfaceMethod not declared because in the context of a TBar method, the default scope (self) is a TBar, not IFoo.

If TBar implemented IFoo directly then the TBar class would (normally) have the method you are trying to call as well. i.e. whether you have a TBar reference or an IFoo reference, both would support that method. An exception to this might be if you had an interface method resolution clause in place on TBar to implement a particular interface method with some method of another name in the implementing class.

There might also be differences outside of the scope of TBar if the implementation of an interface method did not have public scope. All interface methods are public so if the implementation of a particular method is private or protected, then a TBar reference could not be used to call that interface method, only an IFoo, for example.

But in your case, the entire interface is delegated and so the methods simply do not exist on the TBar class itself.

You should be able to type-cast self to the required interface type, but there is no point in incurring the QueryInterface, AddRef/Release calls and other scaffolding required, only to make that possible.

You already have a reference to the interface you need. You can simply call it directly:

procedure TBar.ObjectMethod
begin
  fFoo.InterfaceMethod;  // fFoo holds the delegated interface so if the TBar
                         //  needs to call the delegated interface, just call it!
end;

The same applies in the case of your TSampleDbEngine which similarly already has the required reference to the delegated interface:

procedure TSampleDbEngine.Execute;
begin
  fSqlMaker.GetSqlStatement;
end;

In the comments below you describe that your real concern is with providing a consistent interface [sic] to your implementation, without the user having to worry about whether they have a reference to the object or to an interface that the object implements.

This I think suggests a misunderstanding of what an interface represents.

If code has only an object reference then it can only call methods defined for that type of object.

o: TFoo;

o.ObjectMethod;    // This is OK, o is a TFoo reference
o.Bar;             // This is NOT OK.  o is NOT an IFoo reference

The confusion can arise if TFoo directly and publicly implements the Bar method required by the IFoo interface:

TFoo = class(TObject, IFoo)
public
  procedure Bar;
end;

o: TFoo;

o.Bar;   // This is fine, but o is still a TFoo. 
         //  IFoo is completely irrelevant here,
         //  it may as well not exist.

This can make it appear that you are using the IFoo interface of TFoo. But you are not.

It is simply that the method that satisfies that interface also happens to be publicly accessible through an object reference to the implementation object. This can be illustrated by the fact that if that method is made private, the interface is still satisfied and can be used via an interface reference, but is no longer accessible via an object reference:

TFoo = class(TObject, IFoo)
private
  procedure Bar;
end;

o: TFoo;
i: IFoo;

i := o as IFoo;
i.Bar;   // This is fine.  Interface methods are always public
o.Bar;   // Once again, this will NOT compile because TFoo.Bar is now private

Obviously with an interface reference the consumer of the reference does not know nor care whether that interface has been directly implemented by an object or has been delegated.

Note that this is no different than the situation where an object might implement TWO (or more) different interfaces. You cannot call methods of one interface using a reference to another.

You must obtain the correct type of reference in order to use a reference of that type.

So your real problem is that your implementation allows a user to obtain references of the wrong type from the start.

Presumably you have a constructor which of course yields an object reference, but there is then an assumption that a consumer of that object should convert that to an interface reference, but this assumption is not enforced (or enforcable, as implemented).

You can help enforce that by not providing a public constructor, but instead providing a factory method that only yields an interface reference and throwing an exception if someone incorrectly attempts to instantiate the class directly:

type
  IFoo = interface
    procedure Bar;
  end;

  TFoo = class(TInterfacedObject, IFoo)
  private
    procedure Bar;
    constructor InternalCreate;
  public
    class function NewFoo: IFoo;
    constructor Create;
  end;


procedure TFoo.Bar;
begin

end;


constructor TFoo.InternalCreate;
begin
  inherited Create;
end;


constructor TFoo.Create;
begin
  raise ENotSupportedException.Create('Do not create instances of TFoo.  Use the factory method(s) provided');
end;


class function TFoo.NewFoo: IFoo;
begin
  result := TFoo.InternalCreate;  // We can call our own private, internal constructor
end;

Now the consumers of your class are only able to obtain an interface reference to the object that implements the interface and so cannot get into a sticky situation where they are confused by the methods on the class versus the methods on the interface and any differences that might exist due to method visibility, interface delegation or anything else that they really shouldn't be concerned with in the first place.

In general terms: Do not try - and do not encourage consumers of your code to try - to mix interfaces and object references to the same object.

Quite apart from these problems, there are other more serious issues that you will likely run into around the lack of reference counting on the object references.

Deltics
  • 22,162
  • 2
  • 42
  • 70
  • Thanks, I do understand I can use the interface property directly in the class instead of type case Self. My problem is actually in outside the class when create an Object with interface delegation implementation. I have to call the Interface methods via the delegated property. From the point of object programming, I want to hide the implementation from the user. So if a user use an Object interface method, the user should not need to know if interface is implemented by the Class itself or is delegated. The code to call an Object interfaced methods should be the same. – Gerald Me Apr 05 '17 at 04:35
  • (Sorry, ignore the above, can't edit it.) I do understand I can call interface methods via the delegated property. But my question is this what interface designed for? The idea of interface is to hide the implementation from the user. So if a user use an Object interface method, the user should not need to know if the interface is implemented by the Class itself or is delegated. The code to call an Object interfaced methods should be the same. – Gerald Me Apr 05 '17 at 04:44
  • And you can, if your reference to the implementing object is an interface reference. If you only hold a reference to the object as a class then of course you can only call methods on that class, just as if you hold a reference to one interface you cannot call methods on other interfaces that the underlying implementing object may implement. If you don't want the consumer of an interface to use the class reference, don't allow them to get one. Provide a factory method that only yields an interface reference. – Deltics Apr 05 '17 at 04:46
  • Thanks. But my understand of adding interface to an object is to enhance the ability of the object. Otherwise, we may define two different classes. And put the object of the second class into the frist class. Just like delegation but without all the need of Interface definition. I also aware of the reference couting issue. but I kind of agree to an article suggest not to use reference counting at all. – Gerald Me Apr 06 '17 at 08:29
  • Buy many thans to Deltics. To the answer of my question, ain below already confirm it is an issue of D5. So my question is answered. But to the concept of using Interface and if the Delphi implementation of interface is useful to me, this should be another question, I think. – Gerald Me Apr 06 '17 at 08:46