2007. július 15., vasárnap

How to call procedures by name using an array of records


Problem/Question/Abstract:

I have a unit that all it does is store SQL statement for me to load and right now I'm doing:

if ReportName = "Some_Report_Name" then
  LoadSomeReportNameSql;
else if ReportName = "Some_Other_Report" then
  LoadSomeOtherReportSql;

I have about 200 reports so far...would a case statment be faster? I would, of course, change the identifier for the report to a numeric identifier, rather than a string identifier. My concern is that there will begin to be a very noticable difference once I get up to the 500 or so reports.

Answer:

With that many reports, there are two better solutions than an if/ then or case statement.


Solve 1:

An array of records containing the report name and report procedure might be faster and easier to maintain. The list could be sorted on the report name, and a binary search algorithm could be used to quickly locate the correct report procedure to execute.

This method is not new, but works very well. It is not automagical, so the programmer has to do some typing. It could be improved in a myriad of ways, like array of const parameters, TVarRec results, action identifers and encapsulation in a class. The last could get hairy if you expect that class to serve objects of other classes as well, but it is possible.


unit NamedFunctions;

interface

const
  MaxFuncs = 3;
  MaxFuncName = 13;

type
  TFuncRange = 1..MaxFuncs;
  TNamedFunc = function(args: string): string;
  TFuncName = string[MaxFuncName];
  TFuncInfo = record
    Name: TFuncName;
    Func: TNamedFunc
  end; { TNamedFunc }

  TFuncList = array[TFuncRange] of TFuncInfo;
function XSqrt(args: string): string;
function XUpStr(args: string): string;
function XToggle(args: string): string;

const
  {This list must be sorted for the function to be found}
  FuncList: TFuncList = ((Name: 'xsqrt'; Func: XSqrt), (Name: 'xtoggle'; Func: XToggle), (Name: 'xupstr'; Func: XUpStr));

function ExecFunc(AName: TFuncName; args: string): string;

implementation

uses
  Dialogs, SysUtils;

function ExecFunc(AName: TFuncName; args: string): string;
{ Binary search is overkill for a small number of functions. }
var
  CompRes, i, j, m: integer;
  Found: boolean;
begin
  AName := LowerCase(AName);
  i := 1;
  j := MaxFuncs;
  m := (i + j) shr 1;
  Found := false;
  while not Found and (i <= j) do
  begin
    CompRes := AnsiCompareStr(AName, FuncList[m].Name);
    if CompRes < 0 then
      j := m - 1
    else if CompRes > 0 then
      i := m + 1
    else
      Found := true;
    if not Found then
      m := (i + j) shr 1
  end;
  if Found then
    Result := FuncList[m].Func(args)
  else
  begin
    Result := '';
    ShowMessage('Function ' + AName + ' not found in list')
  end;
end;

function XSqrt(args: string): string;
var
  value: real;
begin
  value := 0;
  try
    value := StrToFloat(args)
  except
    on EConvertError do
      ShowMessage(args + ' is not a valid real number (XStr)')
  end;
  if value >= 0 then
    Result := FloatToStr(sqrt(value))
  else
  begin
    Result := '0.0';
    ShowMessage('Negative number passed to XSqrt')
  end;
end;

function XUpStr(args: string): string;
begin
  Result := UpperCase(args)
end;

function XToggle(args: string): string;
{ Anything other than 'TRUE' or 'T' is assumed false. }
begin
  args := UpperCase(args);
  if (args = 'TRUE') or ((length(args) = 1) and (args = 'T')) then
    Result := 'FALSE'
  else
    Result := 'TRUE'
end;

end.


Solve 2:

Another way to go would be to use the GetProcAddress Win32 API function to locate the report procedure based on the report name. This way you could store the report names and report procedure names in a text file or database. (Tip: EXEs can export routines just like DLLs can. GetProcAddress only finds exported routine names). The code might look something like this (off the top of my head...):


unit MyReports;

interface

type
  TReportProcedure = procedure;

procedure LoadSomeReportNameSql;
procedure LoadSomeOtherReportSql;
procedure ExecuteReport(AReportName: string);

implementation

procedure ExecuteReport(AReportName: string);
var
  ReportProc: TReportProcedure;
  ProcPointer: TFarProc;
begin
  {Table contains two columns: "Report Name" and "Report Procedure".  Primary key is "Report Name"}
  try
    Table1.Open;
    if Table1.FindKey([AReportName]) then
    begin
      {Get the address of the exported report procedure}
      ProcPointer := GetProcAddress(HInstance, Table1.FieldByName('Report Procedure').AsString);
      if Assigned(ProcPointer) then
      begin
        ReportProcedure := TReportProcedure(ProcPointer);
        ReportProcedure;
      end;
    end;
  finally
    Table1.Close;
  end;
end;

procedure LoadSomeReportNameSql;
begin
end;

procedure LoadSomeOtherReportSql;
begin
end;

exports
  LoadSomeReportNameSql;
LoadSomeOtherReportSql;

end.

Nincsenek megjegyzések:

Megjegyzés küldése