Die Lifetime von Instanzen in Delphi will verwaltet sein.
Die RoT (Rule of Thumb) sagt dazu
Zitat:
Wer erzeugt, der gibt auch wieder frei!
Das ist ja alles schön und gut, aber wie geht man damit um, wenn man nun doch eine Instanz von irgendwo her geliefert bekommt und man gar nicht weiß, wie die Verwaltung dort nun vonstatten geht?
Die Ausgangs-Situation ist also, wir haben da eine Klasse
Delphi-Quellcode:
unit Model_FooPoco;
interface
type
TFooPoco =
class
private
FValue:
string;
public
property Value:
string read FValue
write FValue;
end;
implementation
end.
und einen Service, der eine Instanz dieser Klasse liefert:
Delphi-Quellcode:
unit Model_IService;
interface
uses
Model_FooPoco;
type
IService =
interface
[ '
{1BBDDF2F-3074-4FDB-BC5A-32D857FE6B56}' ]
function GetFooPoco: TFooPoco;
end;
implementation
end.
Je nach Implementierung dieses Services, kann diese Instanz von dem verwaltet werden oder eben nicht, und dann ist der Aufrufer verantwortlich.
Hier eine Test-Implementierung dieses Services
Delphi-Quellcode:
unit Model_TestService;
interface
uses
Model_IService, Model_FooPoco;
type
TTestService =
class( TInterfacedObject, IService )
public
function GetFooPoco: TFooPoco;
end;
implementation
{ TTestService }
function TTestService.GetFooPoco: TFooPoco;
begin
Result := TFooPoco.Create;
Result.Value := '
TestFoo';
end;
end.
Ok, da wird eine Instanz erzeugt und einfach zurückgeliefert. Somit ist der Aufrufer für die Freigabe zuständig.
Gut machen wir dann mal:
Delphi-Quellcode:
procedure TForm1.SetLabel( ALabel: TLabel;
const ACaption, AHint:
string );
begin
ALabel.Caption := ACaption;
ALabel.Hint := AHint;
ALabel.ShowHint :=
not AHint.IsEmpty;
end;
procedure TForm1.ServiceGetFooActionExecute( Sender: TObject );
var
LFoo: TFooPoco;
begin
SetLabel( Label1, '
?', '
hole Daten...' );
ServiceGetFooAction.Enabled := False;
try
try
LFoo := FService.GetFooPoco;
try
SetLabel( Label1, LFoo.Value, '
' );
finally
LFoo.Free;
end;
except
on E:
Exception do
begin
SetLabel( Label1, '
Fehler!', E.ToString );
end;
end;
finally
ServiceGetFooAction.Enabled := True;
end;
end;
Da haben wir an alles gedacht, Inhalt der Instanz wird auf der Form angeziegt, Freigabe der Instanz und auch Exceptions werden abgefangen und dem Anwender auf der Form angezeigt.
Allerdings ist es auch eine Menge Code und drei ineinander geschachtelte
try..finally/except
sind vonnöten. Und wehe man ruft die Service-Methode einmal unbedacht auf mit
FService.GetFoo;
(kompiliert einwandfrei) und schon habe ich mir ein Memory-Leak geschaffen.
Gut, das Problem könnte man ja lösen, indem man die Implementierung des Services anders aufbaut und die erzeugten Instanzen dort verwaltet ... man freut sich auf die hoffentlich peinlich genaue Dokumentation, was der Service da intern veranstaltet und dass jede Implemetierung das auch genau so macht, sonst habe ich mal Speicherlecks, oder Speicherfehler.
Geht das auch irgendwie besser?
Ja, geht mit einem Callback:
Delphi-Quellcode:
unit Model_IBetterService;
interface
uses
System.SysUtils,
Model_FooPoco;
type
TResultAction<TResult> = reference
to procedure( AResult: TResult; AException:
Exception );
TObjectResultAction<TResult:
class> = reference
to procedure( AResult: TResult; AException:
Exception;
var ADispose: Boolean );
IBetterService =
interface
[ '
{4FE27425-4312-4551-9EDD-5CE0BAF4AFBE}' ]
procedure GetFoo( callback: TResultAction<TFooPoco> );
overload;
procedure GetFoo( callback: TObjectResultAction<TFooPoco> );
overload;
end;
implementation
end.
Wir bieten jetzt für die Instanz zwei Methode an. Die
TResultAction<TResult>
kann alles zurückliefern und
TObjectResultAction<TResult: class> = reference to procedure( AResult: TResult; AException: Exception; var ADispose: Boolean );
liefert nur Klassen-Instanzen, mit einem
ADispose
Argument, wo man bei der Verarbeitung angeben kann, ob man sich um die Freigabe der Instanz selber kümmern möchte.
Hat man jetzt einen Service, der die Instanzen selber verwaltet, dann bietet man auch nur den Callback
TResultAction<TResult>
an und wenn der Service die Verwaltung auch abgeben kann, dann wird auch der Callback
TObjectResultAction<TResult: class>
angeboten.
Lustig ist jetzt, dass man dadurch die interne Arbeitsweise des Services nach aussen dokumentiert hat.
Man beachte auch das Argument
AException : Exception
. Wird während der Verarbeitung der Anfrage eine
Exception ausgelöst, dann wird diese über den Callback zurückgeliefert. Damit spart man sich alle
try..finally/except
auf der Aufrufer-Seite.
Dann verwenden wir diesen neuen Service doch einmal:
Delphi-Quellcode:
procedure TForm1.BetterServiceGetFooActionExecute( Sender: TObject );
begin
SetLabel( Label2, '
?', '
hole Daten...' );
BetterServiceGetFooAction.Enabled := False;
FBetterService.GetFoo(
procedure( AResult: TFooPoco; AException:
Exception )
begin
if Assigned( AException )
then
SetLabel( Label2, '
Fehler!', AException.ToString )
else
SetLabel( Label2, AResult.Value, '
' );
BetterServiceGetFooAction.Enabled := True;
end );
end;
Das ist ja schon mal viel übersichtlicher ...
Und die Implementierung von dem Service? Hier:
Delphi-Quellcode:
unit Model_BetterService;
interface
uses
System.SysUtils,
Model_IService,
Model_IBetterService,
Model_FooPoco;
type
TBetterService =
class( TInterfacedObject, IBetterService )
private
FService: IService;
public
constructor Create( AService: IService );
procedure GetFoo( callback: TResultAction<TFooPoco> );
overload;
procedure GetFoo( callback: TObjectResultAction<TFooPoco> );
overload;
end;
implementation
{ TBetterService }
procedure TBetterService.GetFoo( callback: TResultAction<TFooPoco> );
begin
GetFoo(
procedure( AResult: TFooPoco; AException:
Exception;
var ADispose: Boolean )
begin
callback( AResult, AException );
end );
end;
constructor TBetterService.Create( AService: IService );
begin
inherited Create;
FService := AService;
end;
procedure TBetterService.GetFoo( callback: TObjectResultAction<TFooPoco> );
var
LFoo: TFooPoco;
LDispose: Boolean;
begin
LFoo :=
nil;
LDispose := True;
try
try
LFoo := FService.GetFooPoco;
except
on E:
Exception do
begin
callback(
nil, E, LDispose );
Exit;
end;
end;
callback( LFoo,
nil, LDispose );
finally
if LDispose
then
LFoo.Free;
end;
end;
end.
Da ich faul war, habe ich einfach mal den alten Service mit dem neuen Service gewrappt. Genau das benötigt man auch, wenn man von der alten Variante auf die neue umsteigen möchte, ohne den ganzen Code sofort auf den Kopf stellen zu müssen.
War es das schon?
Nein, ich als Thread-Fetischist habe da noch einen Pfeil im Köcher:
Der Test-Service (s.o.) ist ja ein idealer Fall, der in der Realität eher seltener vorkommt. Meistens ist die Beschaffung von einer Instanz etwas aufwändiger, bzw. kann irgendwann aufwändiger werden. Heute kommen die Daten direkt von der Festplatte, morgen aus der Datenbank und übermorgen von einem WebService. Und die Antwortzeiten können schwanken (von blitzschnell bis ar***lahm).
Hier habe ich mal einen Service geschrieben, der ein wenig mehr Zeit vertrödelt:
Delphi-Quellcode:
unit Model_Service;
interface
uses
Model_IService, Model_FooPoco;
type
TService =
class( TInterfacedObject, IService )
public
function GetFooPoco: TFooPoco;
end;
implementation
uses
System.SysUtils;
{ TService }
function TService.GetFooPoco: TFooPoco;
begin
// Der reale Dienst benötigt zwei bis fünf Sekunden zum Bereitstellen der Daten
Sleep( Random( 3000 ) + 2000 );
Result := TFooPoco.Create;
Result.Value := '
Foo';
end;
end.
Jetzt wäre es ja schön, wenn die Abfrage in einem Thread laufen würde.
Eben und mit dem Callback ist genau das kein Problem mehr, wir haben schon alles vorbereitet und brauchen nur die Service-Implementierung ändern:
Delphi-Quellcode:
unit Model_BestService;
interface
uses
Model_IService, Model_IBetterService, Model_FooPoco;
type
TBestService =
class( TInterfacedObject, IBetterService )
private
FService: IService;
public
constructor Create( AService: IService );
procedure GetFoo( callback: TResultAction<TFooPoco> );
overload;
procedure GetFoo( callback: TObjectResultAction<TFooPoco> );
overload;
end;
implementation
uses
System.Classes, System.SysUtils;
{ TBestService }
constructor TBestService.Create( AService: IService );
begin
inherited Create;
FService := AService;
end;
procedure TBestService.GetFoo( callback: TResultAction<TFooPoco> );
begin
GetFoo(
procedure( AResult: TFooPoco; AException:
Exception;
var ADispose: Boolean )
begin
callback( AResult, AException );
end );
end;
procedure TBestService.GetFoo( callback: TObjectResultAction<TFooPoco> );
begin
TThread.CreateAnonymousThread(
procedure
var
LFoo: TFooPoco;
LDispose: Boolean;
LException: TObject;
begin
LFoo :=
nil;
LDispose := True;
try
try
LFoo := FService.GetFooPoco;
except
LException := ExceptObject;
TThread.Synchronize(
nil,
procedure
begin
callback(
nil, LException
as Exception, LDispose );
end );
Exit;
end;
TThread.Synchronize(
nil,
procedure
begin
callback( LFoo,
nil, LDispose );
end );
finally
if LDispose
then
LFoo.Free;
end;
end ).Start;
end;
end.
Und wie sieht jetzt der Aufruf aus?
Delphi-Quellcode:
procedure TForm1.BestServiceGetFooActionExecute( Sender: TObject );
begin
SetLabel( Label3, '
?', '
hole Daten...' );
BestServiceGetFooAction.Enabled := False;
FBestService.GetFoo(
procedure( AResult: TFooPoco; AException:
Exception )
begin
if Assigned( AException )
then
SetLabel( Label3, '
Fehler!', AException.ToString )
else
SetLabel( Label3, AResult.Value, '
' );
BestServiceGetFooAction.Enabled := True;
end );
end;
Aha, der ändert sich ja gar nicht ...
Eben, während der Entwicklungsphase arbeitet man mit einem Test-Service der ratz-fatz Dummy-Daten ausliefert. Der echte Service holt dann die echten Daten innerhalb eines Threads ab.
Eins fehlt noch, die Übernahme der Instanz-Verwaltung:
Delphi-Quellcode:
procedure TForm1.BestServiceGetFooToListActionExecute( Sender: TObject );
begin
SetLabel( Label4, '
?', '
hole Daten...' );
BestServiceGetFooToListAction.Enabled := False;
FBestService.GetFoo(
procedure( AResult: TFooPoco; AException:
Exception;
var ADispose: Boolean )
begin
if Assigned( AException )
then
SetLabel( Label4, '
Fehler!', AException.ToString )
else
begin
SetLabel( Label4, '
', '
' );
FFooList.Add( AResult );
ADispose := False;
// wir übernehmen die Kontrolle
end;
BestServiceGetFooToListAction.Enabled := True;
end );
end;
Das gesamte Projekt mit Source und Kompilat befindet sich im Anhang
cu
Sir Rufo