如何在没有大量 IF 的情况下控制执行?

How to control execution without lots of IFs?

我有一个从文件导入数据开始,然后执行一系列过程的过程,但在任何时候它都能发现问题,应该停止执行其余部分和 运行 另一组。

这是我的示例,其中每个过程都设置了指示停止进程的全局 gStop 变量。如果它停止了,我最后需要 运行 一些代码。

var gStop:boolean;


procedure Run;
begin
  gStop:=False;
  Import; // imports data from file
  If Not gStop Then
    AfterImport1;
  If Not gStop Then
    AfterImport2;
  If Not gStop Then
    AfterImport3;
  If Not gStop Then
    AfterImport4;
  If Not gStop Then
  If fTypeOfData = cMSSQL Then // function returns type of imported data
  begin
    ProcessMSSQLData1;
    If not gStop Then
      ProcessMSSQLData2;
    If not gStop Then
      ProcessMSSQLData3;
    If not gStop Then
      If fObjectAFoundInData Then // function checks if ObjectA was found in imported data
        ProcessObjectA;
    If not gStop Then
      ProcessMSSQLData4;
  end;
  If Not gStop Then
    AfterImport5;
  ...
  // If stopped at anytime
  If gStop then
  begin    
    LogStoppedProcess; 
    ClearImportedData;
    ...  
  end;
end;

在我的例子中,它实际上超过了 200 行代码,所以我在维护这部分代码时必须上下滚动。

有什么方法可以改进此过程以使其更具可读性、更易于维护,或者是否有任何其他方法可以在没有所有 IF 的情况下停止该过程?

编辑 1:

每一个程序都可以找到一个错误的数据,可以设置gStop := True;,当gStop = True;

编辑 2:

我想从主程序 (运行) 控制工作流,这样我就可以看到主导入后 运行 的所有任务。如果我将执行分解为许多更小的过程,我只会看到更多的混乱和更少的可读性和可维护性。那我就可以:

procedure Run;
begin
  gStop:=False;
  Import; // imports data from file
  RunEverytingAferImport; // execute ALL tasks after import
  // If stopped at anytime
  If gStop then
  begin    
    LogStoppedProcess; 
    ClearImportedData;
    ...  
  end;
end;

此工作流程似乎设计不正确。我想知道导入后 运行ning 的主要任务是什么,而不是每次我需要查看它时都进行发现之旅。所有任务都已按目的、它们做什么、如何做以及结果分组到程序中。

结论:

即使不是最好的选择,我还是决定在需要停止进程时使用 Raising Exception。我 'kind of' 理解这种方法带来的影响(一旦我实施它就会知道更多),但这似乎是朝着有朝一日更好地实施整个过程的合乎逻辑的一步。现在我不会为每个任务执行都看到所有这些 IF。好的!代码将更具可读性、可维护性。

我阅读了提供的链接以解释在停止工作流执行时使用异常的陷阱,但是,正如 Dalija Prasnikar 在评论中解释的那样,这不是任务的性能相关部分;每个应用程序只执行一次进程运行;任务已经根据他们所做的事情进行了结构化;任务已经包含多个 IF 语句,其中检查停止的进程等等,所以我认为异常不会成为我问题的一个非常糟糕的解决方案。

此外,如果我将任务转换为返回结果的函数,我想我也会遇到同样的问题,检查每个任务的值并根据该值停止或继续该过程。

所以,引发异常是我的选择。

您应该使用自定义异常并在遇到原因中断工作并转到 Stopped 代码时引发它。在您的 ImportAfterImport1 和其他代码逻辑中,只需调用 Stop 过程,它将执行 Stopped 过程。另一方面,如果一切顺利,Stopped 将不会被调用。

您可以从 EAbort 创建静默异常

派生异常
type
  EStopException = class(EAbort);

或从基础 Exception class 派生以使用常规类型异常。

type
  EStopException = class(Exception);

procedure Stop(const Msg: string);
begin
  raise EStopException.Create(Msg);
end;

procedure Import;
var sl: TStringList;
begin
  sl := TStringList.Create;
  try
    // your code logic
    if NeedToStop then Stop('something happened');        
  finally
    // perform any cleanup code needed here
    sl.Free;
  end;
end;

procedure Stopped;
begin
end;

procedure Run;
begin
  try
    Import;
    AfterImport1;
    AfterImport2;
  except
    on e: EStopException do
      Stopped;
  end;
end;

最好的方法是通过 RTTI

以下是您问题的虚拟实现: 单位 ImportU;

interface

{$M+}

uses
  RTTI;

Type
  TImporter = class
  strict private
    RttiContext: TRttiContext;
    gStop: Boolean;
    function GetMethod(const aMethodName: string): TRttiMethod;
    procedure Import;
  public    
    procedure AfterImport1;
    procedure AfterImport2;
    procedure AfterImport3;
    procedure AfterImport4;
    procedure Run;
  end;

implementation

uses
  Sysutils;
{ TImporter }

procedure TImporter.AfterImport1;
begin

end;

procedure TImporter.AfterImport2;
begin

end;

procedure TImporter.AfterImport3;
begin
  gStop := True;
end;

procedure TImporter.AfterImport4;
begin

end;

function TImporter.GetMethod(const aMethodName: string): TRttiMethod;
begin
  Result := RttiContext.GetType(Self.ClassType).GetMethod(aMethodName);
end;

procedure TImporter.Import;
begin

end;

procedure TImporter.Run;
var
  i: Integer;
  Stop: Boolean;
  RttiMethod: TRttiMethod;
begin
  i := 0;
  repeat
    inc(i);
    RttiMethod := GetMethod('AfterImport' + IntToStr(i));

    if RttiMethod = nil then
      break; //Break loop

    RttiMethod.Invoke(self, []);
  until (gStop = false);
end;

end.

此实现的优点是,如果您创建 AfterImport 函数,它将自动被调用。

类似于基于 RTTI 的 Jens Borrisholt 示例,但没有 RTTI。 因此不会绑定到包含所有方法的单个超级对象。

type TAfterImportActor = reference to procedure (var data: TImportData; var StopProcess: boolean);
     TAfterImportBatch = TList< TAfterImportActor >;

var Batch1, Batch2, BatchMSSQL: TAfterImportBatch; // don't forget to create and free them.

procedure InitImportBatches;
begin
  Batch1 := TAfterImportBatch.Create;
  Batch2 := TAfterImportBatch.Create; 
  BatchMSSQL := TAfterImportBatch.Create;

  Batch1.Add( AfterImport1 );
  Batch1.Add( SomeObject.AfterImport2 ); // not only global procedures
  Batch1.Add( SomeAnotherObject.AfterImport3 ); // might be in different modules
  Batch1.Add( AfterImport4 );

  Batch2.Add( AfterImport5 );
...
  Batch2.Add( AfterImport123 );

  BatchMSSQL.Add( ProcessMSSQLData1 );
...
  BatchMSSQL.Add( ProcessMSSQLData5 );
end;

procedure ProcessBatch(const Batch: TAfterImportBatch; var data: TImportData; var StopProcess: Boolean);
var action: TAfterImportActor;
begin
  if StopProcess then exit;

  for action in Batch do begin
    action( data, StopProcess );
    if StopProcess then break;
  end; 
end;

procedure Run;
var gStop: boolean;
    data: TImportData;
begin
  gStop:=False;
  Import(data, gStop); // imports data from file

  ProcessBatch( Batch1, data, gStop );

  If fTypeOfData = cMSSQL Then // function returns type of imported data
     ProcessBatch( BatchMSSQL, data, gStop );

  ProcessBatch( Batch2, data, gStop );
  ...

  // If stopped at anytime
  If gStop then
  begin    
    LogStoppedProcess; 
    ClearImportedData;
    ...  
  end;
end;

PS。这个框架(和上面的 RTTI 框架)缺乏任何异常控制,所以如果任何导入处理器会引发一些未捕获的异常 - 执行将跳出主进程循环而不调用清理例程。因此,这意味着您仍然必须在每个参与者(脆弱的)或 Run 过程中捕获可能的异常。但在后一种情况下,您可能会完全省略 gStop 变量,而是引发您的自定义异常。就个人而言,我更喜欢基于异常的方式而不是布尔标志。甚至 EurekaLog 也可能有用,如果您失败的 afterimport 过程会向异常添加一些有意义的消息,说明为什么导入被中止。

PPS。我还将 gStop 分成两个不同的 variables/exceptions:Batch Cancel 和 Import Abort。然后 If fTypeOfData = cMSSQL Then - 或任何其他先决条件 - 检查可能只是批处理中的第一个演员。然后这些批次可以合并到第二层 array/collection.

我还认为 EurekaLog 会忽略您的自定义异常,您会从 EAbort - http://docwiki.embarcadero.com/RADStudio/XE8/en/Silent_Exceptions

继承它们吗
type TAfterImportActor = reference to procedure (var data: TImportData; var CancelBatch, AbortImport: boolean);
     TAfterImportBatch = TList< TAfterImportActor >;

var Batch1, Batch2, BatchMSSQL: TAfterImportBatch; 
// don't forget to create and free them.

    ImportBatches: TArray<TAfterImportBatch>;

procedure MSSQLCheck(var data: TImportData; var CancelBatch, AbortImport: boolean);
begin
  CancelBatch := data.fTypeOfData <> cMSSQL; 
end;

procedure InitImportBatches;
begin
  Batch1 := TAfterImportBatch.Create;
  Batch2 := TAfterImportBatch.Create; 
  BatchMSSQL := TAfterImportBatch.Create;

  Batch1.Add( AfterImport1 );
  Batch1.Add( SomeObject.AfterImport2 ); // not only global procedures
  Batch1.Add( SomeAnotherObject.AfterImport3 ); // might be in different modules
  Batch1.Add( AfterImport4 );

  Batch2.Add( AfterImport5 );
...
  Batch2.Add( AfterImport123 );

  BatchMSSQL.Add( MSSQLCheck ); // If fTypeOfData = cMSSQL Then Run This Batch
  BatchMSSQL.Add( ProcessMSSQLData1 );
...
  BatchMSSQL.Add( ProcessMSSQLData5 );

  ImportBatches := TArray<TAfterImportBatch>.Create
     ( Batch1, BatchMSSQL, Batch2);
end;

procedure ProcessBatch(const Batch: TAfterImportBatch; var data: TImportData; var StopProcess: Boolean);
var action: TAfterImportActor; CancelBatch: boolean;
begin
  if StopProcess then exit;

  CancelBatch := false;
  for action in Batch do begin
    action( data, CancelBatch, StopProcess );
    if StopProcess or CancelBatch then break;
  end; 
end;

procedure Run;
var gStop: boolean;
    data: TImportData;
    CurrentBatch: TAfterImportBatch;
begin
  gStop := False;
  Import(data, gStop); // imports data from file

  for CurrentBatch in ImportBatches do begin   
    if gStop then break; 
    ProcessBatch( CurrentBatch, data, gStop );
  end;

  ...

  // If stopped at anytime
  If gStop then
  begin    
    LogStoppedProcess; 
    ClearImportedData;
    ...  
  end;
end;

PPPS。您可能还想看看 http://www.uweraabe.de/Blog/2010/08/16/the-visitor-pattern-part-1/

关注如何注册和调用不同的动作制定者。 这可能会给您一些想法,尽管这不完全是您的问题。

另一件需要考虑的事情可能是像 Spring4D 库中的多播事件。

为什么不把程序分成更小的子程序呢?例如:

var gStop:boolean;

procedure AfterImport;
begin
  If Not gStop Then
    AfterImport1;
  If Not gStop Then
    AfterImport2;
  If Not gStop Then
    AfterImport3;
  If Not gStop Then
    AfterImport4;

end;

procedure ProcessMSSQLData;
begin
  If Not gStop Then
  If fTypeOfData = cMSSQL Then // function returns type of imported data
  begin
    ProcessMSSQLData1;
    If not gStop Then
      ProcessMSSQLData2;
    If not gStop Then
      ProcessMSSQLData3;
    If not gStop Then
      If fObjectAFoundInData Then // function checks if ObjectA was found in imported data
        ProcessObjectA;
    If not gStop Then
      ProcessMSSQLData4;
  end;
end;

procedure AfterProcessMSSQLData;
begin
  If Not gStop Then
    AfterImport5;  
end;

这样,您的最终 Run; 将有大约 15 行代码:

procedure Run;
begin
  gStop:=False;
  Import; // imports data from file
  AfterImport;
  ProcessMSSQLData;
  AfterProcessMSSQLData;
  // If stopped at anytime
  If gStop then
  begin    
    LogStoppedProcess; 
    ClearImportedData;
    ...  
  end;
end;

可读性更高,更易于维护。