【问题标题】:How to control execution without lots of IFs?如何在没有大量 IF 的情况下控制执行?
【发布时间】:2015-12-06 14:12:37
【问题描述】:

我有一个流程,从文件导入数据开始,然后执行一系列程序,但它随时可能发现问题,应该停止执行其余程序并运行另一组程序。

这是我的示例,其中每个过程都设置了全局 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;

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

结论:

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

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

另外,如果我将任务转换为返回结果的函数,我想我会遇到同样的问题,检查每个任务的值并以此为基础停止或继续进程。

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

【问题讨论】:

  • 如果你想中止,你可以使用try...finally 并引发异常。
  • 只是在你调用的函数中引发异常?
  • @AndreasRejbrand。 whosrdaddy 是的,似乎引发异常很容易适应我当前的代码,没有太多的变化。 Dailija 向我展示了如何有效地做到这一点。我现在正在研究解决方案。
  • 也许使用 Abort 例程来引发静默异常...
  • 你的整个设计真的很糟糕。试图让它变得更好而不面对它不会产生任何值得拥有的东西。你最好什么都不做,因为三心二意的改变不会带来长期的好处,而且可能会破坏你的计划。您不能继续让程序中的每一方都可以写入的全局布尔值确定控制流!这就是我们在 1960 年代做事的方式!

标签: delphi delphi-xe7


【解决方案1】:

您应该使用自定义异常并在遇到中断工作的原因时引发它并转到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;

【讨论】:

  • @Jens 不! EurekaLog 和异常完全兼容。
  • @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; 或 @987654333 @ - 我更喜欢后者,我不喜欢 Dalija 的无参数停止这种信息丢失
  • @Arioch'我已经修改了代码使用EAbort异常类。
  • @Arioch'我的代码的主要目的是展示如何使用异常来中断执行。由于原始代码使用的是简单的过程,因此我使用这种风格使概念更易于理解。
  • 你不需要从 EAbort 派生。你只需要处理异常。
【解决方案2】:

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

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

我还认为 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;

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

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

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

【讨论】:

  • 谢谢,我不知道这可以做到,非常有趣的批处理控制。需要考虑它如何适合我的应用程序,它如何与其他代码一起使用。
  • 将 ImportBatches 转换为 TObjectList&lt;TAfterImportBatch&gt; 如果您要为每个导入任务创建批次然后释放它们,您可能会在某种程度上简化批次销毁。尽管考虑到它们是全局流控制对象,但也许您会在应用程序启动时创建它们并且永远不会销毁它们:-D
【解决方案3】:

最好的方法是通过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。
  • 许多 IoC 框架使用 RTTI,尽管 @DavidHeffernan 而单元测试库可能是其中的一个具体示例。
  • 这个特定的框架禁止为 AfterImportXXXX 方法使用有意义的名称,并且使得在其间插入新方法变得乏味。所以最后你必须开始使用属性而不是 IntToStr :-) 不是很难,但又是一个需要学习的技巧。
  • 好吧,这是一篇“我也是”的帖子,但我很想重申 Jens 的评论——RTTI(以及一般的反射)是一个非常有用的工具,尽管可能不是最好的工具这份工作。
  • @Vaneik 螺丝刀很棒,但没有那么多钉子
【解决方案4】:

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

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;

可读性更强,更容易维护。

【讨论】:

  • 这不是我正在寻找的工作流程,因为我认为对于主流程,所有主要任务都应该从一个主流程(运行)可见和可访问。如果我开始将部分移动到程序中,我将不得不来回跳来看看发生了什么。所有这些程序都已经按照他们所做的事情进行了设计,并根据结果进行了分组和命名。请参阅我的问题中的 EDIT 2。我想避免这种情况。
  • 这与问题中的代码相同。一样糟糕。
  • 我不同意,但您总是可以提出更好的解决方案。
  • 很难想象比这种滥用全局变量更糟糕的事情了。
  • 为什么是复数?有一个全局变量,我们不知道它的范围。它可能在单元的实施部分。无论如何,OP 没有要求修复全局变量,而是要求更易读的代码。我不是在这里为全局变量辩护。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-20
  • 2012-04-22
  • 1970-01-01
相关资源
最近更新 更多