Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Cleancode, Dependency Injection oder wie stelle ich mich richtig an (https://www.delphipraxis.net/180344-cleancode-dependency-injection-oder-wie-stelle-ich-mich-richtig.html)

Ralle1 13. Mai 2014 12:51

Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Hallo!

Ich entwickle z.Z. eine Komponete. Es handelt sich um ein Pagecontrol, bei dem man Tabs verschieben oder ausblenden kann. Für meine Fragestellung eigentlich nebensächlich, entscheidend ist nur, dass die Einstellungen gespeichert werden können und so dauerhaft bestehen bleiben.

Die Komponente arbeitet mit einer Klasse in der die Speicherung der Einstellungen ausgelagert ist. Zur eindeutigen Speicherung (an einem zentralen Ort, Datenbank) benötige ich u.a. eine User-ID aus der Anwendung. Die Anwendung selbst arbeitet mit einer globalen Variabel, in der diese ID abgelegt ist und so an allen Programmstellen verfügbar ist.

Die Frage ist nun: Wie ermittle ich am besten die UserID innerhalb der Komponentenklasse, ohne diese dauerhaft mit meiner Anwendung und der o.g. globalen Variabel zu koppeln?

Wichtig ist mir dabei, die saubere Trennung zw. Komponente und Anwendung trotzdem zu halten und sämtliche SOLID-Prinzipien (http://de.wikipedia.org/wiki/Prinzip...ierten_Designs) sollen dabei eingehalten werden.

Nicht gewünscht ist, jedem einzelnen Pagecontrol-Objekt von außen die ID mitzuteilen, da dieses an unzähligen Programmstellen eingesetzt werden soll.

Danke für ein Feedback.

Mavarik 13. Mai 2014 12:59

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Eine OnGetUserID Procedure in das Object einfügen..

Mavarik

Ralle1 13. Mai 2014 13:05

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
und dann?
Ich meine, wenn es eine Event-Zuweisung werden soll, dann muss ich dies Event "OnGetUserId" ja bei jedem Object zuweisen. Das möchte ich nicht aufgrund der vielzahl an Objekten.

stahli 13. Mai 2014 13:07

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Mir fallen 3 Varianten ein, die aber alle letztlich auf das Gleiche hinaus laufen.

1) globale Variable "pcUserId" in der PageControl-Unit anlegen und bei der Initialisierung des Projektes zuweisen.
2) das gleiche als Klassenvariable (weiß nicht, ab welchem Delphi das unterstützt wird)
3) eine Funktionsvariable (oder wie heißt das?) definieren, der man bei der Projektinitialisierung eine Funktion zur Ermittlung der UserId zuweist.


(PS: schreib mal Deine Delphi-Version in Dein Profil)

TiGü 13. Mai 2014 13:08

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Hm, irgendwie habe ich das Gefühl das wichtige Informationen oder zumindest ein kleines Codebeispiel vom Ist-Zustand fehlen.

Auf jeden Fall sollte deine visuelle Komponente NICHT diese ID kennen, sondern nur deine Klasse, die für das speichern zuständig ist.

Ralle1 13. Mai 2014 13:18

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Delphi Version XE3.

Hier ein kleines Codebeispiel (ich war davon ausgegangen, dass dies nicht mehr Aussagekraft hat, als mein engehender Text).
Den realen Code kann ich aufgrund des Umfangs schlecht hier unterbringen:

Delphi-Quellcode:
type
  TFormMain = class(TForm)
    PageControlMain: TMyPageControl;
    TabSheet1: TMyTabSheet;
    TabSheet2: TMyTabSheet;
    TabSheet3: TMyTabSheet;
    procedure FormCreate(Sender: TObject);
  private
    { Private-Deklarationen }

  public
    { Public-Deklarationen }
  end;

var
  FormMain: TFormMain;
  fUserID : integer; //globale User ID

implementation

{$R *.dfm}

procedure TFormMain.FormCreate(Sender: TObject);
begin
  fUserID := GetGlobalUserID; //globale User ID wird von wo auch immer gesetzt bei Programmstart
end;

(******************************************************************************)

type
  TMyPageControl = class(TComponent)
  private
    { Private-Deklarationen }
    Settings : TControlSettings;
  public
    { Public-Deklarationen }
  end;

(******************************************************************************)

type
  TControlSettings = class
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
    procedure SaveSettingstoDatabase;
  end;


implementation

procedure TControlSettings.SaveSettingstoDatabase;
begin
  //Hier wird die globale UserID benoetigt
end;

(******************************************************************************)

stahli 13. Mai 2014 13:26

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Eine einfache Lösung wäre folgende:

Delphi-Quellcode:
type
   TControlSettings = class
   private
     { Private-Deklarationen }
   public
     { Public-Deklarationen }
     class var UserId: Integer;
     procedure SaveSettingstoDatabase;
   end;

...

procedure TFormMain.FormCreate(Sender: TObject);
begin
  fUserID := GetGlobalUserID; //globale User ID wird von wo auch immer gesetzt bei Programmstart
  TControlSettings.UserId := fUserId;
end;

himitsu 13. Mai 2014 13:28

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Man kann das Event auch als Class-Property oder über eine Register-Klassenprozedur integrieren, welche den Callback allen Instanzen/Nachkommen der Klasse zur Verfügung stellt.
[edit] siehe #7 und das in der Klasse kommt zwar auf's "Gleiche" raus, wie das in #6, aber diese globalen Variablen sollte man meistens gleich verbieten. [/edit]

Falls kein Callbck angegeben wurde, könnte intern problemlos zumindestens eine ID aus GetUserName+GetComputerName erzeugt werden.
Irgendwie müsste es auch möglich sein die BenutzerID (GUID) des Benutzerkontos vom Windows zu erfragen. (die wäre theoretisch weltweit eindeutig)

TiGü 13. Mai 2014 13:31

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Einfach in verschiedene Units aufteilen?!? :gruebel:

Delphi-Quellcode:
unit Component;

interface

uses
  System.Classes,
  SettingsSaver;

type
  TMyPageControl = class(System.Classes.TComponent)
    private
      FSettings : TControlSettings;
  end;

implementation

end.
Delphi-Quellcode:
unit SettingsSaver;

interface

uses
  System.Classes;

type
  TControlSettings = class(TObject)
  strict private
    type
      TYourDatabase = class(TObject)
        function Save(AUserID : Integer) : Boolean;
      end;
  private
    FDatabase : TYourDatabase;
  public
    function SaveSettingstoDatabase : Boolean;
  end;

implementation

uses
  User;

function TControlSettings.SaveSettingstoDatabase : Boolean;
begin
  Result := FDatabase.Save(User.GetUserID)
end;

function TControlSettings.TYourDatabase.Save(AUserID : Integer) : Boolean;
begin
  Result := AUserID > 0;
end;

end.
Delphi-Quellcode:
unit User;

interface

function GetUserID : Integer;

implementation

function GetUserID : Integer;
begin
  Result := 1234;
end;

end.

Uwe Raabe 13. Mai 2014 13:53

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Ralle1 (Beitrag 1258749)
Delphi-Quellcode:
procedure TControlSettings.SaveSettingstoDatabase;
begin
  //Hier wird die globale UserID benoetigt
end;

Du könntest hier den Zugriff auf die Database über einen Wrapper realisieren, der die Zugriffe auf die UserID vor TControlSettings verbirgt. TControlSettings arbeitet dann mit einer abstrakten Klasse. Die davon abgeleitete Klasse bekommt dann die UserID mit.

Aber da aus deinem Code die tatsächliche Verwendung der UserID nicht hervorgeht (insofern bringt er wirklich nicht mehr als deine ursprüngliche Beschreibung), kann man halt auch keine genaueren Hinweise geben.

Ralle1 13. Mai 2014 14:06

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Danke erstmal für das zahlreiche Feedback :thumb:
Daraus fasse ich folgende Lösung für mich als am sinnvollsten zusammen:
Delphi-Quellcode:
type
   TApplicationUser = class
   private
     { Private-Deklarationen }
   public
     { Public-Deklarationen }
     class var UserId: Integer;
   end;

...

type
   TControlSettings = class
   private
     { Private-Deklarationen }
     User : TApplicationUser;
   public
     { Public-Deklarationen }
     procedure SaveSettingstoDatabase;
   end;

...

procedure TFormMain.FormCreate(Sender: TObject);
begin
  fUserID := GetGlobalUserID; //globale User ID wird von wo auch immer gesetzt bei Programmstart
  TApplicationUser.UserId := fUserId;
end;

Allerdings habe ich dabei den Ansatz von Uwe noch nicht beachtet, weil ich ihn noch nicht genau verstanden habe.
Um die Dir fehlende Info nachzureichen:
In der Methode "SaveSettingsToDatabase" sollen die Einstellungen dann in die Datenbank geschoben werden, also Query-Objekt erzeugen und ein Update/Insert auf eine Tabelle abfeuern.
Die UserId bildet dabei den Foreignkey. Über eine weitere Methode Methode "LoadSettingsFromDatabase" wird dieser Datensatz später wieder ausgelesen

Sir Rufo 13. Mai 2014 14:18

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Also du hast da irgendwelche Komponenten und da sollen Einstellungen gelesen und geschrieben werden.

Du brauchst also eine
Delphi-Quellcode:
TReader
und
Delphi-Quellcode:
TWriter
Klasse, die abstrakt Eigenschaften lesen und schreiben können. Mehr muss die Komponente doch gar nicht wissen, wohin und mit wem kann der doch egal sein.

Delphi-Quellcode:
TMyComponent = class( TComponent )
public
  procedure ReadSettings( AReader : TReader );
  procedure WriteSettings( AWriter : TWriter );
end;
Dann benötigst du eine abstrakte
Delphi-Quellcode:
TSettingsStore
Klasse, die eine
Delphi-Quellcode:
TReader
und
Delphi-Quellcode:
TWriter
-Instanz zur Verfügung stellt.

Jetzt werden wir konkreter:

Du leitest dir von
Delphi-Quellcode:
TSettingsStore
eine Klasse ab, die mit einer Datenbank kommunizieren kann und auch eine UserID aufnehmen kann.

Ralle1 13. Mai 2014 15:07

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
dieser Ansatz setzt allerdings voraus, dass
Delphi-Quellcode:
ReadSettings
und
Delphi-Quellcode:
WriteSettings
von außen aufgerufen wird, oder?
Delphi-Quellcode:
procedure TFormMain.FormCreate(Sender: TObject);
begin
  ...
  MyPageControl.ReadSettings(SettingsStore.Reader);
  ...
end;

procedure TFormMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ...
  MyPageControl.WriteSettings(SettingsStore.Writer);
  ...
end;
Dies ist nämlich nicht der Fall. Die Komponente kümmert sich eigenständig um das Auslösen dieser Ereignisse.

Sir Rufo 13. Mai 2014 15:15

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Und wo ist jetzt das Problem daraus Events zu machen und entsprechend zu verdrahten?

Irgendwo musst du ansetzen und etwas übergeben, ansonsten legst du dich starr fest.
Wo ist egal, und wenn die Komponenten einfach ein "Singleton" ansprechen und sich darüber die Instanz zum Speichern/Laden holen.

Diesem Singleton übergibst du dann einfach beim Start der Anwendung die konkrete Instanz und gut.

Also mal ganz simpel:
Delphi-Quellcode:
unit MyUserSettings;

interface

type
  IUserSettingsStorage = interface
    ['{F3E5657B-39EF-4FA1-A601-8AFCEE50D6D1}']
    procedure WriteString( const AName, AValue : string );
    function ReadString( const AName, ADefault : string ) : string;
  end;

  TUserSettings = class
  private
    class var _Storage : IUserSettingsStorage;
  private
    class function GetStorage : IUserSettingsStorage; static;
    class procedure SetStorage( const Value : IUserSettingsStorage ); static;
  public
    class property Storage : IUserSettingsStorage read GetStorage write SetStorage;
  end;

implementation

{ TUserSettings }

class function TUserSettings.GetStorage : IUserSettingsStorage;
begin
  // Wenn _Storage NIL, dann könnte man auch eine DUMMY/NULL-Instanz zurückgeben, die einfach nichts macht
  // dann spart man sich die Überprüfung, ob es eine Instanz gibt
  Result := _Storage;
end;

class procedure TUserSettings.SetStorage( const Value : IUserSettingsStorage );
begin
  _Storage := Value;
end;

end.
und in der Komponente dann
Delphi-Quellcode:
interface

type
  TMyComponent = class(...)
  protected
    procedure DoLoadUserSettings;
    procedure DoSaveUserSettings;
  end;

implementation

uses
  MyUserSettings;

procedure TMyComponent.DoSaveUserSettings;
begin
  TUserSettings.Storage.WriteString( 'Name', Name );
end;

procedure TMyComponent.DoLoadUserSettings;
begin
  Name := TUserSettings.Storage.ReadString( 'Name', Name );
end;
Es gibt da natürlich noch eine ganze Bandbreite an Spielmöglichkeiten.

Die Storage gibt mir eine spezielle Instanz für die Komponente zurück
Delphi-Quellcode:
TUserSettings.Storage('TForm1.PageControl1').WriteString('Name',Name);
etc., etc.

Ralle1 13. Mai 2014 16:33

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Das hört sich sehr gut an. So werde ich es mal angehen. Vielen dank an alle für das Feedback!!!

Stevie 13. Mai 2014 20:03

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Ralle1 (Beitrag 1258736)
Die Komponente arbeitet mit einer Klasse in der die Speicherung der Einstellungen ausgelagert ist. Zur eindeutigen Speicherung (an einem zentralen Ort, Datenbank) benötige ich u.a. eine User-ID aus der Anwendung.

Imo ist die Annahme, dass die Einstellungen eine UserID brauchen schonmal eine aufgezwungene Kopplung, die nicht notwendig ist. Die Komponente interessieren die Einstellungen, ob die einem User zugehörig oder in ne Ini Datei auf C geschrieben werden, ist unerheblich. Dementsprechend sollte die Komponente nur die Settings Schnittstelle/Instanz interessieren, aus der es befüttert wird.

@Sir Rufo:
Naja, ist immer noch gekoppelt, nur nett als Klasse mit class var verpackt, dass es ein global State ist, ändert sich nicht. Imo kann man doch locker ein Settings Objekt/Komponente basteln, die man an das PageControl, was die Settings gerne hätte heften kann, genau wie man normalerweise Komponenten aneinander steckt. Dann hat man sogar den Vorteil, dass man übermorgen, wenn mal jemand die verrückte Idee hat 2 verschiedene PageControls unterschiedlich zu konfigurieren, nicht dumm da steht.

P.S.: Weil ich das Video zu dem Thema so gut finde und so gerne verlinke: https://www.youtube.com/watch?v=-FRm3VPhseI&t=1s ;)

Dejan Vu 13. Mai 2014 20:44

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Stevie (Beitrag 1258802)
Imo ist die Annahme, dass die Einstellungen eine UserID brauchen schonmal eine aufgezwungene Kopplung, die nicht notwendig ist.

Die Einstellungen benötigen das schon, sofern wir beide von den gleichen 'Einstellungen' reden. Ich definiere 'Einstellungen' als 'Settings' und so wie der Terminus hier verwendet wird, scheint es sich um den Kontext zu handeln, der die Properties der Komponente liest und schreibt und *nicht* um die Properties der Komponente (so wie Du das vielleicht definierst).

Die Komponente an sich braucht und darf die UserId nicht kennen, denn sonst könnte sie ohne nicht leben. Ich würde sogar soweit gehen, das 'LoadFrom(aReader : TReader)' und 'SaveTo(aWriter : TWriter)' aus der Komponente zu entfernen. Die Komponente muss nicht lesen und schreiben können. Wir Menschen können ja auch Bäume fällen, Brote essen und an Blümchen Spaß haben, ohne lesen und schreiben zu können (nur der Wille zum neuen Leben als Holzfäller ist hier maßgebend).

Insofern würde ich die Komponente Komponente sein lassen, schlank und so, wie GottProgrammierer sie schuf.

Und wenn nun irgendwer meint, die Eigenschaften irgendwohin speichern zu müssen, soll er das doch tun. Das schreit dann geradezu nach einer TReader und TWriterFactory, sodaß diese Factory für jedes Control den passenden Reader/Writer aus dem Hut zaubert.

Und da wir die Factory wieder über eine Factory erzeugen lassen können, können wir hier dann unsere speziellen DB-ReaderWriter über eine entsprechende Factory erzeugen lassen. D.h. ich habe dann die Freiheit, heute mal in die Registry und morgen von mir aus in die DB (oder eine Textdatei oder oder oder) zu speichern, wobei nur bei der DB-Variante wirklich ausnahmsweise eine UserID nötig wäre. Bei anderen benötigen wir vielleicht irgendeinen Pfad, oder einen Registry-Schlüssel etc. Das ist dann komplett entkoppelt und die spezielle DB-ReaderWriter-Factory kennt dann die UserID und gibt sie bei der Produktion einer Klasse dieser eben mit. Und somit wäre auch das don't-use-globals gelöst, denn global ist die User-ID dann ja nun nicht mehr.

Ralle1 14. Mai 2014 08:46

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Liste der Anhänge anzeigen (Anzahl: 1)
Die letzten Antworten haben mich nun zugegebener Weise mehr verwirrt als geholfen. Das liegt aber wohl zuletzt daran, dass ich noch nicht ganz folgen kann.

Ich habe mir mal die Mühe gemacht, das Problem in einem Demoprojekt nachzubauen. Siehe Anhang.
Dort gibt es:

1. Anwendung: das RememberProject besteht aus den Formularen Main und Detail. Beim Start des FormMain wird quasi als Login eine UserId abgefragt und global gespeichert. Dies simuliert an der Stelle den Bestandscode. Sicher nicht ganz optimal, aber halt der Ist-Zustand.
Das FormDetail bietet nun als Komponente ein Edit (TRememberEdit) das sich je UserId den eingegebenen Text merkt (ganz tolles Feature :-) ).

2. Komponente: im RememberPackage ist die o.g. TRememberEdit Komponente enthalten. Über die Klasse TControlSettings wird das speichern/lesen realisiert. Diese wiederum arbeitet mit TApplicationUser um von dort die User-Id zu holen.

Soweit mein Lösungsansatz. Jetzt würde mich interessieren, ob mir jemand die „saubere“ Lösung als Alternative präsentieren kann.

@Stevie: besonders Deine Lösung würde mich interessieren! Deine Denkansätze beim Delphi-Code-Camp haben mir grundsätzlich gut gefallen, nur hapert es mir hier im konkreten Beispiel bei der Umsetzung...

Vielen Dank!

TiGü 14. Mai 2014 09:22

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Wenn du auf die globale Variable der User-ID im Hauptformular (Main.pas) verzichtest und im Unterformular (Detail.pas) nicht das Hauptformular inkludierst, sondern gleich ApplicationUser, wird das eine runde Sache.
Außerdem kann man RememberEdit noch ein bisschen straffen, indem man die Settings-Klasse als Member-Variable anlegt.
So kommt es wegen jeder kleinen Änderung nicht ständig zum Erschaffen und Zerstören der TControlSettings-Klasse.
Sicher gibt es noch ein paar andere und ausgefallende Ansätze, aber im Wesentlichen hast du damit erreicht was du wolltest: Die Komponente kennt nicht mehr die UserID.
Ggf. könnte man die UserID per Property o.ä. an das Unterformular (Detail.pas) übergeben, dann muss man hier auch nichts mehr inkludieren. Also weder Main noch ApplicationUser.

Delphi-Quellcode:
unit Main;

interface

uses
  System.SysUtils,
  System.Classes,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.StdCtrls;

type
  TFormMain = class(TForm)
    Button1 : TButton;
    procedure Button1Click(Sender : TObject);
    procedure FormShow(Sender : TObject);
  private
    function GetUserId : integer;
    function CheckUserId : boolean;
  public
  end;

var
  FormMain : TFormMain;

implementation

{$R *.dfm}


uses
  Detail,
  ApplicationUser;

procedure TFormMain.Button1Click(Sender : TObject);
var
  FormDetail : TFormDetail;
begin
  FormDetail := TFormDetail.Create(Self);
  try
    FormDetail.ShowModal;
  finally
    FreeAndNil(FormDetail);
  end;
end;

function TFormMain.CheckUserId : boolean;
begin
  Result := TApplicationUser.UserId >= 0;
  if not Result then
  begin
    MessageDlg('UserID ungültig', mtError, [mbOk], 0);
  end;
end;

procedure TFormMain.FormShow(Sender : TObject);
begin
  TApplicationUser.UserId := GetUserId;
  if not CheckUserId then
    Application.Terminate;

  Self.Caption := 'Login mit UserID=' + IntToStr(TApplicationUser.UserId);
end;

function TFormMain.GetUserId : integer;
var
  UserString : string;
begin
  Result := 0;
  UserString := InputBox('User-ID', 'Bitte die User-ID eingeben:', '0');
  Result := StrToIntDef(UserString, 0);
end;

end.
Delphi-Quellcode:
unit Detail;

interface

uses
  System.SysUtils,
  System.Classes,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.StdCtrls,
  RememberEdit;

type
  TFormDetail = class(TForm)
    Label1 : TLabel;
    RememberEdit1 : TRememberEdit;
    lblHallo : TLabel;
    procedure FormCreate(Sender : TObject);
  private
  public
  end;

implementation

{$R *.dfm}

uses
  ApplicationUser;

procedure TFormDetail.FormCreate(Sender : TObject);
begin
  lblHallo.Caption := lblHallo.Caption + ' ' + IntToStr(ApplicationUser.TApplicationUser.UserId);
end;

end.
Delphi-Quellcode:
unit RememberEdit;

interface


uses
  SysUtils,
  WinTypes,
  WinProcs,
  Messages,
  Classes,
  Vcl.StdCtrls,
  ControlSettings;

type
  TRememberEdit = class(TEdit)
  private
    FControlSettings : TControlSettings;
    procedure SetTextToSettings(const Value : string);
    function GetTextFromSettings : string;
  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('RememberTest', [TRememberEdit]);
end;

constructor TRememberEdit.Create(AOwner : TComponent);
begin
  inherited;
  FControlSettings := TControlSettings.Create;
  Self.Text := GetTextFromSettings;
end;

destructor TRememberEdit.Destroy;
begin
  SetTextToSettings(Self.Text);
  FControlSettings.Free;
  inherited;
end;

function TRememberEdit.GetTextFromSettings : string;
begin
  Result := '';
  if (csDesigning in ComponentState) then
    exit;
  Result := FControlSettings.Read;
end;

procedure TRememberEdit.SetTextToSettings(const Value : string);
begin
  if (csDesigning in ComponentState) then
    exit;
  FControlSettings.Write(Value);
end;

end.

Dejan Vu 14. Mai 2014 10:49

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Delphi-Quellcode:
... finally
     FreeAndNil(FormDetail);
   end;
Ich würde da 'Release' verwenden. Soweit ich mich erinnere, ist das der bevorzugte Weg, ein Windows-Formular freizugeben.

Und nochwas: Die Logik von CheckUserId gehört nicht ins Login/Hauptformular, imho.

Uwe Raabe 14. Mai 2014 11:09

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Dejan Vu (Beitrag 1258852)
Delphi-Quellcode:
... finally
     FreeAndNil(FormDetail);
   end;
Ich würde da 'Release' verwenden. Soweit ich mich erinnere, ist das der bevorzugte Weg, ein Windows-Formular freizugeben.

Das ist eigentlich nur notwendig, wenn die Freigabe innerhalb eines Events des Forms erfolgt. Man würde sich ja sonst selbst die Füße wegschießen.

Allerdings genügt in diesem Fall auch ein simples
Delphi-Quellcode:
FormDetail.Free;

Dejan Vu 14. Mai 2014 12:33

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1258854)
Das ist eigentlich nur notwendig, wenn die Freigabe innerhalb eines Events des Forms erfolgt.

Meine Erfahrung besagt, das das auch aus anderen Events heraus ein Problem ist. Aber vielleicht ist das Aberglaube.

Stevie 14. Mai 2014 13:01

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Liste der Anhänge anzeigen (Anzahl: 1)
Zitat:

Zitat von Ralle1 (Beitrag 1258829)
@Stevie: besonders Deine Lösung würde mich interessieren! Deine Denkansätze beim Delphi-Code-Camp haben mir grundsätzlich gut gefallen, nur hapert es mir hier im konkreten Beispiel bei der Umsetzung...

Danke - ich freu mich, wenn ich zumindest einen kleinen Denkanstoß geben konnte :)

Ist noch nicht die 100%ige Lösung, da mir die Abhängigkeit vom Detailform auf das Mainform noch nicht gefällt (siehe Kommentar im Source).
Außerdem wird durch das Fehlen einer Abstrakten Settings Klasse noch die konkrete Implementierung vom TRememberEdit benötigt - da sollte man dann noch eine abstrakte Klasse implementieren (ich weiß grad nicht, ob man auch Interfaces im OI verdrahten kann).

Den noch etwas unsauberen Code im Mainform hab ich mal so belassen, denn darum ging es ja nicht hauptsächlich.

TiGü 14. Mai 2014 13:30

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Stevie (Beitrag 1258879)
Ist noch nicht die 100%ige Lösung, da mir die Abhängigkeit vom Detailform auf das Mainform noch nicht gefällt (siehe Kommentar im Source).
Außerdem wird durch das Fehlen einer Abstrakten Settings Klasse noch die konkrete Implementierung vom TRememberEdit benötigt - da sollte man dann noch eine abstrakte Klasse implementieren (ich weiß grad nicht, ob man auch Interfaces im OI verdrahten kann).

Den noch etwas unsauberen Code im Mainform hab ich mal so belassen, denn darum ging es ja nicht hauptsächlich.

Warum kennt bei dir die Mainform und die Detailform die ControlSettings für das RememberEdit?
Geht es nur darum, dass fancy im OI zu verdrahten?
Da fand ich meine Lösung sauberer.

Dejan Vu 14. Mai 2014 13:31

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Stevie (Beitrag 1258879)
Ist noch nicht die 100%ige Lösung, da mir die Abhängigkeit vom Detailform auf das Mainform noch nicht gefällt (siehe Kommentar im Source).

a) Entkoppel doch einfach die Settings komplett von der Komponente
a.1) Lass das TFormDetail.FormCreate ein
Delphi-Quellcode:
Settings.Load(RememberEdit)
ausführen
a.2) Schreib eine TSettings-Komponente, klatsch die auf das TFormDetail und füge die 'RememberEdit'-Komponente der TSettings-Komponente zu (so macht das DevExpress mit seinem TPropertyStore oder wie das Teil heißt).
b) So, wie Du das schon angedacht hast: Settings => Datamodule

Ich find a besser, denn die Lösung geht mit allen Komponenten. Bei eurer bisherigen Lösung muss man zwangsweise für jede Komponente, die ihre Settings persistiert, eine Ableitung machen :wall: in meinen Augen.

Stevie 14. Mai 2014 14:09

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von TiGü (Beitrag 1258889)
Warum kennt bei dir die Mainform und die Detailform die ControlSettings für das RememberEdit?
Geht es nur darum, dass fancy im OI zu verdrahten?
Da fand ich meine Lösung sauberer.

Die Mainform kennt das, weil die Komponente drauf liegt
Die Detailform kennt es, weil die Main.pas im uses steht und man dann über RAD Mechanismen an das RememberEdit das Settings dingen eines anderen Moduls hängen kann - manche Leute brauchen das, daher mein Kommentar - ich brauch sowas persönlich nich, da es nur funktioniert, wenn man diese globalen Variablen in den Units stehen lässt. Ich würd des Setting als Property meines Detailforms machen und beim setzen die Properties anwenden. Braucht halt mehr Code, da das nicht automagically im Loaded passiert.

Zitat:

Zitat von Dejan Vu (Beitrag 1258890)
a) Entkoppel doch einfach die Settings komplett von der Komponente
a.1) Lass das TFormDetail.FormCreate ein
Delphi-Quellcode:
Settings.Load(RememberEdit)
ausführen
a.2) Schreib eine TSettings-Komponente, klatsch die auf das TFormDetail und füge die 'RememberEdit'-Komponente der TSettings-Komponente zu (so macht das DevExpress mit seinem TPropertyStore oder wie das Teil heißt).
b) So, wie Du das schon angedacht hast: Settings => Datamodule

Ich find a besser, denn die Lösung geht mit allen Komponenten. Bei eurer bisherigen Lösung muss man zwangsweise für jede Komponente, die ihre Settings persistiert, eine Ableitung machen :wall: in meinen Augen.

Die Anforderung war, das Settings Teil nur einmal zu haben, mit a) geht das nicht - bei DX zum Beispiel braucht auch jedes Form nen PropertyStore.

Dejan Vu 14. Mai 2014 14:16

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Stevie (Beitrag 1258895)
Die Anforderung war, das Settings Teil nur einmal zu haben, mit a) geht das nicht

Doch. Version a1.

Stevie 14. Mai 2014 14:18

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Dejan Vu (Beitrag 1258900)
Zitat:

Zitat von Stevie (Beitrag 1258895)
Die Anforderung war, das Settings Teil nur einmal zu haben, mit a) geht das nicht

Doch. Version a1.

Woher kommt im FormCreate denn das Settings her?

Dejan Vu 14. Mai 2014 14:56

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Stevie (Beitrag 1258901)
Zitat:

Zitat von Dejan Vu (Beitrag 1258900)
Zitat:

Zitat von Stevie (Beitrag 1258895)
Die Anforderung war, das Settings Teil nur einmal zu haben, mit a) geht das nicht

Doch. Version a1.

Woher kommt im FormCreate denn das Settings her?

Von der einen Instanz natürlich, die ja gefordert ist. Der einzige Unterschied zwischen deiner angedachten Lösung mit dem Datenmodul und meiner Variante (a1) ist die Entkopplung der Settings von der Komponente. Anstatt also die Komponente sich selbst laden zu lassen, wird das nun von außen angestoßen. Klar, muss man dann jeweils machen. Gut aufgehoben ist so eine Settings-Komponente natürlich zentral, in einem Datenmodul o.ä.

Ich finde meine Variante einfach wiederverwendbarer und praktischer, weil, wie schon gesagt, keine SonderlockenIchKannMitSettingskomponenten erstellt werden müssen. So ein Settingsdingens kann mit Jedem.

Variante a.2) ist eben Old-School Delphi: "Für alles ne Komponente". Einfach, RAD, Chaos.

Allerdings muss man sich auch genau überlegen, was man da eigentlich umsetzen will.
Eine spezielle(!) Edit-Komponente, die ihre letzte Eingabe persistieren will, ist anders umzusetzen, als die Fähigkeit, beliebige Eigenschaften von Komponenten zu persistieren.

Soweit ich mich erinnere, ging es im Eingangspost um ein PageControl, das Eigenschaften mit Hilfe einer User-ID als Schlüssel in einer DB ablegen will und da dachte ich, wir sprechen über Letzteres. Im Beispiel mit dem RememberEdit wäre vermutlich die hier vorliegende Variante passender.

Stevie 14. Mai 2014 15:58

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Dejan Vu (Beitrag 1258907)
Von der einen Instanz natürlich, die ja gefordert ist. Der einzige Unterschied zwischen deiner angedachten Lösung mit dem Datenmodul und meiner Variante (a1) ist die Entkopplung der Settings von der Komponente. Anstatt also die Komponente sich selbst laden zu lassen, wird das nun von außen angestoßen. Klar, muss man dann jeweils machen. Gut aufgehoben ist so eine Settings-Komponente natürlich zentral, in einem Datenmodul o.ä.

Ich finde meine Variante einfach wiederverwendbarer und praktischer, weil, wie schon gesagt, keine SonderlockenIchKannMitSettingskomponenten erstellt werden müssen. So ein Settingsdingens kann mit Jedem.

Woher weiß das Detailsform denn nun von der einen Settings Instanz? Constructor Injection? Zuweisen über eine Eigenschaft und Anstoßen des Settings ladens?

Grundsätzlich stimme ich dir zu, was das Speichern/Laden der Settings angeht (Stichwort SRP). Aber auch dann muss man entweder loader/saver haben, die speziell für unterstützten Komponenten sind oder die Komponenten müssen über irgendeinen Mechanismus (z.B. Attribute) mitteilen, was sie denn gespeichert haben möchten.
Ansonsten artet das Settingsdingen irgendwann in einem Riesengroßen if then else Chaos aus, in dem überprüft wird, welche Komponente ich denn gerade speichern möchte inklusive der abhängigkeit des Settingsdingen auf alles, was es kann.

Dennoch: Was wollen wir erreichen? Lose kopplung, um einzelne Teile zu testen ohne den gesamten Klump deiner Anwendung dran hängen zu haben. Sofern man das Settingsdingen noch abstrahiert, so dass ein TRememberEdit nicht mit der konkreten Implementierung arbeitet sondern mit einer Abstraktion habe ich das erreicht. Ebenfalls ist mein konkretes TSettings nicht von irgendwelchen global States abhängig und könnte auch ohne UserId seine Settings in localappdata oder sonstwo hin ballern. Wie und wo ich dann diese beiden Kollegen miteinander bekannt mache ist fast nebensächlich.

Zitat:

Zitat von Dejan Vu (Beitrag 1258907)
Soweit ich mich erinnere, ging es im Eingangspost um ein PageControl, das Eigenschaften mit Hilfe einer User-ID als Schlüssel in einer DB ablegen will und da dachte ich, wir sprechen über Letzteres. Im Beispiel mit dem RememberEdit wäre vermutlich die hier vorliegende Variante passender.

Ob nun Pagecontrol oder Edit, oder Datenbank oder Textdatei spielt eigentlich keine Rolle - ich glaube so viel abstrahieren kann hier jeder.

Dejan Vu 14. Mai 2014 16:20

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Stevie (Beitrag 1258911)
Woher weiß das Detailsform denn nun von der einen Settings Instanz? Constructor Injection? Zuweisen über eine Eigenschaft und Anstoßen des Settings ladens?

Die Settings sind in einem Datenmodul.
Delphi-Quellcode:
Procedure TFormDetail.FormCreate(Sender : TObject);
Begin
  myDataModule.Settings.Load(RemberEdit);
  // bzw.
  For control in Controls do myDataModule.Settings.Load(control);
End;
Um es kurz zu machen: Deine Lösung ist vollkommen ausreichend, ich wollte noch das SRP einbringen, das ist alles. Wie man den Rest löst (muss ich dir ja nicht erzählen), hatte ich weiter oben ausgeführt (Die Geschichte mit einer Factory, einer FactoryFactory usw.), nur hat das wohl keiner gelesen oder verstanden oder beides.

Zitat:

Ob nun Pagecontrol oder Edit, oder Datenbank oder Textdatei spielt eigentlich keine Rolle - ich glaube so viel abstrahieren kann hier jeder.
Habe ich mich misverständlich ausgedrückt? Mein Einwand war:
Will ich (nur) eine Komponente, die ihren inneren Zustand irgendwo persistiert, oder will ich eine generelle Möglichkeit, Komponenten bzw. Klassen zu persistieren?
Im ersten Fall ist die Persistierung Bestandteil der Komponente ("Wer ist zuständig?" "Die Komponente!") Im zweiten Fall ist die Persistierung Bestandteil meines Frameworks, mit allen Vorteilen.
Mit meinem Fokus auf die Eingangsfrage war nicht das Pagecontrol gemeint (das ist wirklich wurst), sondern die für mich dann doch im Vordergrund stehende Möglichkeit, Komponenten generell zu persistieren. So hatte ich die Frage jedenfalls verstanden. Und dann wäre dein Vorschlag eben verbesserungswürdig. Das kannst Du natürlich anders sehen, wenn Du magst.

Stevie 14. Mai 2014 17:12

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Zitat:

Zitat von Dejan Vu (Beitrag 1258916)
Zitat:

Zitat von Stevie (Beitrag 1258911)
Woher weiß das Detailsform denn nun von der einen Settings Instanz? Constructor Injection? Zuweisen über eine Eigenschaft und Anstoßen des Settings ladens?

Die Settings sind in einem Datenmodul.
Delphi-Quellcode:
Procedure TFormDetail.FormCreate(Sender : TObject);
Begin
  myDataModule.Settings.Load(RemberEdit);
  // bzw.
  For control in Controls do myDataModule.Settings.Load(control);
End;

Genau da sitzt ja der Hase wieder im Pfeffer, denn auch das ist leider wieder ein Singleton bzw global state (nett versteckt und durch RAD unheimlich sexy verpackt, da man es schön fein im OI an bestehende Komponenten stecken kann). Oder wenn man nicht auf RAD und den Firlefanz steht, kann man immernoch wuppdi mal ebend im Code drauf zugreifen, tut ja nich weh.

Nu hab ich in jedem Form, wo ich meine Settings laden/speichern möchte als Abhängigkeit das myDataModule - nicht falsch verstehen, das hab ich in der Lösung, die ich oben gezeigt habe, auch. Nur das als irgendwie besser zu verkaufen ist Augenwischerei, da es dasselbe in grün ist.

Deshalb stell ich mal eine Behauptung auf bis mich jemand vom Gegenteil überzeugt:
"DI ist die einzige Möglichkeit, Abhängigkeiten voneinander zu entkoppeln."

Ob man das immer muss oder manchmal auch der halbe Weg reicht, steht wie so oft bei solchen Diskussionen auf einem anderen Blatt. ;)

Dejan Vu 15. Mai 2014 06:47

AW: Cleancode, Dependency Injection oder wie stelle ich mich richtig an
 
Da ich die Abhängigkeit von den Controlsettings nur noch an einem Pattern habe (FormCreate), anstatt in zwei (FormCreate und jede Komponente), kann ich das nun durch Vererbung wegkürzen.

Bei meiner Lösung habe ich die Abhängigkeit zunächst aus der Komponente entfernt und mit einer abstrakten Basisklasse wäre die Abhängigkeit an genau einer Stelle. Und wenn ich sie nur dort verwende, kann ich auf das Globalgedöns auch ganz verzichten.

Ganz ohne Abhängigkeiten von den Controlsettings *kann* es gar nicht funktionieren, weil die Verwendung Teil der Lösung ist. Ergo haben wir irgendwo mindestens ein 'uses ControlSettings'. Je weniger Stellen, desto besser. Ganz Ohne geht nicht, mehr als eine Abhängigkeit ist aber beinhae schon zuviel, also:
Delphi-Quellcode:
Type
  TBaseControlSettingsForm = Class(TForm)
  Private
    Procedure LoadSettings;
  Public
    Procedure FormCreate(Sender : TObject); // lädt die Properties der Controls über die ControlSettings-Klasse
  end;
...

//  TFormDetail = Class (TBaseControlSettingsForm);
//  TFormMain = Class (BaseControlSettingsForm);

Procedure TBaseControlSettingsForm.FormCreate(..);
Begin
  LoadSettings;
End;
So würde ich das lösen. Natürlich sind die Formulare in einzelnen Dateien. Habe das nicht SOLID-mäßig abgeklopft, aber von meinem alten Bierbauchgefühl würde ich sagen, das es ausreichend ordentlich ist.

Ich kann der BasisForm auch eine 'UserID' spendieren, sofern diese systemimmanent ist, also integraler und unverzichtbarer Bestandteil des (Form-)Frameworks. In meinem Ansatz ist z.B. die Verwendung der Controlsettings systemimmanent, weil eben genau so gewollt.

Übrigens, wenn man DI konsequent verwendet, tippt man sich einen Wolf und erzeugt wunderbar testbaren Code, der aber so grottig (weil schlecht lesbar), das man kotzen könnte. Den Weg muss man aber dort gehen, wo Abhängigkeiten durch Faulheit entstehen würde.

Aber wenn die Abhängigkeit -tolles Wort- systemimmanent (und nur dann) ist, sollte man den DI-Firlefanz außen vor lassen.


Alle Zeitangaben in WEZ +1. Es ist jetzt 19:16 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz