views:

76

answers:

1

Hi All,

I want to implement a RPC module. Different requests are encoded as JSON objects. They will be decoded and then be handled by a request handler. At last a corresponding response will be returned. The demo code looks as follows:

type
  IRequestHandler = interface
    function Handle(const Request: TAaaRequest): TResponse;
    function Handle(const Request: TBbbRequest): TResponse;
  end;

  TDecoder = class
    class function Decode(const Json: TJsonObject; const RequestHandler: IRequestHandler): TResponse;
  end;

class function TDecoder.Decode(const Json: TJsonObject; const RequestHandler: IRequestHandler): TResponse;
var
  Method: string;
  Request: TObject;
begin
  Method := Json['method'].AsString;
  if (Method = TAaaRequest.ClassName) then
  begin
    Request := TAaaRequest.FromJSON(Json); // Casted as TObject
    if Request <> nil then
    begin
      Result := RequestHandler.Handle(TAaaRequest(Request));
      Request.Free;
    end;
  end
  else if (Method = TBbbRequest.ClassName) then
  begin
    Request := TBbbRequest.FromJSON(Json); // Casted as TObject
    if Request <> nil then
    begin
      Result := RequestHandler.Handle(TBbbRequest(Request));
      Request.Free;
    end;
  end
  else
    Result := CreateErrorResponse('Unknown method: ' + Json.ToString);
end;

According to the code, the handling of different request types are very similar. If I have 100 different request types, I have to copy and paste the above code block 100 times. This is not clever. I am looking for a better way to do the same logic. My imagination is as follows:

TDecoder = class
private
  FRequestTypes: TDictionary<string, TClassInfo>; // Does this work?
public
  constructor Create;
  destructor Destroy; override;
  function Decode(const Json: TJsonObject; const RequestHandler: IRequestHandler): TResponse;
end;

constructor TDecoder.Create;
begin
  FRequestTypes := TDictionary<string, TClassInfo>.Create;
  FRequestTypes.Add(TAaaRequest.ClassName, TAaaRequest); // Does this work?
  FRequestTypes.Add(TBbbRequest.ClassName, TBbbRequest); 
end;

destructor TDecoder.Destroy;
begin
  FRequestTypes.Free;
  inherited;
end;

function TDecoder.Decode(const Json: TJsonObject; const RequestHandler: IRequestHandler): TResponse;
var
  Method: string;
  Info: TClassInfo;
  Request: TObject;
begin
  Method := Json['method'].AsString;
  if FRequestTypes.ContainsKey(Method) then
  begin
    // An universal way
    Info := FRequestTypes[Method];
    Request := Info.FromJSON(Json); // Casted as TObject
    if Request <> nil then
    begin
      Result := RequestHandler.Handle(Info(Request)); // Casted to corresponding class type (e.g. TAaaRequest or TBbbRequest)
      Request.Free;
    end;
  end
  else
    Result := CreateErrorResponse('Unknown method: ' + Json.ToString);
end;

I do not know, if I can write an universal way to handle a great number of different request types. Development environment Delphi 2010.

Any hint is appreciated.

+1  A: 

Your second attempt is very close. You're only missing a couple of details.

Where you've used the made-up type TClassInfo, you need to define a metaclass to represent your request classes. I assume TAaaRequest and TBbbRequest (and the 100 other request classes) all descend from some base TRequest class. Define TRequestClass like this:

type
  TRequestClass = class of TRequest;

The FromJSON method does something different for each class, right? If that's the case, then it should be virtual. (If the method does the same thing in each class, then it doesn't have to be virtual, despite what others may tell you.) You don't have to type-cast the result of the constructor; simply declare Info as a TRequest instead of a TObject.

The biggest change you'll need to make is to your IRequestHandler interface. Since every one of your objects is a TRequest, it will be clumsy to dispatch to the right interface method without having a giant if-else ladder to check each possible class.

Instead, use virtual dispatch again. Give each TRequest object a virtual Handle method, so the class declaration will look like this:

type
  TRequest = class
  public
    constructor FromJSON(const json: string);
    function Handle: TResponse; virtual; abstract;
  end;

Implement Handle for each descendant, and you're done. Ultimately, the IRequestHandler interface can go away. You've already written the handling ability into each of the request classes. You don't need one class to represent the request and another class to handle it.

If you want to have a separate handling class, then you could either go with what you have already, where you'll have a big conditional to decide which IRequestHandler method you'll call, or you have lots of request-handler objects all implement the same interface, and you decide which one to create the same way you decide which request class to create. Then you give the request object to the request-handler object and let them work together.

For example, define your handler interface:

type
  IRequestHandler = interface
    function Handle(request: TRequest): TResponse;
  end;

Register handlers like you register requests:

// Use the same names as the requests, but a different dictionary
FRequestHandlers.Add(TAaaRequest.ClassName, TAaaHandler);
FRequestHandlers.Add(TBbbRequest.ClassName, TBbbHandler);

Instantiate handlers like you do requests:

HandlerType := FRequestHandlers[Method];
HandlerObject := HandlerType.Create;
if not Supports(HandlerObject, IRequestHandler, Handler) then
  exit;

Then pass the request to the handler:

Result := Handler.Handle(Request);
Rob Kennedy
Thanks for your solution. But it seems that TRequest should implement the Handle method. I want this part to be isolated from TRequest object.
stanleyxu2005
Well, OK. Then go with my second suggestion and have a separate handler class.
Rob Kennedy