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 ??
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.