如何在不使用大量if语句的情况下控制执行?

4

我有一个流程,首先从文件中导入数据,然后执行一系列的过程,但是在任何时候都可能出现问题,应该停止执行其余的过程,并运行另一组过程。

这里是我的示例,每个过程设置全局变量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:
我希望从主过程(Run)控制工作流程,这样我就可以看到主要导入之后运行的所有任务。如果我将执行分成许多较小的过程,我只会看到更多的混乱和较少的可读性和可维护性。那么我只需要:
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;

这个工作流似乎设计得不正确。我想知道导入后主要运行的任务是什么,而不需要每次都进行发现之旅。所有任务已经按照目的、它们所做的事情以及它们如何完成分组到程序中,并给出结果。

结论:

尽管不是最好的选择,但当需要停止过程时,我决定采用引发异常的方法。我“有点”理解这种方法带来的影响(一旦我实施它,将了解更多),但这似乎是迈向整个过程更好实施的逻辑步骤。现在我不会看到每个任务执行的所有IF语句。很好!代码将更易读、可维护。

我阅读了提供的链接,以解释在停止工作流执行时使用异常的缺陷,但是,正如Dalija Prasnikar在注释中解释的那样,这不是任务的性能相关部分;该过程每次应用程序运行时只执行一次;任务已经相当结构化,按照它们所做的事情分组;任务已经包括多个IF语句,在其中检查停止进程等等,因此我认为异常并不是一个真正糟糕的解决方案。

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

因此,我选择了引发异常的方法。


2
如果你想要中止程序,可以使用 try...finally 并抛出一个异常。 - Andreas Rejbrand
2
在你所调用的函数中抛出一个异常就好了吗? - whosrdaddy
1
也许可以使用Abort例程来引发一个静默异常... - Jannie Gerber
2
你的整个设计真的很糟糕。试图在不面对它的情况下改进它将不会产生任何值得拥有的东西。你最好什么都不做,因为半心半意的改变不会带来长期的好处,而且很可能会破坏你的程序。你不能让全局布尔值决定控制流程,每个程序中的参与方都可以写入!那是我们在20世纪60年代的做法! - David Heffernan
我同意,这就是为什么我正在测试下面的Raise异常解决方案,因为它似乎是目前最优的解决方案。 - Mike Torrettinni
显示剩余4条评论
4个回答

7
当你遇到需要中断工作并进入Stopped代码的原因时,应该使用自定义异常并引发它。在你的ImportAfterImport1和其他代码逻辑中,只需调用Stop过程,它将执行Stopped过程。另一方面,如果一切顺利,就不会调用Stopped
你可以从EAbort派生出你的异常,创建一个静默异常。
type
  EStopException = class(EAbort);

或者从基类 Exception 派生出常规类型异常来使用。
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;

5
不!EurekaLog 和异常完全兼容。 - David Heffernan
2
@MikeTorrettinni type EStopException = class(EAbort) public class procedure Stop(const msg: string = 'Import error happened'); end; ... class procedure EStopException.Stop; begin raise EStopException.Create(msg); end; 可以提供更少的命名空间污染(“stop”是一个太通用的名称,可能会被许多库/对象使用),您可以将其称为 EStopException.Stop;EStopException.Stop(一些有意义的日志信息) - 我更喜欢后者,我不喜欢Dalija的无参数Stop,因为这样会丢失信息。 - Arioch 'The
2
@Arioch'The 我已经修改了代码,使用EAbort异常类。 - Dalija Prasnikar
2
你不需要从EAbort派生。你只需要处理异常即可。 - David Heffernan
1
@DalijaPrasnikar 嗯,辅助函数也必须依赖于某些东西,因此您基本上在那里打包了一个全局变量。但这不是重点,重点是仅有 Run 过程依赖于全局变量。在您的更改之后,另外一百个方法将依赖于 NeedToStop。那么从依赖性方面来看,这算是一种改进吗? - Wodzu
显示剩余23条评论

3
类似于Jens Borrisholt基于RTTI的例子,但是没有使用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框架)缺乏任何异常控制,因此如果任何导入处理器引发了一些未捕获的异常 - 执行将跳出主进程循环而不调用清理例程。这意味着您仍然必须在每个Actor(易碎品)或Run过程中捕获可能的异常。但是在后一种情况下,您可以完全省略gStop变量,而是引发自定义异常。个人而言,我更喜欢基于异常的方式而不是布尔标志。即使EurekaLog也可能有用,如果您失败的afterimport过程会添加到异常一些有意义的消息,以解释为什么导入被中止。

PPS. 我还将gStop拆分为两个不同的变量/异常:批次取消和导入中止。然后,If fTypeOfData = cMSSQL Then - 或任何其他先决条件 - 检查可以成为批处理中的第一个Actor。然后,批处理可以合并成第二层数组/集合。

我还认为,如果您从EAbort继承它们,EurekaLog将忽略您的自定义异常 - 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;

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

关注不同的操作制造者如何注册和调用。这可能会给您一些想法,虽然它并不完全符合您的问题。

另一个需要考虑的事情可能是类似于Spring4D库中的多播事件。


谢谢,我不知道这可以做到,批处理控制非常有趣。需要考虑如何将其融入我的应用程序中,以及它如何与其他代码配合使用。 - Mike Torrettinni
ImportBatches 转换为 TObjectList<TAfterImportBatch>,您可以在创建每个导入任务的批次并释放它们后,有所简化。但考虑到它们是全局流控对象,也许您会在应用程序启动时创建它们,并且永远不会销毁它们 :-D - Arioch 'The

0

最好的方法是通过 RTTI 来实现。

以下是一个虚拟实现你的问题的单元: unit 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函数,它将自动调用。


5
以我之见,最糟糕的做法是通过运行时类型识别实现这一点。 - David Heffernan
1
许多IoC框架使用RTTI,尽管@DavidHeffernan说单元测试库可能是这些框架的一个具体示例。 - Arioch 'The
2
这个特定的框架禁止使用有意义的名称来命名AfterImportXXXX方法,并且使得在它们之间插入新方法变得繁琐。因此,最终你将不得不开始使用属性而不是IntToStr :-) 这并不是很难,但又是要学习的另一个技巧。 - Arioch 'The
1
好的,这有点像是一个“我也是”的帖子,但我觉得有必要重申Jens的评论 - 运行时类型信息(RTTI)(以及反射一般而言)是一种非常有用的工具,尽管可能不是最适合这个任务的最佳工具。 - Vaneik
1
@Vaneik 螺丝刀很好用,但对于钉子来说就不太行了。 - David Heffernan
显示剩余3条评论

0
为什么不将程序分成更小的子程序呢?例如:
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;

更易读,维护也稍微容易一些。


这不是我想要的工作流程,因为我认为对于主要过程,所有主要任务都应该在一个主过程(Run)中可见和可访问。如果我开始将部件移入过程中,我将不得不来回跳转以查看正在发生的情况。所有这些过程已经通过它们所做的事情进行了设计,按结果进行了分组和命名。请参见我的问题中的EDIT 2。我想避免这种情况。 - Mike Torrettinni
2
这与问题中的代码完全相同,同样糟糕。 - David Heffernan
我不同意,但你可以提出更好的解决方案。 - Wodzu
很难想象有什么比这种滥用全局变量更糟糕的事情了。 - David Heffernan
为什么要用复数形式?有一个全局变量,我们不知道它的作用域。它可能在单元的实现部分中。无论如何,OP没有要求修复全局变量,而是要求更易读的代码。我并不是在为全局变量辩护。 - Wodzu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接