Recent

Author Topic: Interface method and derived types  (Read 4549 times)

dubst3pp4

  • Jr. Member
  • **
  • Posts: 86
  • Retro computing ~ GNU/Linux
    • me on Mastodon
Interface method and derived types
« on: December 07, 2017, 03:50:57 pm »
Hello,

I've a question about whether the following is considered as good style or not:

I'm defining an Interface with just one function:

Code: Pascal  [Select][+][-]
  1. ICommand = interface ['{CFEA9748-7530-49E7-A1FF-91251101B458}']
  2.   function Execute: TObject;
  3. end;

Now there are several Classes that implement the interface, but return each a different, derived type of TObject, for example:

Code: Pascal  [Select][+][-]
  1. TMyCommand = class(TInterfacedObject, ICommand)
  2. public
  3.   function Execute: TObject; virtual;
  4. end;
  5.  
  6. implementation
  7.  
  8. function TMyCommand.Execute: TObject;
  9.   Result := TMyObject.Create;
  10. end;

As TMyObject is derived from TObject, this should work. But from the interface part of the unit it is not visible for the developer, that he can expect a TMyObject instance instead of a TObject instance as the result from the function. Another downside is, that the user has to cast the instance back to TMyObject, when he wants to use it.

Any ideas what would be a better design?

Best regards,
Marc
Jabber: xmpp:marc.hanisch@member.fsf.org -- Support the Free Software Foundation: https://my.fsf.org/donate

Thaddy

  • Hero Member
  • *****
  • Posts: 14201
  • Probably until I exterminate Putin.
Re: Interface method and derived types
« Reply #1 on: December 07, 2017, 04:40:29 pm »
As long as you stick to the rules your code looks fine and should be used like this:
Code: Pascal  [Select][+][-]
  1. var
  2.   testme:ICommand;  //NOT TMyObject!!
  3.  Rslt:TObject;
  4. begin
  5.   testme := TMyCommand.Create as ICommand;
  6.   Rslt := testme.Execute;
  7.   Rslt.free // Is TObject, not interface....
  8. end.  
interfaces are refcounted, no need to free them provided instantiated as interface

If you want not TObject, but ICommand, let your function return a instatiated object as ICommand;
Code: Pascal  [Select][+][-]
  1. TMyCommand = class(TInterfacedObject, ICommand)
  2. public
  3.   function Execute: ICommand; virtual;
  4. end;
Then you can have:
Code: Pascal  [Select][+][-]
  1. var
  2.   testme, Rslt:ICommand;  //NOT TMyObject!!
  3. begin
  4.   testme := TMyCommand.Create as ICommand;
  5.   Rslt := testme.Execute;
  6.   Rslt := Rslt.execute;  // Rslt can now be be a totally different object that supports ICommand and so on and so on...
  7. end.  
Now your function returns an instantiated object (any) that supports ICommand.
You can now use is and as to determine which version of a class that supports icommand is called.

« Last Edit: December 07, 2017, 05:06:59 pm by Thaddy »
Specialize a type, not a var.

Thaddy

  • Hero Member
  • *****
  • Posts: 14201
  • Probably until I exterminate Putin.
Re: Interface method and derived types
« Reply #2 on: December 07, 2017, 05:18:02 pm »
A full example looks like this:
Code: Pascal  [Select][+][-]
  1. program untitled;
  2. {$ifdef fpc}{$mode delphi}{$H+}{$I-}{$endif}
  3. type
  4.   ICommand = interface ['{CFEA9748-7530-49E7-A1FF-91251101B458}']
  5.      function Execute: ICommand;
  6.   end;
  7.  
  8.   TMyCommand = class(TInterfacedObject, ICommand)
  9.   public
  10.     function Execute: ICommand; virtual;
  11.   end;
  12.  
  13.   TMyCommand2 = class(TInterfacedObject, ICommand)
  14.   public
  15.     function Execute: ICommand; virtual;
  16.   end;
  17.  
  18.   function TMyCommand.Execute: ICommand;
  19.   begin
  20.     Result := TMyCommand2.Create as ICommand;
  21.   end;
  22.  
  23.   function TMyCommand2.Execute: ICommand;
  24.   begin
  25.     Result := TMyCommand2.Create as ICommand;
  26.   end;
  27.  
  28. var a,b:ICommand;  
  29. begin
  30.   a:= TMyCommand.Create as ICommand;
  31.   b:= a.Execute;
  32.   if a is TMyCommand then writeln('a is of type TMyCommand') else writeln('a is not of type TMyCommand');
  33.   if b is TMyCommand then writeln('b is of type TMyCommand') else writeln('b is not of type TMyCommand');
  34. end.

No leaks!
Specialize a type, not a var.

dubst3pp4

  • Jr. Member
  • **
  • Posts: 86
  • Retro computing ~ GNU/Linux
    • me on Mastodon
Re: Interface method and derived types
« Reply #3 on: December 08, 2017, 09:27:22 am »
Hi Thaddy, thanks for your detailed reply. What I'm searching for is called invariant return types - which are not supported in Object Pascal. So I think I will stick with my solution - although it would also be possible to use generics. But I think this would be too heavy for my simple use case...

But can you explain, why you don't need to free an variable of type Interface? I think I don't fully understand the memory model of Free Pascal yet...
Jabber: xmpp:marc.hanisch@member.fsf.org -- Support the Free Software Foundation: https://my.fsf.org/donate

Thaddy

  • Hero Member
  • *****
  • Posts: 14201
  • Probably until I exterminate Putin.
Re: Interface method and derived types
« Reply #4 on: December 08, 2017, 11:55:49 am »
Interfaces are reference counted types. That means that if the reference count is zero, the compiler will clean up the underlying class.
You have to be a clean and careful programmer to be consistent: ALWAYS instantiate to an interface and NEVER to a class.
People often make that mistake, use the interface (works) but if it is a class instance you need to free it.
When you consistently use interfaces, everything is auto-free'd

BTW:  invariant return types are to some extend supported in multiple ways:
- Methods do not change their signature when part of an interface
- Methods can be overridden with the same signature and not call inherited
- Use TClass instead of TObject as return value: callee can reconstruct.
- Use RTTI
- Typesafe softcasts (is to test, as to use)

My example is an example of an invariant return type.... O:-) Usually fully compiled languages can not achieve the same flexibility as Java or C# which are half way scripting languages.
But if you stick to interfaces you can achieve a lot.
« Last Edit: December 08, 2017, 12:25:05 pm by Thaddy »
Specialize a type, not a var.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 844
Re: Interface method and derived types
« Reply #5 on: December 09, 2017, 08:16:44 am »
You haven't told us, what goal you try to achieve, so I can't determine, whether there is better solution of your problem, or not. Currently I assume, that you need to implement some commands, that return results, that should be completely abstracted from result handlers. I.e. result handlers don't know in a advance, what results can be returned from commands.

While Delphi\Lazarus isn't language with fully dynamic typing, it still supports many ways to determine type of object at runtime, so we still can simulate dynamic typing via the same way, it's implemented in scripts - via using dictionaries.

First of, lets define command result handler:
Code: Pascal  [Select][+][-]
  1. TAbstractCommandHandler = class
  2.   public
  3.     procedure HandleCommand(ACommandResult:TObject);//Or IUnknown
  4. end;
  5.  
  6. THandlers = class(TDictionary<$Type identifier$, TAbstractCommandHandler>)
  7.   public
  8.     procedure HandleResult(ACommandResult:TObject);//Or IUnknown
  9. end;
  10.  
  11. procedure THandlers.HandleResult(ACommandResult:TObject);
  12.   var Handler:TAbstractCommandHandler;
  13. begin
  14.   if TryGetValue(ACommandResult.$Type identifier$, Handler) then
  15.     Handler.HandleCommand(ACommandResult)
  16.   else
  17.     ShowError('Result type is unsupported: ' + ACommandResult.$Type identifier$);
  18. end;
  19.  
  20. {Or this variant}
  21.  
  22. procedure THandlers.HandleResult(ACommandResult:TObject);
  23.   var Pair:TPair<$Type identifier$, TAbstractCommandHandler>;
  24. begin
  25.   for Pair in Self do begin
  26.     if ACommandResult is Pair.Key then begin
  27.       Pair.Value.Handle(ACommandResult);
  28.       Exit;
  29.     end;
  30.   end;
  31.   ShowError('Result type is unsupported: ' + ACommandResult.$Type identifier$);
  32. end;
  33.  

Now let's define different $Type identifiers$ you may use:

1) Easiest way - to provide type explicitly, either via Id or Name
Code: Pascal  [Select][+][-]
  1. TMyId = (idFirstType, idSecondType);
  2.  
  3. TAbstractObject = class
  4.   protected
  5.     FMyTypeId:TMyId ;
  6.   public
  7.     property MyTypeId:TMyId read FMyTypeId;
  8. end;
  9.  
  10. THandlers = class(TDictionary<TMyId, TAbstractCommandHandler>)
  11.  
  12. if TryGetValue(ACommandResult.MyTypeId, Handler) then
  13.  
  14. {Or this variant}
  15.  
  16. TAbstractObject = class
  17.   protected
  18.     FMyTypeName:String;
  19.   public
  20.     property MyTypeName:String read FMyTypeName;
  21. end;
  22.  
  23. THandlers = class(TDictionary<String, TAbstractCommandHandler>)
  24.  
  25. if TryGetValue(ACommandResult.MyTypeName, Handler) then
  26.  

2) Use RTTI type
Code: Pascal  [Select][+][-]
  1. THandlers = class(TDictionary<TClass, TAbstractCommandHandler>)
  2.  
  3. if TryGetValue(ACommandResult.ClassType, Handler) then
  4.  
  5. {Or this variant}
  6.  
  7. THandlers = class(TDictionary<TClass, TAbstractCommandHandler>)
  8.  
  9. if ACommandResult is Pair.Key then begin
  10.  
  11. {Or this}
  12.  
  13. THandlers = class(TDictionary<TClass, TAbstractCommandHandler>)
  14.  
  15. if ACommandResult.InheritsFrom(Pair.Key) then begin
  16.  

3) Use GUID of interface
Code: Pascal  [Select][+][-]
  1. THandlers = class(TDictionary<TGUID, TAbstractCommandHandler>)
  2.  
  3. var MyInterface:IUnknown;
  4.  
  5. {As I know, there is no analogue of TClass for interfaces.
  6. Can we use this?
  7. MyInterface is MyGUID
  8. I'm not sure.}
  9.  
  10. ACommandResult.QueryInterface(Pair.Key, MyInterface);
  11.  
  12. if Assigned(MyInterface) then begin
  13.  

4) Use published methods and properties
Code: Pascal  [Select][+][-]
  1. TMyObject = class(TPersistent)
  2.   published
  3.     procedure MyMethod(*Some parameters*);
  4. end;
  5.  
  6. THandlers = class(TDictionary<String, TAbstractCommandHandler>)
  7.  
  8. TMyMethod = procedure($Some parameters$) of object;
  9.  
  10. var MyMethod:TMyMethod;
  11.  
  12. MyMethod := ACommandResult.MethodAddress(Pair.Key);
  13.  
  14. if Assigned(MyMethod) then begin
  15.   MyMethod($Some parameters$);
  16.   Exit;
  17. end;
  18.  
« Last Edit: December 09, 2017, 06:13:18 pm by Mr.Madguy »
Is it healthy for project not to have regular stable releases?
Just for fun: Code::Blocks, GCC 13 and DOS - is it possible?

dubst3pp4

  • Jr. Member
  • **
  • Posts: 86
  • Retro computing ~ GNU/Linux
    • me on Mastodon
Re: Interface method and derived types
« Reply #6 on: December 10, 2017, 09:09:17 pm »
Thank you all for the replies and the great excursion! I think my use case is much simpler, so I try to describe it again by some simple words:

I want to create a set of classes (which are commands to an API), which have in common, that they have a method to execute the command and return a result. For this I created an interface with the execute function.

The response of each command can be different. These won't be complex classes, but classes with a slightly different data structure (which were JSON responses parsed into a class instance).

According to the interface definition, the result of the function must be of the same type in each command. So I created a base class for the responses which is the parent for each individual response. Although the definition for the execute function says that it returns this base class, it is actually returning an instance of that specific child class.

The only problem I have with this approach is, that the developer does not know that he has to cast the result back to the child class without looking into the implementation details (or into my comments). So I thought there must be a better way to achieve the same - maybe with the help of some sort of design pattern...

Best regards,
Marc
Jabber: xmpp:marc.hanisch@member.fsf.org -- Support the Free Software Foundation: https://my.fsf.org/donate

guest58172

  • Guest
Re: Interface method and derived types
« Reply #7 on: December 11, 2017, 02:31:21 am »
Hello, another option is to do all the casts and let the user implement a kind of visitor. This option would be to explore if you have many commands. If commands are added, you have to do most of the work but user still have to add new overloads (this can be solved by using a class rather than an interface, see comments). Also this design only works if users don't have to add custom commands (i assume this is the case since commands are made from a JSON)

Code: Pascal  [Select][+][-]
  1. type
  2.  
  3.   TResultId = (r1, r2);
  4.  
  5.   // the id is used to speed up dynamic cast
  6.   TCustomResult = class
  7.     class var id: TResultId;
  8.   end;
  9.  
  10.   ICommand = interface ['{CFEA9748-7530-49E7-A1FF-91251101B458}']
  11.     function Execute: TCustomResult;
  12.   end;
  13.  
  14.   TCommand1 = class(TInterfacedObject, ICommand)
  15.   type
  16.     TResult = class(TCustomResult) end; // id set to r1
  17.   public
  18.     function Execute: TCustomResult; virtual; abstract; // a TResult
  19.   end;
  20.  
  21.   TCommand2 = class(TInterfacedObject, ICommand)
  22.   type
  23.     TResult = class(TCustomResult) end; // id set to r2
  24.   public
  25.     function Execute: TCustomResult; virtual; abstract; // a TResult
  26.   end;
  27.  
  28.   // Implemented by the user
  29.   // as an interface: possible user changes required
  30.   // as a class with virtual members: no changes needed bu user may forget to udpate
  31.   //    when new commands are added
  32.   TCustomCommandDispatcher = interface
  33.     procedure dispatch(command: TCommand1.TResult);
  34.     procedure dispatch(command: TCommand2.TResult);
  35.   end;
  36.  
  37.   // Users dont have to cast
  38.   // To Be Determined: where and how do you call this
  39.   procedure ExecuteCommand(dispatcher: TCustomCommandDispatcher; command: ICommand);
  40.   var
  41.     r: TCustomResult;
  42.   begin
  43.     r := command.Execute;
  44.     case r.id of
  45.       TResultId.r1: dispatcher.dispatch(TCommand1.TResult(r));
  46.       TResultId.r2: dispatcher.dispatch(TCommand2.TResult(r));
  47.     end;
  48.   end;

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 844
Re: Interface method and derived types
« Reply #8 on: December 11, 2017, 07:12:39 am »
Thank you all for the replies and the great excursion! I think my use case is much simpler, so I try to describe it again by some simple words:

I want to create a set of classes (which are commands to an API), which have in common, that they have a method to execute the command and return a result. For this I created an interface with the execute function.

The response of each command can be different. These won't be complex classes, but classes with a slightly different data structure (which were JSON responses parsed into a class instance).

According to the interface definition, the result of the function must be of the same type in each command. So I created a base class for the responses which is the parent for each individual response. Although the definition for the execute function says that it returns this base class, it is actually returning an instance of that specific child class.

The only problem I have with this approach is, that the developer does not know that he has to cast the result back to the child class without looking into the implementation details (or into my comments). So I thought there must be a better way to achieve the same - maybe with the help of some sort of design pattern...

Best regards,
Marc
Common way of achieving such goal in OOP - polymorphism. 1) Some abstract class is declared, that contains everything, that is needed for result handlers to work with it. 2) Actual result classes are descendants of this class. 3) Result handlers work with actual results through this abstract class - not cast it back to descendants.

Best example: device drivers in operation system. Operation system doesn't know anything about actual printer - it works with printer through abstract TAbstractPrinter class, that has Print(ABitmap:TBitmap) method. And drivers just implement this method.

Code: Pascal  [Select][+][-]
  1. {Interface - is actually abstract class}
  2. TAbstractPrinter = class
  3.   protected
  4.     {"abstract" means "no implementation" - to avoid dummy implementation stubs}
  5.     function GetResolution:Integer;virtual;abstract;
  6.   public
  7.     function Print(ABitmap:TBitmap):Boolean;virtual;abstract;
  8.     {Read only property implemented via getter method}
  9.     property Resolution:Integer read GetResolution;
  10. end;
  11.  
  12. {Actual implementation}
  13. TMyPrinter = class(TAbstractPrinter)
  14.   protected
  15.     function GetResolution:Integer;override;
  16.   public
  17.     function Print(ABitmap:TBitmap):Boolean;override;
  18. end;
  19.  
  20. function TMyPrinter.GetResolution:Integer;
  21. begin
  22.   {Real implementation here}
  23. end;
  24.  
  25. function TMyPrinter.Print(ABitmap:TBitmap):Boolean;
  26. begin
  27.   {Real implementation here}
  28. end;
  29.  
  30. {Example of usage}
  31.  
  32. {MyPrinter.drv DLL file}
  33.  
  34. function CreatePrinter:TAbstractPrinter;
  35. begin
  36.   {100% valid syntax! That's, where polymorphism magic happens!}
  37.   Result := TMyPrinter.Create;
  38. end;
  39.  
  40. {OS code}
  41.  
  42. TCreatePrinter = function:TAbstractPrinter;
  43.  
  44. function Print(ADriverName:String;ABitmap:TBitmap):Boolean;
  45.   var
  46.     Printer:TAbstractPrinter;
  47.     Driver:Handle;
  48.     CreatePrinter:TCreatePrinter;
  49. begin
  50.    Driver := LoadLibrary(DriverName);
  51.    CreatePrinter := TCreatePrinter(GetProcAddress(Driver, 'CreatePrinter'));
  52.  
  53.    Printer := CreatePrinter^;
  54.  
  55.    Result := Printer.Print(Bitmap);
  56. end;
  57.  

But please note, that this method is applicable, only if all necessary stuff, needed for access to result, can be consolidated in abstract class. If result data can be so arbitrary, that it's impossible - then only one of methods, provided above, can help.
« Last Edit: December 11, 2017, 07:16:01 am by Mr.Madguy »
Is it healthy for project not to have regular stable releases?
Just for fun: Code::Blocks, GCC 13 and DOS - is it possible?

Thaddy

  • Hero Member
  • *****
  • Posts: 14201
  • Probably until I exterminate Putin.
Re: Interface method and derived types
« Reply #9 on: December 11, 2017, 10:01:41 am »
@Mr.Madguy:

You forgot that there are two constructs common in c++ that in Pascal can only be solved by interfaces:
- Friend classes
- Multiple inheritance.

For his apparent goal his initial code is actually the right way to start with: By using his ICommand interface he implements Friend like functionality for classes that can be otherwise totally unrelated.
That is a valid pattern and probably a better solution than single inheritance OOP for his purpose. I like the dispatch code, but dispatch code is often slow.
« Last Edit: December 11, 2017, 10:10:28 am by Thaddy »
Specialize a type, not a var.

Mr.Madguy

  • Hero Member
  • *****
  • Posts: 844
Re: Interface method and derived types
« Reply #10 on: December 11, 2017, 06:11:55 pm »
@Mr.Madguy:

You forgot that there are two constructs common in c++ that in Pascal can only be solved by interfaces:
- Friend classes
- Multiple inheritance.

For his apparent goal his initial code is actually the right way to start with: By using his ICommand interface he implements Friend like functionality for classes that can be otherwise totally unrelated.
That is a valid pattern and probably a better solution than single inheritance OOP for his purpose. I like the dispatch code, but dispatch code is often slow.
But it's better to start with polymorphism, cuz interfaces may be overkill in this case.

I don't know. I don't like solution with explicit casting, cuz it's clunky. I don't know, how to explain it better - it's like using some undocumented functionality or hack. Yeah, explicit casing - is actually some sort of hack. It's unsafe, you do it at your own risk and it wouldn't be possible in case of strict typing. So, I personally try to avoid such solutions as much, as I can. But it's my personal opinion.
Is it healthy for project not to have regular stable releases?
Just for fun: Code::Blocks, GCC 13 and DOS - is it possible?

 

TinyPortal © 2005-2018