4

I have a process that starts with importing data from file and then executes a series of procedures, but at anytime it can find a problem and should stop executing rest of them and run another set.

Here is my example where every procedure sets global gStop variable that indicates stopping the process. If it was stopped, I need run some code for that at the end.

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;

In my case it actually goes over 200 lines of code, so I have to scroll up and down when I maintain this part of code.

Any way I can improve this procedure to be more readable, easier to maintain or just is there any other way I can stop the process without all the IFs?

EDIT 1:

Every procedure can find a faulty data and can set gStop := True; and the process should skip all the rest of the procedure and just execute the part of the code at the end when gStop = True;

EDIT 2:

I would like to keep the workflow controlled from main procedure (Run) so I can see all the tasks that are run after the main Import. I only see more confusion and less readability and maintainability if I break the execution into many smaller procedure. Then I could just have:

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;

This workflow doesn't seem designed correctly. I want to know what are main tasks running after Import and not go on discovery trip every time I need to review it. All tasks are already group into the procedure by purpose, what they do and how they do it, and results.

CONCLUSION:

Even though not the best option, I decided to go with Raising Exception when stopping the process is needed. I 'kind of' understand the implications that this approach brings (more will be known once I implement it), but it seems a logical step towards some-day better implementation of the whole process. Now I will not see all these IFs for every task execution. Good! The code will be more readable, maintainable.

I read the links provided to explain the pitfalls of using Exceptions in stopping workflow execution, but, as Dalija Prasnikar explained in comments, this is not a performance related part of the task; process is only executed once per application run; tasks are already quite structured by what they do; tasks already include multiple IF statements where stopped process is checked and so on, so I think exceptions don't fall into a really bad solution to my problem.

Also, if I turn tasks into functions with returning results, I think I will have the same problem, checking values of every task and stop or continue the process based on that.

So, Raising exception is what I choose.

Mike Torrettinni
  • 1,759
  • 2
  • 12
  • 39
  • 2
    You could use a `try...finally` and raise an exception if you want to abort. – Andreas Rejbrand Dec 06 '15 at 14:20
  • 2
    Just raise an exception in the function you are calling? – whosrdaddy Dec 06 '15 at 14:20
  • @AndreasRejbrand. whosrdaddy Yes, it seems raising exception fits easily into my current code, without too many changes. Dailija showed me how to do it efficiently. I'm investigating the solution now. – Mike Torrettinni Dec 06 '15 at 18:12
  • 1
    Maybe use the Abort routine to raise a silent exception... – Jannie Gerber Dec 07 '15 at 10:11
  • You wrote that your `Run;` procedure has over 200 lines of code. Are you trying to say that you have over 100 main tasks in your procedure? (With the assumption that 100 lines of code is used for `gStop` check). If that is true then well.. those are not main tasks. I don't know how organizing your code into smaller chunks can be less readable and maintanable. – Wodzu Dec 07 '15 at 13:13
  • Correct, with checking `gStop` amounts to over 200 lines. I'm already trying to combine some tasks as much as I can, but will still leave at least 50 major tasks, that I need in one flow. So, the question stay exactly the same, how to improve based on the fact that every major task needs to have condition to run when not stopped. – Mike Torrettinni Dec 07 '15 at 13:29
  • 2
    Your entire design is really poor. Trying to make it better without facing up to that won't yield anything worth having. You'd be better doing nothing because a half-hearted change will bring no long term benefit and likely break your program. You can't go on having control flow determined by a global boolean that every party in your program can write to! That's how we did things in the 1960s! – David Heffernan Dec 07 '15 at 14:43
  • I agree, that is why I'm testing the Raise exception solution below, as it seem to be the optimal solution, for me, right now. – Mike Torrettinni Dec 07 '15 at 16:04
  • Exceptions are not a bad approach here, they are clearly the best approach – David Heffernan Dec 08 '15 at 07:27

4 Answers4

7

You should use custom exception and raise it when ever you encounter reason to break your work and go to Stopped code. In your Import, AfterImport1 and other code logic just call Stop procedure and it will execute Stopped procedure. On the other hand if all goes well, Stopped will not be called.

You can derive your exception from EAbort creating silent exception

type
  EStopException = class(EAbort);

or derive from base Exception class to use regular type 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;
Dalija Prasnikar
  • 24,159
  • 30
  • 74
  • 140
  • Interesting. Will this not trigger exception logging/catching with EurekaLog? – Mike Torrettinni Dec 06 '15 at 14:42
  • EurekaLog is not exactly my territory, but yes it should trigger logging, but AFAIK you can also exclude processing for specific exceptions. – Dalija Prasnikar Dec 06 '15 at 14:58
  • @MikeTorrettinni if you are using Eurekalog then dont use exceptions. – Jens Borrisholt Dec 06 '15 at 15:16
  • 5
    @Jens No! EurekaLog and exceptions are perfectly compatible. – David Heffernan Dec 06 '15 at 16:00
  • I guess the only change to knock off EurekaLog would be `EStopException = class(EAbort);` - http://docwiki.embarcadero.com/RADStudio/XE8/en/Silent_Exceptions – Arioch 'The Dec 06 '15 at 17:22
  • @DalijaPrasnikar Thank you also for pointing out that I can raise exceptions from another procedure and call it from all the places needed to raise that exception, because I need to raise same exception from every procedure in the process...sometimes from multiple places within the same procedure.... very good! – Mike Torrettinni Dec 06 '15 at 18:04
  • 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;` might provide with less namespace pollution ("stop" is way too generic name and can be used by many libs/objects) and you might call it as `EStopException.Stop;` or `EStopException.Stop( some meaningful message to log )` - i'd prefer the latter, I do not like Dalija's parameter-less Stop for this information loss – Arioch 'The Dec 06 '15 at 18:27
  • 2
    @Arioch'The I have modified the code to use `EAbort` Exception class. – Dalija Prasnikar Dec 06 '15 at 18:36
  • 1
    @Arioch'The Primary purpose of my code was to show how to use exception to break execution. Since, original code was using plain procedures, I used that style to make concept easier to understand. – Dalija Prasnikar Dec 06 '15 at 18:41
  • 2
    You don't need to derive from EAbort. You only need to handle the exception. – David Heffernan Dec 06 '15 at 20:03
  • @DavidHeffernan I am not familiar with EurekaLog, so in that combination EAbort might be more suitable. – Dalija Prasnikar Dec 06 '15 at 20:31
  • 1
    You don't need to change anything with EurekaLog. All it does is put in special handling for unhandled exceptions. It doesn't change the logic. – David Heffernan Dec 07 '15 at 07:06
  • Seems like this will be what I will implement - Raising custom exception and if I name it properly, it should be very clear what is it for. Quite easy and understandable. I'm doing some tests, but before I finally start changing... is there any obvious way this will not work or any obvious issues might arise? – Mike Torrettinni Dec 07 '15 at 15:06
  • The thing is that you need to move `NeedToStop` inside **every** of the smaller functions If those functions are in different units then you are introducing a dependency. So from now on, you can not run `AfterImport1;` without including a unit in which NeedToStop is declared. That might be a bad thing. – Wodzu Dec 07 '15 at 15:09
  • @Wodzu Original code depended on setting global variable. Depending on specific custom exception type and/or single helper function to raise that exception is improvement as far as dependency is concerned. – Dalija Prasnikar Dec 07 '15 at 15:31
  • @Wodzu correct, all needed units are already linked - used, so no change here. Every procedure already has a way to set global gStop variable, so if I replace these with Raise exception will be a bit clearer as I don't need all these IF checks. I see this as big improvement in my code. As Dalija Prasnikar very clearly showed how I can very cleanly raise exception via procedure, I can actually raise 2 different exceptions, if needed and don't need to have 2 global variables and check both of them. Correct? – Mike Torrettinni Dec 07 '15 at 15:36
  • @MikeTorrettinni the only thing you should take care is that you have `try...finally` blocks if you need to run any cleanup code (freeing objects and resources) – Dalija Prasnikar Dec 07 '15 at 15:37
  • 1
    @DalijaPrasnikar Well, the helper function must depend on something too, so you are basically packing up a global variable there. But that is not the point, the point is that **only** `Run` procedure depended on the global variable. After your change, one houndred other methods will depend on `NeedToStop`. So how this is an improvement in terms of dependency? – Wodzu Dec 07 '15 at 15:39
  • @DalijaPrasnikar I do have this, when I create objects. When procedure is stopped, I also need to re-set user interface to be ready for another run, so I prefer knowing that it was stopped and I can have all the code to re-set the data, clear arrays, clean user interface, re-set things... in a block of code, that is not within `finally... end;` block. If that's what you meant... – Mike Torrettinni Dec 07 '15 at 15:41
  • No, that is not what I meant. If you are allocating some local objects in your procedures, you have to protect that allocation with try... finally block. I'll update above example code to make it more clear. – Dalija Prasnikar Dec 07 '15 at 15:44
  • 1
    I added local object allocation and cleanup (`TStringList`) in `Import` procedure as example. If you have similar patterns in your procedures then you have to use `try...finally` to make sure your objects will be released if exception is raised. Code inside `finally` block will always execute. – Dalija Prasnikar Dec 07 '15 at 15:48
  • @Wodzu How would you solve dependency issue? I need to be able to know if any of the tasks failed. How to do that without some kind of dependency? – Mike Torrettinni Dec 07 '15 at 15:59
  • @DalijaPrasnikar Thank you, now I know! Because Raise exception will immediateyley exit the flow, unless I have `try finally` block. Very good point! – Mike Torrettinni Dec 07 '15 at 16:02
  • 1
    @MikeTorrettinni You haven't had this dependency in your original code ;) Do you plan to reuse any of the `AfterImport1;` in other parts of program? If yes, then dependency might be a problem. If no, and methods are for private use in the scope of the unit then you don't need to worry about it. Sorry I can not invest more time into this, just think for some time about this answer. Is it really eliminates a lot of IFs, or is it just moving them to lower levels of your code? – Wodzu Dec 07 '15 at 16:32
  • 1
    You have a prettier `Run;` procedure but what about the other parts of code, has it become better after this modification? [Should you control the program flow with exceptions?](http://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control) Grab a copy of _The Pragmatic Programmer_ book if you will have a chance and read how to control the program flow. Good luck:) – Wodzu Dec 07 '15 at 16:32
  • 1
    @Wodzu Exceptions are clean solution for this kind of code. It is longish operation with multiple points of failure. SO question you have linked is talking about quite different situations. Exceptions exist to be used. If they simplify code and make it easier to follow, and don't have performance cost (and they don't in OP's code) then yes they should be used. I am sure that OP's code could be made even better. But code improvements have to be made in small steps, you cannot jump from removing global variable to dependency injection in one step and single SO question. – Dalija Prasnikar Dec 07 '15 at 17:57
  • 1
    @DalijaPrasnikar Sure thing, every language feature can be used in a good or bad way. I've just pointed OP a few things to consider. Personally I think that this example complicates things, because from now on you always need to remember to raise a particular exception and causes not needed dependiences. But thats my opinion, if OP is happy that he sees less IF's (altough the IF's are still there) than its ok. – Wodzu Dec 07 '15 at 18:57
  • @Wodzu, Dalija Prasnikar Great comments, the solution would not be as valuable as it is because of these comments. I will definitely be more cautions in implementing the proposed solution. See the Conclusion so you will understand you didn't just provide a quick solution to my problem, but you did provide very valuable insight into an area I just don't have time to learn on my own - I wish I had time for every time somebody here on SO suggests learning new stuff, it's impossible, but i try my best to understand the suggestions a bit more than just copy&paste it to my app. – Mike Torrettinni Dec 07 '15 at 19:35
3

Similar to RTTI-based example of Jens Borrisholt, but without RTTI. And thus not bound to a single super-object containing all methods.

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. This framework ( and RTTI framework above ) lack any exception control, so if any of import processors would raise some not-caught exception - the execution would jump out of the main process loop without calling clean-up routines. So that means you still have to caught possible exceptions within every actor (fragile) or within the Run procedure. But in the latter case you might totally omit gStop variable and instead raise your custom exception. Personally i'd prefer the exception-based way to Boolean flag. And even EurekaLog might be of use, if your failing afterimport procedure would add to the exception some meaningful message as to why exactly the import was aborted.

PPS. I'd also split gStop into TWO different variables/exceptions: Batch Cancel and Import Abort ones. Then the If fTypeOfData = cMSSQL Then - or any other prerequisite - check could be just the first actor in the batch. Then the batches could be just combined into 2-nd tier array/collection.

I also think EurekaLog would ignore your custom exceptions would you inherit them from 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. You would also maybe want to have a look at http://www.uweraabe.de/Blog/2010/08/16/the-visitor-pattern-part-1/

Focusing how different action-makers are registered and invoked. This might give you some ideas, though it is not exactly your problem.

Another thing to consider might be Multi-cast events like those in Spring4D library.

Arioch 'The
  • 15,005
  • 31
  • 59
  • Thank you, I had no idea this can be done, pretty interesting Batch control. Need to think how it can fit into my application, how it would go along with other code. – Mike Torrettinni Dec 06 '15 at 16:46
  • Turning ImportBatches into `TObjectList` you might somewhat simplify the batches destruction, if you would create batches per-import-task and then free them. Though considering them global flow-control objects, maybe you would create them at app start and never destroy them :-D – Arioch 'The Dec 06 '15 at 17:02
0

The best way to do this is via RTTI

The following is a dummy implementaiton of you problem: 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.

This implementation has the advantage that if you create a AfterImport function it will automatically get called.

Jens Borrisholt
  • 5,641
  • 1
  • 25
  • 56
  • 5
    In my view, the worst way to do this is via RTTI. – David Heffernan Dec 06 '15 at 16:00
  • 1
    Many IoC frameworks use RTTI though @DavidHeffernan And unit-testing libs might be a specific example of those. – Arioch 'The Dec 06 '15 at 16:05
  • @Jens This is area I have no experience with, RTTI. I doubt I would be able to fit it into my code to work all together. Too much unknown for me, right now. – Mike Torrettinni Dec 06 '15 at 18:15
  • Even if you decide to go an other way it would like to show you how this could be done via rtti. Create me a version of your problem with just empty implementation and mail it to me – Jens Borrisholt Dec 06 '15 at 18:25
  • first name at last name dot dk – Jens Borrisholt Dec 06 '15 at 18:26
  • 2
    This particular framework prohibits to have meaningful names for AfterImportXXXX methods and makes it tedious to insert new methods in-between. So in the end you would have to start using attributes instead of IntToStr :-) Not very hard, but yet another trick to learn. – Arioch 'The Dec 06 '15 at 18:33
  • 1
    Ok, this is kind of a "me too" post, but I feel the urge to reiterate Jens's comment - RTTI (and reflection in general) is an incredibly useful tool, although maybe not the best tool for this job. – Vaneik Dec 07 '15 at 14:57
  • 1
    @Vaneik Screwdrivers are great, but not so much with nails – David Heffernan Dec 08 '15 at 07:15
0

Why not to divide procedure into smaller subprocedures? For example:

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;

In this way, your final Run; will have like 15 lines of code:

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;

A lot more readable and a bit easier to maintain.

Wodzu
  • 6,705
  • 8
  • 57
  • 99
  • This is not the workflow I'm looking for, as I think for the main process all main tasks should be visible and accessible from one main process (Run). If I start moving parts into procedures I will have to jump back and forth to see what is going on . All these procedure are already designed by what they do, grouped and named by what the result is. See EDIT 2 in my question. I would like to avoid that. – Mike Torrettinni Dec 07 '15 at 12:35
  • 2
    This is just the same as the code in the question. It's just as bad. – David Heffernan Dec 07 '15 at 14:41
  • I disagree, but you can always propose a better solution. – Wodzu Dec 07 '15 at 14:51
  • It's hard to think of anything worse than this abuse of global variables. – David Heffernan Dec 08 '15 at 07:16
  • Why in plural? There is one global variable and we do not know its scope. It might be in the unit's implementation section. Anyway, the OP did not ask for fixing global variable but for a more readable code. I am not defending a global variable here. – Wodzu Dec 08 '15 at 08:00