Einzelnen Beitrag anzeigen

Benutzerbild von Sir Rufo
Sir Rufo

Registriert seit: 5. Jan 2005
Ort: Stadthagen
9.454 Beiträge
 
Delphi 10 Seattle Enterprise
 
#5

AW: [Beispiel] Wie funktionieren Interfaces?

  Alt 4. Feb 2016, 16:14
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).
Angehängte Dateien
Dateityp: zip MoneyThroughWire.zip (915,7 KB, 59x aufgerufen)
Kaum macht man's richtig - schon funktioniert's
Zertifikat: Sir Rufo (Fingerprint: ‎ea 0a 4c 14 0d b6 3a a4 c1 c5 b9 dc 90 9d f0 e9 de 13 da 60)
  Mit Zitat antworten Zitat