Search code examples
delphireflectioninterfacerttispring4d

Delphi SysUtils.Supports unexpectedly returns true


I am making a eventPublisher based on Spring4d's documentation example

The difference is that subscribers have to explicitly subscribe to events.

I want to trigger their Handle procedure based on wether or not they implement the IEventHandler<TEventType> interface.

When an incoming event is published, I find the IEventHandler<TEventType> type reference using the class name of the event and Spring4d's TType.FindType('IEventHandler<TEvent1>')

Then I loop through my subscribers (objects implementing a IEventHandler interface) and check if it supports the IEventHandler type for example.

The problem is that the Supports method returns true even if the subscriber does not implement the interface.

Also, I tried listing the interfaces of say TMyEventHandler2 type. It contains IEventHandler<TEvent2> ??

I beleive this is due to a limitation with the IEventHandler<TEvent2> and IEventHandler<TEvent1> sharing the same GUID

Is there a workaround for this ?

Using these classes & interfaces :

TEvent1 = class(TObject)
end;

TEvent2 = class(TObject)
end;

IEventHandler = interface(IInvokable)
[guid]
procedure Handle(aEvent : TObject);
end;

IEventHandler<T : class> = interface(IEventHandler)
[guid]
procedure Handle(aEvent : T);
end;

TMyEventHandler1 = class(TObject, IEventHandler, IEventHandler<TEvent1>)
public 
procedure Handle(AEvent : TObject); overload;
procedure Handle(AEvent : TEvent1); overload;
end;

TMyEventHandler2 = class(TObject, IEventHandler, IEventHandler<TEvent2>)
public 
procedure Handle(AEvent : TObject); overload;
procedure Handle(AEvent : TEvent2); overload;
end;

TEventPublisher = class(TObject)
public
  fSubscribers : IList<TValue>;
  procedure Subscribe(aSubscriber : TValue);  // Simply adds the subscriber to the list of subscribers
  procedure Publish(aEvent : TObject); // Publishes an event to the subscribers
end;

procedure TEventPublisher.Publish(const event: TObject; ownsObject: Boolean = True);
const
  IEventSubscriberName = 'IEventSubscriber<*>';
var
  consumerTypeName: string;
  consumerType    : TRttiType;
  intfType        : TRttiInterfaceType;
  subscriber      : TValue;
  subscribed      : IInterface;
  lEventSubscriber: IEventSubscriber;
  lIntfs          : IReadOnlyList<TRttiInterfaceType>;
begin

  consumerTypeName := StringReplace(IEventSubscriberName, '*', GetQualifiedClassName(event), []);
  consumerType     := TType.FindType(consumerTypeName);
  intfType         := consumerType as TRttiInterfaceType;

  for subscriber in fSubscribers do
  begin

    lIntfs := TType.GetType(subscriber.AsObject.ClassInfo).GetInterfaces();

    // lIntfs for TMyEventHandler2 containts IEventHandler<TEvent1> ???

    if Supports(subscriber.AsObject, intfType.GUID, subscribed) then
      if Supports(subscriber.AsObject, IEventSubscriber, lEventSubscriber) then
      begin
        intfType.GetMethod('Handle').Invoke(TValue.From(@subscribed, intfType.Handle), [event])
      end;
  end;

  if ownsObject then
    event.Free;
end;


lEventPublisher := TEventPublisher.Create;
lEventPublisher.Subscribe(TMyEventHandler1.Create);
lEventPublisher.Subscribe(TMyEventHandler2.Create);
lEventPublisher.Publish(TEvent1.Create); // Will both trigger TMyEventHandler1.Handle and TMyEventHandler2.Handle. Why ??

Solution

  • This happens because if you put a guid on a generic interface, every specialization of that interface will have the same guid regardless its generic type parameter.

    I usually solve this by putting a way to provide information about that into the interface (like Spring.Collections.IEnumerable has an ElementType property to get the actual type of an IEnumerable<T>).

    So the implementation would look like this:

    program GenericEventPublisher;
    
    {$APPTYPE CONSOLE}
    
    uses
      Spring,
      Spring.Collections,
      System.SysUtils;
    
    type
      IEventHandler = interface
        ['{2E4BD8F4-4EB8-4B33-84F4-B70F42EF9208}']
        procedure Handle(const event: TObject);
      end;
    
      IEventHandler<T: class> = interface
        ['{82B7521E-D719-4051-BE2C-2EC449A92B22}']
        procedure Handle(const event: T);
        function GetHandledClass: TClass;
      end;
    
      IEventPublisher = interface
        ['{2A460EF0-AE27-480F-ACEA-1B897F2DE056}']
        procedure Subscribe(const subscriber: IEventHandler);
        procedure Publish(const event: TObject; ownsObject: Boolean = True);
      end;
    
      TEventHandlerBase<T: class> = class(TInterfacedObject, IEventHandler, IEventHandler<T>)
      private
        function GetHandledClass: TClass;
        procedure Handle(const event: TObject); overload;
      public
        procedure Handle(const event: T); overload; virtual; abstract;
      end;
    
      TEvent1 = class
      end;
    
      TEvent2 = class
      end;
    
      TMyEventHandler1 = class(TEventHandlerBase<TEvent1>)
      public
        procedure Handle(const event: TEvent1); override;
      end;
    
      TMyEventHandler2 = class(TEventHandlerBase<TEvent2>)
      public
        procedure Handle(const event: TEvent2); override;
      end;
    
      TEventPublisher = class(TInterfacedObject, IEventPublisher)
      private
        fSubscribers: IList<IEventHandler>;
      public
        constructor Create;
        procedure Subscribe(const subscriber: IEventHandler);
        procedure Publish(const event: TObject; ownsObject: Boolean = True);
      end;
    
    { TEventPublisher }
    
    constructor TEventPublisher.Create;
    begin
      fSubscribers := TCollections.CreateList<IEventHandler>;
    end;
    
    procedure TEventPublisher.Publish(const event: TObject; ownsObject: Boolean);
    var
      subscriber: IEventHandler;
      eventSubscriber: IEventHandler<TObject>;
    begin
      for subscriber in fSubscribers do
        if Supports(subscriber, IEventHandler<TObject>, eventSubscriber)
          and (eventSubscriber.GetHandledClass = event.ClassType) then
            eventSubscriber.Handle(event);
    
      if ownsObject then
        event.Free;
    end;
    
    procedure TEventPublisher.Subscribe(const subscriber: IEventHandler);
    begin
      fSubscribers.Add(subscriber)
    end;
    
    { TEventHandlerBase<T> }
    
    function TEventHandlerBase<T>.GetHandledClass: TClass;
    begin
      Result := T;
    end;
    
    procedure TEventHandlerBase<T>.Handle(const event: TObject);
    begin
      Assert(event is T);
      Handle(T(event));
    end;
    
    { TMyEventHandler1 }
    
    procedure TMyEventHandler1.Handle(const event: TEvent1);
    begin
      Writeln(event.ClassName, ' handled by ', ClassName);
    end;
    
    { TMyEventHandler2 }
    
    procedure TMyEventHandler2.Handle(const event: TEvent2);
    begin
      Writeln(event.ClassName, ' handled by ', ClassName);
    end;
    
    var
      eventPublisher: IEventPublisher;
    begin
      eventPublisher := TEventPublisher.Create;
      eventPublisher.Subscribe(TMyEventHandler1.Create);
      eventPublisher.Subscribe(TMyEventHandler2.Create);
      eventPublisher.Publish(TEvent1.Create);
      eventPublisher.Publish(TEvent2.Create);
    end.
    

    Since there is the class constraint on the interface we can be sure that the interfaces are binary compatible regardless the type of T (because they can only be objects). Also using a base type for the generic event handler reduces the extra code to write. It just redirects the non generic Handle method to the generic one which has to be implemented in the concrete implementation.

    Also since the base class implements both interfaces we don't need to store the handlers in a list of TValue but can use the non generic interface type and easily access them without the need of RTTI.

    Now the Publish method is using a little trick calling Support with IEventHandler<TObject> - since the eventSubscriber is of that type we can pass the event parameter to its Handle method which happens to be the right one - this is because of the binary compatibility I explained before because we are just dealing with different classes as type of T - story would be completely different if we did not have that class constraint.