Wenn es gefällt, dann habe ich hier noch ein Beispiel zum Einsatz von Interfaces:
Wir sollen in einer Anwendung echte Zahlungen verarbeiten (
ec-Terminal, Paypal, whatever).
Da wir bislang noch nicht wissen, wie das mit dieser Zahlung konkret abgewickelt wird und vor allem mit wem, wir aber die Oberfläche schon bauen wollen (und auch können) fangen wir erst einmal mit einem Interface an:
- Es wird der Betrag übergeben
- Nach Abschluss des Zahlvorgangs wird ein Callback(*) mit einem Response-Objekt aufgerufen. Dort befindet sich dann eine ID des Zahlungsdienstleisters und der bestätigte Betrag.
(*) Ein Callback, weil wir ja nicht wollen, dass uns die
GUI einfriert, wenn die Zahlung überprüft wird
Warum überhaupt ein Interface und keine abstrakte Klasse?
Weil ich über die Implementierung und das Lifetime-Management noch gar nichts sagen kann. Hinter einem Interface kann man alles verstecken, so wie es in dem konkreten Fall nachher benötigt wird.
Also Interface:
Delphi-Quellcode:
unit Services;
interface
uses
System.SysUtils;
type
IGetMoney =
interface
procedure GetMoney(
const Amount: Currency; Callback: TProc<TObject> );
end;
type
TGetMoneyResponse =
class
private
FId :
string;
FAmount: Currency;
public
constructor Create(
const Id:
string;
const Amount: Currency );
property Id:
string read FId;
property Amount: Currency
read FAmount;
end;
type
ServiceLocator =
class sealed
private
class var FMoneyService: TFunc<IGetMoney>;
public
class property MoneyService: TFunc<IGetMoney>
read FMoneyService
write FMoneyService;
end;
implementation
{ TGetMoneyResponse }
constructor TGetMoneyResponse.Create(
const Id:
string;
const Amount: Currency );
begin
inherited Create;
FId := Id;
FAmount := Amount;
end;
end.
Den
ServiceLocator
verwende ich hier, um einen Zugangspunkt zu meinen Services zu haben. Beim Start der Anwendung wird der einmal konfiguriert und dann einfach immer abgefragt.
Das war ja schon mal einfach, da können wir uns gleich an die Oberfläche machen:
Delphi-Quellcode:
unit Forms.MainForm;
interface
uses
Winapi.Windows,
Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics,
Vcl.Controls,
Vcl.Forms,
Vcl.Dialogs,
Vcl.StdCtrls;
type
TMainForm =
class( TForm )
AmountEdit: TEdit;
RequestButton: TButton;
Label1: TLabel;
InfoMemo: TMemo;
procedure RequestButtonClick( Sender: TObject );
procedure AmountEditChange( Sender: TObject );
private
procedure MoneyServiceCallback( AObject: TObject );
procedure GetMoney(
const Amount: Currency );
procedure WaitForMoneyService(
const Amount: Currency );
procedure FinishedMoneyService( );
public
{ Public-Deklarationen }
end;
var
MainForm: TMainForm;
implementation
uses
Services;
{$R *.dfm}
procedure TMainForm.RequestButtonClick( Sender: TObject );
begin
GetMoney( StrToCurr( AmountEdit.Text ) );
end;
procedure TMainForm.AmountEditChange( Sender: TObject );
begin
InfoMemo.Clear;
InfoMemo.Color := clWindow;
end;
procedure TMainForm.FinishedMoneyService;
begin
RequestButton.Enabled := True;
AmountEdit.Enabled := True;
AmountEdit.SetFocus;
end;
procedure TMainForm.GetMoney(
const Amount: Currency );
begin
if Amount <= 0
then
raise EArgumentOutOfRangeException.Create( '
Amount' );
WaitForMoneyService( Amount );
try
// Wir fragen den ServiceLocator
ServiceLocator.MoneyService( ).GetMoney( Amount, MoneyServiceCallback );
except
MoneyServiceCallback( AcquireExceptionObject( ) );
end;
end;
procedure TMainForm.MoneyServiceCallback( AObject: TObject );
begin
try
if AObject
is TGetMoneyResponse
then { Zahlung ist akzeptiert }
begin
InfoMemo.Color := clLime;
InfoMemo.Font.Color := clBlack;
InfoMemo.Lines.Add(
string.Format( '
Accepted %m with Id %s', [ TGetMoneyResponse( AObject ).Amount, TGetMoneyResponse( AObject ).Id ] ) );
end
else { irgendwas ist faul }
begin
InfoMemo.Color := clRed;
InfoMemo.Font.Color := clWhite;
if AObject
is Exception
then { eine Exception }
InfoMemo.Lines.Add(
Exception( AObject ).
Message )
else if Assigned( AObject )
then { irgendwas unerwartetes }
begin
InfoMemo.Lines.Add( '
Unknown Response' );
InfoMemo.Lines.Add( AObject.ToString );
end
else { gar nichts }
InfoMemo.Lines.Add( '
Empty Response' );
end;
finally
AObject.Free;
FinishedMoneyService( );
end;
end;
procedure TMainForm.WaitForMoneyService(
const Amount: Currency );
begin
AmountEdit.Enabled := False;
RequestButton.Enabled := False;
InfoMemo.Clear;
InfoMemo.Color := clYellow;
InfoMemo.Font.Color := clBlack;
InfoMemo.Lines.Add(
string.Format( '
Waiting for %m ...', [ Amount ] ) );
end;
end.
Ist im Prinzip relativ unspektakulär. Der Betrag wird aus dem Edit-Feld gelesen, der Service vom ServiceLocator geholt und wir rufen die Service-Methode mit dem Betrag und dem Callback auf.
Wenn der Callback aufgerufen wird, dann haben wir eine gültige Zahlung oder irgendetwas Komisches.
Protokolliert wird das in dem InfoMemo und die Hintergrundfarbe des Memos zeigt den Abfrage-Status an:
- weiß: noch keine Abfrage gestartet
- gelb: Abfrage läuft
- grün: Zahlung erfolgreich
- rot: Zahlung fehlgeschlagen
Soweit so gut. Jetzt wäre ein kleiner Mini-Fake-Service nett, damit wir sehen können, ob die Oberfläche auch so reagiert, wie wir uns das denken.
Delphi-Quellcode:
unit Services.Impl.Simple;
interface
uses
System.Classes,
System.SysUtils,
Services;
type
TSimpleService =
class( TInterfacedObject, IGetMoney )
public
procedure GetMoney(
const Amount: Currency; Callback: TProc<TObject> );
end;
implementation
{ TSimpleService }
procedure TSimpleService.GetMoney(
const Amount: Currency; Callback: TProc<TObject> );
begin
TThread.CreateAnonymousThread(
procedure
begin
Sleep( 2000 );
TThread.Synchronize(
nil,
procedure
begin
Callback( TGetMoneyResponse.Create( TGUID.NewGuid.ToString( ), Amount ) );
end );
end ).Start;
end;
end.
Einfach einen Thread starten, 2 Sekunden warten und dann synchronisiert den Callback mit der Meldung aufrufen.
Und wie kriegen wir die jetzt zusammen? Mit einer Konfiguration:
Delphi-Quellcode:
unit Configuration;
interface
procedure SimpleConfiguration;
implementation
uses
Services,
Services.Impl.Simple;
procedure SimpleConfiguration;
begin
ServiceLocator.MoneyService :=
function: IGetMoney
begin
Result := TSimpleService.Create;
end;
end;
end.
Und in der
DPR Datei rufen wir einfach diese
procedure
auf
Delphi-Quellcode:
program MoneyThroughWire;
uses
Vcl.Forms,
Forms.MainForm
in '
Forms.MainForm.pas'
{MainForm},
Services
in '
Services.pas',
Services.Impl.Simple
in '
Services.Impl.Simple.pas',
Configuration
in '
Configuration.pas';
{$R *.res}
begin
Configuration.SimpleConfiguration;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.
Das ist ja schon mal ganz nett ... die Oberfläche kann man jetzt testen, ob die bei einem Erfolg alles richtig anzeigt.
Schön wäre ja, wenn man auch die anderen Fälle testen könnte ... dann bauen wir uns doch dafür mal einen Service, mit dem wir das bequem erledigen können:
Wir brauchen ein Formular
Delphi-Quellcode:
unit Forms.GetMoneySimpleDialogForm;
interface
uses
Winapi.Windows,
Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics,
Vcl.Controls,
Vcl.Forms,
Vcl.Dialogs,
Vcl.StdCtrls,
Vcl.ExtCtrls;
type
TGetMoneySimpleDialogForm =
class( TForm )
Label1: TLabel;
Button1: TButton;
Button2: TButton;
RadioGroup1: TRadioGroup;
procedure FormShow( Sender: TObject );
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
var
GetMoneySimpleDialogForm: TGetMoneySimpleDialogForm;
implementation
{$R *.dfm}
procedure TGetMoneySimpleDialogForm.FormShow( Sender: TObject );
begin
Left := Application.MainForm.Left + Application.MainForm.Width + 10;
Top := Application.MainForm.Top;
end;
end.
und einen Service, der mit diesem Formular arbeitet:
Delphi-Quellcode:
unit Services.Impl.SimpleDialog;
interface
uses
System.SysUtils,
Services;
type
TSimpleDialogService =
class( TInterfacedObject
{} , IGetMoney )
private
procedure GetMoney(
const Amount: Currency; Callback: TProc<TObject> );
end;
implementation
uses
System.Classes, Forms.GetMoneySimpleDialogForm, System.UITypes;
{ TSimpleDialogService }
procedure TSimpleDialogService.GetMoney(
const Amount: Currency; Callback: TProc<TObject> );
begin
TThread.CreateAnonymousThread(
procedure
begin
TThread.Synchronize(
nil,
procedure
var
lDialog: TGetMoneySimpleDialogForm;
lDialogResult: Integer;
begin
try
if Amount <= 0
then
begin
raise EArgumentOutOfRangeException.Create( '
Amount out of Range' );
end;
lDialog := TGetMoneySimpleDialogForm.Create(
nil );
try
lDialog.Label1.Caption :=
string.Format( '
%m', [ Amount ] );
lDialog.RadioGroup1.Items.Add( '
Not Accepted' );
lDialog.RadioGroup1.Items.Add( '
Not Valid' );
lDialog.RadioGroup1.Items.Add( '
Something completely different' );
lDialog.RadioGroup1.Items.Add( '
nil' );
repeat
lDialogResult := lDialog.ShowModal;
until ( lDialogResult = mrYes )
or ( ( lDialogResult = mrNo )
and ( lDialog.RadioGroup1.ItemIndex >= 0 ) );
if lDialogResult = mrNo
then
case lDialog.RadioGroup1.ItemIndex
of
0:
raise Exception.Create( '
Not Accepted' );
1:
raise Exception.Create( '
Not Valid' );
2:
Callback( TObject.Create );
3:
Callback(
nil );
else
raise ENotImplemented.CreateFmt( '
Index %d not implemented', [ lDialog.RadioGroup1.ItemIndex ] );
end
else
Callback( TGetMoneyResponse.Create( TGUID.NewGuid.ToString, Amount ) );
finally
lDialog.Free;
end;
except
Callback( AcquireExceptionObject( ) );
end;
end );
end ).Start;
end;
end.
Jetzt noch die Konfiguration anpassen
Delphi-Quellcode:
unit Configuration;
interface
procedure SimpleConfiguration;
procedure SimpleDialogConfiguration;
implementation
uses
Services,
Services.Impl.Simple,
Services.Impl.SimpleDialog;
procedure SimpleConfiguration;
begin
ServiceLocator.MoneyService :=
function: IGetMoney
begin
Result := TSimpleService.Create;
end;
end;
procedure SimpleDialogConfiguration;
begin
ServiceLocator.MoneyService :=
function: IGetMoney
begin
Result := TSimpleDialogService.Create;
end;
end;
end.
Und in der
DPR natürlich auch die richtige
procedure
aufrufen.
Delphi-Quellcode:
begin
// Configuration.SimpleConfiguration;
Configuration.SimpleDialogConfiguration;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.
Schon können wir jeden Fall in der Anwendung testen, obwohl wir noch nicht ein Stück Code zum konkreten ZahlungsService haben.
Ist der konkrete Zahlungs-Service dann fertig, dann wird dieser über die Konfiguration verdrahtet und verwendet.
Die Konfigurationen kann man natürlich auch schön über einen Compiler-Switch auswählen, der z.B. durch eine Build-Konfiguration gesetzt wird. Dann bekommt man z.B. eine Version, wo man nur die Oberfläche testen kann, ohne gleich mehrere tausend Euro über PayPal bewegen zu müssen.
Im Anhang Source und EXE (dort gibt es noch ein NotifyService, der sich einfach mal dazwischen klemmt und während des Zahlungsvorgangs den Betrag in einem Fenster anzeigt).