1

I must first admit that I am from the .Net world and am currently relearning Delphi (XE 10.x) (from back in high school - MANY years ago). In .Net, the mediator pattern is fairly well handled by libraries such as MediatR or MassTransit. Yet, I have found very few libraries that support a dynamic (or semi-dynamic) implementation of the Mediator Pattern in Delphi. Without going to the fancy level of scanning the executing Rtti information, I wanted to create a simple mediator where I could register a CommandHandler by Request and then get a response back. Is this possible?

Here is some example code that I've made so far - but I'm just getting stuck on how to dynamically create the objects and whether my approach is even sound.

Before examining the code, I am not stuck on using a TDictionary<string, string> for registering the types, however, my limited knowledge of Rtti makes it difficult to figure out whether it should be using TClass or TRttiTypes. If either of those would be helpful, I would appreciate additional assistance on that.

// interface

uses
  System.Generics.Collections;

type
  TUnit = record
  end;

  IRequest<TResponse> = interface
  end;

  IRequest = interface(IRequest<TUnit>)
  end;

  IRequestHandler<TResponse; TRequest: IRequest<IResponse>> = interface(IInvokable)
    function Handle(ARequest: TRequest): TResponse;
  end;

  IRequestHandler<TRequest: IRequest<TUnit>> = interface(IRequestHandler<TUnit, TRequest>)
  end;

  TMediator = class
  private
    FRequestHandlers: TDictionary<string, string>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure RegisterHandler(AHandlerClass, ARequestClass: TClass);
    function Send<TResponse, TRequest>(ARequest: TRequest): TResponse;
  end;

// implementation

constructor TMediator.Create;
begin
  Self.FRequestHandlers := TDictionary<string, string>.Create;
end;

destructor TMediator.Destroy;
begin
  Self.FRequestHandlers.Free;
  inherited;
end;

procedure TMediator.RegisterHandler(AHandlerClass, ARequestClass: TClass);
var
  LTempRequestClass : string;
  rContext          : TRttiContext;
  rType             : TRttiType;
begin
  if Self.FRequestHandlers.TryGetValue(ARequestClass.QualifiedClassName, LTempRequestClass) then
    exit;

  { I would like to add some error checking functionality to prevent classes 
    that do not implement IRequest or IRequest<> from being added here. }

  Self.FRequestHandlers.Add(ARequestClass.QualifiedClassName, AHandlerClass.QualifiedClassName);
end;

function TMediator.Send<TResponse, TRequest>(ARequest: TRequest): TResponse;
var
  LRequestHandlerClassName: string;
  LRequestHandler   : IRequestHandler<TResponse, TRequest>;
begin
  if not Self.FRequestHandlers.TryGetValue(ARequest.QualifiedClassName, LRequestHandlerClassName) then
    raise Exception.Create('Handler class not registered with this mediator.');

  { Not sure what to do here to get the LRequestHandler - I'm also using Spring4d, 
    so I considered using the QualifiedClassName as a way to resolve classes 
    registered in the TContainer }

  Result := LRequestHandler.Handle(ARequest);

end;

My anticipated usage of this would be:

NOTE: Edits below - I want to be able to register and call ANY commands that implement IRequest or IRequest<> from a single moderator.

// interface

type
  TMyResponse = class
  private
    FFoo: string;
  public
   property Foo: string read FFoo write FFoo;
  end;

  TMyResponse2 = class
  private
    FFoo2: string;
  public
   property Foo2: string read FFoo2 write FFoo2;
  end;
  
  TMyRequest = class(TInterfacedObject, IRequest<TMyResponse>)
  private
    FBar: string;
  public
    property Bar: string read FBar write FBar;
  end;

  TMyRequest2 = class(TInterfacedObject, IRequest<TMyResponse2>)
  private
    FBar2: string;
  public
    property Bar2: string read FBar2 write FBar2;
  end;

  TMyRequestHandler = class(TInterfacedObject, IRequestHandler<TMyResponse, TMyRequest>)
  public
    function Handle(ARequest: TMyRequest): TMyResponse;
  end;

  TMyRequestHandler2 = class(TInterfacedObject, IRequestHandler<TMyResponse2, TMyRequest2>)
  public
    function Handle(ARequest: TMyRequest2): TMyResponse2;
  end;


// implementation

var 
  AMediator: TMediator;
  ARequest: TMyRequest;
  ARequest2: TMyRequest2;
  AResponse: TMyResponse;
  AResponse2: TMyResponse2;
begin
  AMediator := TMediator.Create;
  ARequest := TMyRequest.Create;
  ARequest2 := TMyRequest2.Create;
  try
    ARequest.Bar := 'something';
    ARequest2.Bar2 := 'else';

    // Not sure how I would get these either - seems best to use the qualified class name
    AMediator.Register(TMyRequestHandler.QualifiedClassName, TMyRequest.QualifiedClassName);
    AMediator.Register(TMyRequestHandler2.QualifiedClassName, TMyRequest2.QualifiedClassName);

    AResponse := AMediator.Send(ARequest);
    AResponse2 := AMediator.Send(ARequest2);

    // Do something with this value
  finally
    AResponse2.Free;
    AResponse.Free;
    ARequest2.Free;
    ARequest.Free;
    AMediator.Free;
  end;
end.
LU RD
  • 34,438
  • 5
  • 88
  • 296
  • Too many questions here. `TMediator` includes generic elements so should be declared `TMediator = class` where, correspondingly, `function Send(ARequest: R): T`. When you create it it would be `TMediator.Create;` - no need for RTTI. You can use [generic constraints](http://docwiki.embarcadero.com/RADStudio/Sydney/en/Constraints_in_Generics) to ensure that only classes supporting the required interfaces are allowed, ie : `TMediator = class`. As a question I think this overall needs more focus. – J... Jul 14 '21 at 21:08
  • I apologize and will clarify the question. The idea of this TModerator (referring to the exemplars from C# I was working from) is to register and execute the Handler from ANY descendant of the TRequest or TRequest, not just a single method. I will clarify the post. Thanks for the feedback. – Jason Goble Jul 14 '21 at 22:51
  • 1
    It's difficult to understand because you're using generic syntax incorrectly. Most of that is not valid Delphi and this is 100% abstract code so it's difficult to follow your intent. What is expressed in the code here is mostly nonsensical. It would help to understand the problem this is trying to solve. If it's to rewrite a library in Delphi, I think the question needs to be more specific. – J... Jul 14 '21 at 23:16

1 Answers1

1

So, it seems I was going about this the wrong way, thanks to J... who made me rethink what I was doing. In summary, I was trying to have something act as a layer of dependency injection to be able to dynamically run a "Handler" based on a given "Request". In the end, it appears that the simple solution was to call the Spring4d DI layer I was already using to perform the function. I still feel like there is some fairly tight coupling, but I am currently satisfied with the result. Here is the code:

CQRSU.pas

unit CQRSU;

interface

uses
  System.Generics.Collections,
  Spring.Container;

type
  TUnit = record
  end;

  IBaseRequest = interface(IInvokable)
    ['GUID']
  end;

  IRequest<TResponse> = interface(IBaseRequest)
    ['GUID']
  end;

  IRequest = interface(IRequest<TUnit>)
    ['GUID']
  end;

  IRequestHandler<TResponse; TRequest: IRequest<TResponse>> = interface(IInvokable)
    ['GUID']
    function Handle(ARequest: TRequest): TResponse;
  end;

  IRequestHandler<TRequest: IRequest<TUnit>> = interface(IRequestHandler<TUnit, TRequest>)
    ['GUID']
  end;

implementation

end.

ServicesU.pas

unit ServicesU;

interface

  uses
    CQRSU;

  type
    TMyResponse = class
    private
      FMyResult: string;
    public
      property MyResult: string read FMyResult write FMyResult;
    end;

    TMyRequest = class(TInterfacedObject, IRequest<TMyResponse>)
    private
      FMyParameter: string;
    public
      property MyParameter: string read FMyParameter write FMyParameter;
    end;

    TMyRequestHandler = class(TInterfacedObject, IRequestHandler<TMyResponse, TMyRequest>)
    public
      function Handle(ARequest: TMyRequest): TMyResponse;
    end;

implementation

  { TMyRequestHandler }

  function TMyRequestHandler.Handle(ARequest: TMyRequest): TMyResponse;
  begin
    Result := TMyResponse.Create;
    Result.MyResult := ARequest.MyParameter + ' Processed';
  end;

end.

TestCQRS.dpr

program TestCQRS;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Spring.Container,
  System.SysUtils,
  CQRSU in 'CQRSU.pas',
  ServicesU in 'ServicesU.pas';

var
  LContainer:        TContainer;
  LMyRequestHandler: IRequestHandler<TMyResponse, TMyRequest>;
  LRequest:          TMyRequest;
  LResponse:         TMyResponse;

begin
  LContainer := TContainer.Create;
  try
    LRequest             := TMyRequest.Create;
    LRequest.MyParameter := 'Hello there!';
    try
      LContainer.RegisterType<TMyRequestHandler>.Implements<IRequestHandler<TMyResponse, TMyRequest>>;
      LContainer.Build;
      LMyRequestHandler := LContainer.Resolve<IRequestHandler<TMyResponse, TMyRequest>>;
      LResponse         := LMyRequestHandler.Handle(LRequest);
      writeln(LResponse.MyResult);
      readln;
    except
      on E: Exception do
        writeln(E.ClassName, ': ', E.Message);
    end;
  finally
    if Assigned(LResponse) then
      LResponse.Free;
    if Assigned(LRequest) then
      LRequest.Free;
    LContainer.Free;
  end;

end.