Wednesday, November 21, 2007

When code lies - A better solution

Many of you wretched and guffawed at my silly post about Stupid Enumerator Tricks. You know what? I agree. It was a hideous misuse of a feature. One comment from Joe White that stood out was the notion that the code was lying. He is right. I was using a a side-effect as the primary feature, not what the code was actually saying. However, from casually observing the code in question, it could easily be misconstrued. The code was lying.

How would the maintainer of this code figure out the intent a year or more from now? What if I were the maintainer? How many times have you looked at a chunk of code you wrote a long time ago and about all you remember about it was "how utterly awesome and clever you were?" But now, you don't have the faintest idea what it does and how it works. Sure, you could have put comments in, but what if they were wrong and misleading? (this is where I like to say that no comment is far better than a wrong or bad comment). After staring at the code for what seems like hours... you suddenly have a forehead-slapping moment and exclaim, "What was I thinking!!"

But is there a better method of handling the automatic cleanup of this task? Another commenter, Jolyon Smith suggested the use of interfaces as a way to automatically manage the lifetime of an object. I, too, have used this little trick as well. Let's look at my TTask, TTask<T> and TReplicableTask implementation. Here's the declaration of the classes and the interfaces. Yes, I'm using Win32 generics... because, well... they work (for the most part) on my machine :).

  type
TFunctionEvent<T> = function (Sender: TObject): T of object;

ITask = interface
procedure Wait;
procedure Cancel;
function GetIsComplete: Boolean;

property IsComplete: Boolean read GetIsComplete;
end;

ITask<T> = interface(ITask)
function GetValue: T;

property Value: T read GetValue;
end;

EOperationCanceled = class(Exception);

TTask = class(TInterfacedObject, ITask)
private
FDoneEvents: THandleObjectArray;
FCanceled: Boolean;
FException: TObject;
protected
FEvent: TNotifyEvent;
function GetIsComplete: Boolean;
procedure WorkEvent(Sender: TObject);
procedure QueueEvents(Sender: TObject); virtual;
procedure Wait;
procedure Cancel;
property IsComplete: Boolean read GetIsComplete;
public
constructor Create(Sender: TObject; Event: TNotifyEvent);
destructor Destroy; override;
procedure CheckCanceled;
end;

TTask<T> = class(TTask, ITask<T>)
private
FEvent: TFunctionEvent<T>;
FResult: T;
procedure RunEvent(Sender: TObject);
function GetValue: T;
property Value: T read GetValue;
public
constructor Create(Sender: TObject; Event: TFunctionEvent<T>);
end;

TReplicableTask = class(TTask)
protected
procedure QueueEvents(Sender: TObject); override;
end;

So now the usage is like this:


procedure TForm1.Button1Click(Sender: TObject);
var
IntTask: ITask<Integer>;
begin
IntTask := TTask<Integer>.Create(Self, CalcResult);
Caption := Format('Result = %d', [IntTask.Value]);
end;

function TForm1.CalcResult(Sender: TObject): Integer;
var
I: Integer;
begin
Result := 1;
for I := 2 to 9 do
Result := Result * I;
end;

Much cleaner, don't you think? And remember, even if the task isn't complete yet, the call to IntTask.Value does an implicit wait until the task has obtained the value. By making the methods private or protected (or, even better, strict private and protected) on the TTask and TTask<T> classes, you can be more assured that someone doesn't decide to use the object directly. You must obtain the interface in order to use the class instance.


Update: I just updated this Code Central entry with a new Parallel.pas unit that demonstrates some of the above, along with a new TTask, TReplicableTask, and TNestedReplicableTask objects along with requisite interfaces.  A compiled version of the Life demo was also included per some requests.  Finally, the Parallel.pas unit demonstrates a new technique for calling nested procedures without having to litter your code with a bunch of assembler blocks.  I'll post another blog entry describing it.

3 comments:

  1. Be warned that this trick does not work in .Net. In .Net interfaces don't use reference counting and there is no telling as to when the garbage collection kicks in. And if the GC kicks in and cleans up the object the destructor Destroy is not called. in .Net the destructor is only called when Free is explicitly called.

    Nonetheless it's a good trick in win32.

    ReplyDelete
  2. Lars,

    This trick is unecessary in .NET because of the GC.

    Allen.

    ReplyDelete
  3. C Johnson,

    What is wrong about it? It is merely working with the tools that are available at the moment.

    Allen.

    ReplyDelete

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.