How to test the type of a generic interface?
I'm not sure if the title makes sense, but I hope you can understand my question with some code.
Given the following code for a publish/subscribe framework.
type
IMessage = interface
['{B1794F44-F6EE-4E7B-849A-995F05897E1C}']
end;
ISubscriber = interface
['{D655967E-90C6-4613-92C5-1E5B53619EE0}']
end;
ISubscriberOf<T: IMessage> = interface(ISubscriber)
procedure Consume(const message: T);
end;
TMessageService = class
private
FSubscribers: TList<ISubscriber>;
public
constructor Create;
destructor Destroy; override;
procedure SendMessage(const message: IMessage);
procedure Subscribe(const Subscriber: ISubscriber);
procedure Unsubscribe(const Subscriber: ISubscriber);
end;
That would be used like this:
TMyMessage = class(TInterfacedObject, IMessage);
TMySubscriber = class(TInterfacedObject, ISubscriberOf<TMyMessage>)
procedure Consume(const Message: TMyMessage);
end;
TMyOtherMessage = class(TInterfacedObject, IMessage);
TMyOtherSubscriber = class(TInterfacedObject, ISubscriberOf<TMyOtherMessage>)
procedure Consume(const Message: TOtherMessage);
end;
How can I loop the subscriber开发者_JS百科s list and send the message to the proper subscribers?
The subscribers list will have all subscribers for all types of messages. The SendMessage have to find the subscribers for the type of message provided as param and send it to whom implements the proper interface to consume that type of message.
procedure TMessageService.SendMessage(const message: IMessage);
var
Subscriber: ISubscriber;
begin
for Subscriber in FSubscribers do
begin
// How to send the message only to the subscribers of the correspondent type of message
end;
end;
Thanks!
BTW, this code is based on this blog post.
Edit: found a way to make this less convoluted (please vote on this answer as you like this; it took quite a while to get it right).
Note it uses the new Rtti
unit, so it works only for Delphi 2010 and up (I used Delphi XE for developing this, I did not yet verify this in Delphi 2010).
For the Supports
, you need to store some IID GUIDs with your interfaces and a means to query them.
Since you want to use this with generics, you want to able to query the IID GUID from an interface type, not from an interface reference (as Hallvard Vassbotn showed with a hack in 2006).
The new RTTI introduced in Delphi 2010 allows you to do just that:
unit RttiUnit;
interface
type
TRtti = record
//1 similar to http://hallvards.blogspot.com/2006/09/hack11-get-guid-of-interface-reference.html but for the interface type, not for a reference
class function GetInterfaceIID<T: IInterface>(var IID: TGUID): Boolean; static;
end;
implementation
uses
TypInfo,
Rtti;
class function TRtti.GetInterfaceIID<T>(var IID: TGUID): Boolean;
var
TypeInfoOfT: PTypeInfo;
RttiContext: TRttiContext;
RttiInterfaceType: TRttiInterfaceType;
RttiType: TRttiType;
begin
TypeInfoOfT := TypeInfo(T);
RttiContext := TRttiContext.Create();
RttiType := RttiContext.GetType(TypeInfoOfT);
if RttiType is TRttiInterfaceType then
begin
RttiInterfaceType := RttiType as TRttiInterfaceType;
IID := RttiInterfaceType.GUID;
Result := True;
end
else
Result := False;
end;
end.
So now the changed code, which I rearranged a bit, and spread over more units to keep the overview.
ClassicMessageSubscriberUnit: has the non generic interfaces IMessage
and ISubscriber
(they descend from IImplementedWithClass
which makes it easier to log things.
unit ClassicMessageSubscriberUnit;
interface
type
IImplementedWithClass = interface(IInterface)
function ToString: string;
end;
IMessage = interface(IImplementedWithClass)
['{B1794F44-F6EE-4E7B-849A-995F05897E1C}']
end;
ISubscriber = interface(IImplementedWithClass)
['{D655967E-90C6-4613-92C5-1E5B53619EE0}']
end;
implementation
end.
GenericSubscriberOfUnit: contains the generic ISubscriberOf
interface which descends from the generic ISupporterOf
and a generic base implementation called TSupporterOf
:
unit GenericSubscriberOfUnit;
interface
uses
ClassicMessageSubscriberUnit;
type
ISupporterOf<T: IMessage> = interface(ISubscriber)
['{0905B3EB-B17E-4AD2-98E2-16F05D19484C}']
function Supports(const Message: T): Boolean;
end;
ISubscriberOf<T: IMessage> = interface(ISupporterOf<T>)
['{6FD82B1D-61C6-4572-BA7D-D70DA9A73285}']
procedure Consume(const Message: T);
end;
type
TSupporterOf<T: IMessage> = class(TInterfacedObject, ISubscriber, ISupporterOf<T>)
function Supports(const Message: T): Boolean;
end;
implementation
uses
SysUtils,
RttiUnit;
function TSupporterOf<T>.Supports(const Message: T): Boolean;
var
IID: TGUID;
begin
if TRtti.GetInterfaceIID<T>(IID) then
Result := SysUtils.Supports(Message, IID)
else
Result := False;
end;
end.
MessageServiceUnit: now only contains TMessageService
, some type aliases and some actual code for managing the list so I could actually test it.
unit MessageServiceUnit;
interface
uses
Generics.Collections,
ClassicMessageSubscriberUnit,
GenericSubscriberOfUnit;
type
ISubscriberOfIMessage = ISubscriberOf<IMessage>;
TListISubscriber = TList<ISubscriber>;
TMessageService = class
private
FSubscribers: TListISubscriber;
strict protected
procedure Consume(const SubscriberOf: ISubscriberOfIMessage; const Message: IMessage); virtual;
public
constructor Create;
destructor Destroy; override;
procedure SendMessage(const Message: IMessage);
procedure Subscribe(const Subscriber: ISubscriber);
procedure Unsubscribe(const Subscriber: ISubscriber);
end;
implementation
uses
SysUtils;
constructor TMessageService.Create;
begin
inherited Create();
FSubscribers := TListISubscriber.Create();
end;
destructor TMessageService.Destroy;
begin
FreeAndNil(FSubscribers);
inherited Destroy();
end;
procedure TMessageService.SendMessage(const Message: IMessage);
var
LocalMessage: IMessage;
lSubscriber: ISubscriber;
lSubscriberOf: ISubscriberOf<IMessage>;
begin
for lSubscriber in FSubscribers do
begin
LocalMessage := Message; // to prevent premature freeing of Message
if Supports(lSubscriber, ISubscriberOf<IMessage>, lSubscriberOf) then
if lSubscriberOf.Supports(LocalMessage) then
Consume(lSubscriberOf, LocalMessage);
end;
end;
procedure TMessageService.Subscribe(const Subscriber: ISubscriber);
begin
FSubscribers.Add(Subscriber);
end;
procedure TMessageService.Unsubscribe(const Subscriber: ISubscriber);
begin
FSubscribers.Remove(Subscriber);
end;
procedure TMessageService.Consume(const SubscriberOf: ISubscriberOfIMessage; const Message: IMessage);
begin
SubscriberOf.Consume(Message);
end;
end.
Finally a unit that I used to test everything (it uses the bo-library at http://bo.codeplex.com):
unit GenericPublishSubscribeMainFormUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, LoggerInterfaceUnit, MessageServiceUnit,
MessageSubscribersUnit, ClassicMessageSubscriberUnit;
type
TGenericPublishSubscribeMainForm = class(TForm)
TestPublisherButton: TButton;
LogMemo: TMemo;
procedure TestPublisherButtonClick(Sender: TObject);
strict private
FLogger: ILogger;
strict protected
function GetLogger: ILogger;
property Logger: ILogger read GetLogger;
public
destructor Destroy; override;
end;
type
TLoggingMessageService = class(TMessageService)
strict private
FLogger: ILogger;
strict protected
procedure Consume(const SubscriberOf: ISubscriberOfIMessage; const Message: IMessage); override;
public
constructor Create(const Logger: ILogger);
property Logger: ILogger read FLogger;
end;
var
GenericPublishSubscribeMainForm: TGenericPublishSubscribeMainForm;
implementation
uses
LoggerUnit,
OutputDebugViewLoggerUnit,
LoggersUnit,
MessagesUnit;
{$R *.dfm}
destructor TGenericPublishSubscribeMainForm.Destroy;
begin
inherited Destroy;
FLogger := nil;
end;
function TGenericPublishSubscribeMainForm.GetLogger: ILogger;
begin
if not Assigned(FLogger) then
FLogger := TTeeLogger.Create([
TOutputDebugViewLogger.Create(),
TStringsLogger.Create(LogMemo.Lines)
]);
Result := FLogger;
end;
procedure TGenericPublishSubscribeMainForm.TestPublisherButtonClick(Sender: TObject);
var
LoggingMessageService: TLoggingMessageService;
begin
LoggingMessageService := TLoggingMessageService.Create(Logger);
try
LoggingMessageService.Subscribe(TMySubscriber.Create() as ISubscriber);
LoggingMessageService.Subscribe(TMyOtherSubscriber.Create() as ISubscriber);
LoggingMessageService.SendMessage(TMyMessage.Create());
LoggingMessageService.SendMessage(TMyOtherMessage.Create());
finally
LoggingMessageService.Free;
end;
end;
constructor TLoggingMessageService.Create(const Logger: ILogger);
begin
inherited Create();
FLogger := Logger;
end;
procedure TLoggingMessageService.Consume(const SubscriberOf: ISubscriberOfIMessage; const Message: IMessage);
var
MessageImplementedWithClass: IImplementedWithClass;
MessageString: string;
SubscribeImplementedWithClass: IImplementedWithClass;
SubscriberOfString: string;
begin
SubscribeImplementedWithClass := SubscriberOf;
MessageImplementedWithClass := Message;
SubscriberOfString := SubscribeImplementedWithClass.ToString;
MessageString := MessageImplementedWithClass.ToString; // wrong VMT here, Delphi XE SP2
Logger.Log('Consume(SubscriberOf: %s, Message:%s);',
[SubscriberOfString, MessageString]);
// [SubscriberOf.ClassType.ClassName, Message.ClassType.ClassName]);
inherited Consume(SubscriberOf, Message);
end;
end.
--jeroen
Old solution:
This might do it, but I still find the solution a bit convoluted.
MessageServiceUnit: ISubscriberOf
now has a GUID
and a Supports
method to check if the IMessage
is in fact supported.
unit MessageServiceUnit;
interface
uses
Generics.Collections;
type
IMessage = interface(IInterface)
['{B1794F44-F6EE-4E7B-849A-995F05897E1C}']
end;
ISubscriber = interface(IInterface)
['{D655967E-90C6-4613-92C5-1E5B53619EE0}']
end;
ISubscriberOf<T: IMessage> = interface(ISubscriber)
['{6FD82B1D-61C6-4572-BA7D-D70DA9A73285}']
procedure Consume(const Message: T);
function Supports(const Message: T): Boolean;
end;
TMessageService = class
private
FSubscribers: TList<ISubscriber>;
public
constructor Create;
destructor Destroy; override;
procedure SendMessage(const Message: IMessage);
procedure Subscribe(const Subscriber: ISubscriber);
procedure Unsubscribe(const Subscriber: ISubscriber);
end;
implementation
uses
SysUtils;
constructor TMessageService.Create;
begin
inherited Create();
end;
destructor TMessageService.Destroy;
begin
inherited Destroy();
end;
procedure TMessageService.SendMessage(const Message: IMessage);
var
lSubscriber: ISubscriber;
lSubscriberOf: ISubscriberOf<IMessage>;
begin
for lSubscriber in FSubscribers do
begin
if Supports(lSubscriber, ISubscriberOf<IMessage>, lSubscriberOf) then
if lSubscriberOf.Supports(Message) then
lSubscriberOf.Consume(Message);
end;
end;
procedure TMessageService.Subscribe(const Subscriber: ISubscriber);
begin
FSubscribers.Add(Subscriber);
end;
procedure TMessageService.Unsubscribe(const Subscriber: ISubscriber);
begin
FSubscribers.Remove(Subscriber);
end;
end.
MessagesUnit: Messages each have an interface
with a GUID
so Supports
can check for the GUID
.
unit MessagesUnit;
interface
uses
MessageServiceUnit;
type
IMyMessage = interface(IMessage)
['{84B42EC8-CAC0-44B4-97A8-05AE5B636236}']
end;
TMyMessage = class(TInterfacedObject, IMessage, IMyMessage);
IMyOtherMessage = interface(IMessage)
['{AB323765-FF7B-4852-91AA-B7ECC1845B41}']
end;
TMyOtherMessage = class(TInterfacedObject, IMessage, IMyOtherMessage);
implementation
end.
MessageSubscribersUnit: all subscribers have a Supports
method checking the right GUID
.
unit MessageSubscribersUnit;
interface
uses
MessagesUnit, MessageServiceUnit;
type
TMySubscriber = class(TInterfacedObject, ISubscriberOf<IMyMessage>)
procedure Consume(const Message: IMyMessage);
function Supports(const Message: IMyMessage): Boolean;
end;
TMyOtherSubscriber = class(TInterfacedObject, ISubscriberOf<IMyOtherMessage>)
procedure Consume(const Message: IMyOtherMessage);
function Supports(const Message: IMyOtherMessage): Boolean;
end;
implementation
uses
SysUtils;
procedure TMySubscriber.Consume(const Message: IMyMessage);
begin
//
end;
function TMySubscriber.Supports(const Message: IMyMessage): Boolean;
begin
Result := SysUtils.Supports(Message, IMyMessage);
end;
procedure TMyOtherSubscriber.Consume(const Message: IMyOtherMessage);
begin
//
end;
function TMyOtherSubscriber.Supports(const Message: IMyOtherMessage): Boolean;
begin
Result := SysUtils.Supports(Message, IMyOtherMessage);
end;
end.
MessagesUnit: contains the specific messages (both the interface and class types), which contain the IID GUIDs to distinguish them with Supports
.
unit MessagesUnit;
interface
uses
MessageServiceUnit,
ClassicMessageSubscriberUnit;
type
IMyMessage = interface(IMessage)
['{84B42EC8-CAC0-44B4-97A8-05AE5B636236}']
end;
TMyMessage = class(TInterfacedObject, IMessage, IMyMessage);
IMyOtherMessage = interface(IMessage)
['{AB323765-FF7B-4852-91AA-B7ECC1845B41}']
end;
TMyOtherMessage = class(TInterfacedObject, IMessage, IMyOtherMessage);
implementation
end.
MessageSubscribersUnit: contains the specific subscribers (both the interface and class types), which now do not need the Supports
method any more: they only contain the Consume
method.
unit MessageSubscribersUnit;
interface
uses
MessagesUnit,
MessageServiceUnit,
GenericSubscriberOfUnit,
ClassicMessageSubscriberUnit;
type
TMySubscriber = class(TSupporterOf<IMyMessage>, ISubscriber, ISubscriberOf<IMyMessage>)
procedure Consume(const Message: IMyMessage);
end;
TMyOtherSubscriber = class(TSupporterOf<IMyOtherMessage>, ISubscriber, ISubscriberOf<IMyOtherMessage>)
procedure Consume(const Message: IMyOtherMessage);
end;
implementation
uses
SysUtils;
procedure TMySubscriber.Consume(const Message: IMyMessage);
begin
//
end;
procedure TMyOtherSubscriber.Consume(const Message: IMyOtherMessage);
begin
//
end;
end.
--jeroen
精彩评论