Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Programmieren allgemein (https://www.delphipraxis.net/40-programmieren-allgemein/)
-   -   Entkoppeln von Forms (https://www.delphipraxis.net/193811-entkoppeln-von-forms.html)

TigerLilly 12. Sep 2017 07:31

Entkoppeln von Forms
 
Ich würde gern die Erzeugung von Forms entkoppeln. Wenn also FormA ein FormB erzeugt, möchte ich nicht, dass in FormA ein uses FormB steht. FormB soll sich irgendwo registrieren und von dort soll FormA ihn anfordern können.

Erreichen möchte ich, dass zB für Testzwecke ein anderer FormB sich registriert und ich aber am FormA gar nichts ändern muss.

Aber weder Generics noch Factories machen mich so richtig glücklich :-/

Am ehesten kommt eine Liste aus "FormName" und "FormInstanz" hin, wo sich FormB einträgt und wo FormA sich über den Namen die Instanz abholen kann. Aber irgendwie gefällt mir das auch nicht.

Hat wer eine elegante Idee oder kreative Vorschläge?

Uwe Raabe 12. Sep 2017 07:49

AW: Entkoppeln von Forms
 
Das ist aber wirklich sehr stark Geschmackssache.

Ganz pragmatisch: Wie wäre es mit einer gemeinsam genutzten Unit, die eine globale Variable vom Typ
Delphi-Quellcode:
TFormClass
enthält? Simpler geht's glaub ich nimmer.
Delphi-Quellcode:
unit CommonGlobals;

interface

var
  MyFormClass: TFormClass = nil;

implementation

end.

Codehunter 12. Sep 2017 07:50

AW: Entkoppeln von Forms
 
FormB kann sich nirgends registrieren, solange es nicht instantiiert ist. Du scheinst da konstruktivisch ein Henne-und-Ei-Problem zu haben. Theoretisch kannst du dir eine Manager-Unit anlegen, darin ein TDictionary<string, TCustomForm> sowie eine globale Variable, die du im initialization-Abschnitt instantiierst. Ebenso über initialization kannst du auch in den Units deiner Sekundär-Forms verfahren und dann gleich in die globale Variable der Manager-Unit ablegen.

Elegant sieht aber anders aus ^^

haentschman 12. Sep 2017 07:54

AW: Entkoppeln von Forms
 
Zitat:

Du scheinst da konstruktivisch ein Henne-und-Ei-Problem zu haben.
...eben. Wenn du die Instanzen in einer Liste speicherst, geht das nur über den gemeinsamen Nenner. (TFormClass) Beim Rausholen der Instanz kommst du über einen Cast nicht herum. :zwinker:

TigerLilly 12. Sep 2017 08:14

AW: Entkoppeln von Forms
 
Das mit den Instanzen stimmt natürlich + war "schlampert" von mir. Sollten besser die Klassentypen sein.

Ich könnte einen FormManager machen, ein Dictionary aus Name+TFormClass
Dann könnte FormA sowas wie FormManager.CreateAndShow('FORMB') machen.
Der FormManager sucht nach 'FormB', findet als Klassentyp TFormB + erzeugt dann TFormB.

Wie könnte das gehen?

Codehunter 12. Sep 2017 08:16

AW: Entkoppeln von Forms
 
Zitat:

Zitat von haentschman (Beitrag 1380878)
Beim Rausholen der Instanz kommst du über einen Cast nicht herum. :zwinker:

Es sei denn, alle Sekundärforms wären von einer gemeinsamen Klasse abgeleitet. Also nicht TCustomForm sondern TMyCustomSecondaryForm oder sowas. Ich habe Projekte wo ähnliches veranstaltet wurde wenn es um MDI ging. Ist aber schon lange her.

Ähnliche Probleme habe ich kennengelernt, als es darum ging, Forms in DLLs auszulagern. Die wurden dann durch Installer per Registry mit dem Programm verknüpft und zur Laufzeit nachgeladen. Möglicherweise geht das Projekt ja in eine solche Richtung.

Der schöne Günther 12. Sep 2017 11:34

AW: Entkoppeln von Forms
 
Also entweder habe ich etwas falsch verstanden oder was du willst ist ein völlig legitimes (und hier auch super passendes) Dependency Injection über einen "Inversion of Control"-Container.

Zitat:

Als Dependency Injection [...] wird in der objektorientierten Programmierung ein Entwurfsmuster bezeichnet, welches die Abhängigkeiten eines Objekts zur Laufzeit reglementiert: Benötigt ein Objekt beispielsweise bei seiner Initialisierung ein anderes Objekt, ist diese Abhängigkeit an einem zentralen Ort hinterlegt – es wird also nicht vom initialisierten Objekt selbst erzeugt.
https://de.wikipedia.org/wiki/Dependency_Injection

Bei mir war das erste mal im Delphi-Land etwas ähnliches: Benutzer-Eingaben von Text oder Zahlen fanden über große, touch-freundliche Popups statt. Für Remote-Support über TeamViewer oder an Desktop-Rechnern sollten es aber lieber die gewohnten Eingabe-Dialoge (Vcl.InputQuery(..) oder so) sein. Was lag hier näher als eine zentrale Anlaufstelle
Delphi-Quellcode:
TInputPopup.CreateTextInput(): ITextInput
(oder so ähnlich). Je nach Konfiguration bekommt man zur Laufzeit das passende Eingabe-Popup. Das Formular (FormA) weiß überhaupt nicht ob es nun ein FormB oder FormC oder sonstwas bekommt, es muss es nicht kümmern. Es fordert ein ITextInput an, es bekommt das passende, es arbeitet damit, gut ist.

Wenn dir das alles noch gar nichts sagt macht der folgende Artikel das vielleicht schmackhaft:
http://delphisorcery.blogspot.de/201...practices.html

Der Autor ist auch der Mann hinter "Spring4D" das auch einen DI-Container mitbringt. Man braucht ein paar Minuten für die ersten Schritte, danach kann man nicht mehr ohne.

Ich würde auch gleich das neulich erschienene Dependency Injection in Delphi-Buch von Nick Hodges empfehlen. Mit den Büchern von dem Mann macht man echt nichts falsch, finde ich.

TigerLilly 12. Sep 2017 11:41

AW: Entkoppeln von Forms
 
Wie sagt man so schön: Been there. Done that. :- )

Das löst mein Problem nicht, weil die Forms Creatables + nicht Injectables sind. Und ich möchte sie eben nicht alle zu Beginn erzeugen + dann via DI weiterreichen.

Der schöne Günther 12. Sep 2017 12:04

AW: Entkoppeln von Forms
 
Vielleicht bin ich schon zu lange im Urlaub, aber was genau soll das Problem sein? Weshalb solltest du Instanzen der Formulare zu Beginn erzeugen müssen? Was ist ein Creatable und was ein Injectable?

Container wie Spring bieten auch an, z.B. Klassen als Singletons registrieren zu können und man bekommt dann, wenn man den Typ (bzw. Schnittstelle anfordert) die entsprechende Instanz.


Vielleicht habe ich auch die Frage noch nicht verstanden, es hörte sich zumindest für mich genauso an wie mein Fall mit z.B. den Input-Popups.

TigerLilly 12. Sep 2017 12:35

AW: Entkoppeln von Forms
 
Weil DI wie der Name sagt das Übergeben einer Abhängigkeit ist. Mein FormA müsste also FormB "injected" bekommen. Dann hilft der DI-Container.

Aber wie hast du das mit deinen Input-Popups gelöst?

Fritzew 12. Sep 2017 15:27

AW: Entkoppeln von Forms
 
Wir haben das komplett über Interfaces und passende Factories gelöst.

Es gibt also ein Interface für eine bestimme Form z.B

Delphi-Quellcode:
ICncMachineConverterForm = interface
      ['{DC5376B6-B124-4035-B641-6BECAD516DE9}']
      function ShowcheckMarking(const aGetMark: int64; const aContact : tCncContact) : boolean;

   end;
Dann haben wir verschiedene Factories (Die meistens auch Interfaces sind) für unsere Forms

also zB:
Delphi-Quellcode:
TCnFormFactory = class // Static
 class function getICncMachineConverterForm : ICncMachineConverterForm; static;
end;
Im Source dann einfach

Delphi-Quellcode:
iCncForm := TCnFormFactory. getICncMachineConverterForm;
if IcncForm. ShowcheckMarking(14, myContact) then......
Ob die Form nun selber das Interface zur verfügung stellt oder wo auch immer das herkommt ist unwichtig.

TigerLilly 12. Sep 2017 15:39

AW: Entkoppeln von Forms
 
Ich hab jetzt mal sowas:

Delphi-Quellcode:
type
  TCreateFormFunction = reference to function(Owner: TComponent): TForm;

  TFormFactory = class
    FList: TDictionary<string, TCreateFormFunction>;
  public
    constructor Create; virtual;
    procedure RegisterForm(sName: String; CreateFormFunction: TCreateFormFunction);
    function CreateForm(sName: String; Owner: TComponent): TForm; virtual;
  end;

implementation

constructor TFormFactory.Create;
begin
  FList := TDictionary<string, TCreateFormFunction>.Create;
end;

function TFormFactory.CreateForm(sName: String; Owner: TComponent): TForm;
begin
  Result := FList[sName](Owner);
end;

procedure TFormFactory.RegisterForm(sName: String; CreateFormFunction: TCreateFormFunction);
begin
  FList.Add(sName, CreateFormFunction);
end;
Damit kann eine eigene Unit, die einzelnen Forms registrieren:
Delphi-Quellcode:
var
  FormFactory: TFormFactory;

implementation

uses
  Form01.View, Form02.View;

procedure Formregistration;
begin
  FormFactory := TFormFactory.Create;
  FormFactory.RegisterForm('Form02',
    function(Owner: TComponent): TForm
    begin
      Result := TForm02.Create(Owner);
    end);
  FormFactory.RegisterForm('Form01',
    function(Owner: TComponent): TForm
    begin
      Result := TForm01.Create(Owner);
    end);
end;
Und mein Form01, der gar nichts von Form02 weiß, kann dieses machen:
Delphi-Quellcode:
  Formfactory.CreateForm('Form02',Self).Show;
Das ist doch schon mal nett. :-)

Das verfeinere ich jetzt noch wie folgt:
- Interfaces statt TForms
- Ich will ja nicht x-beliebige Forms registrieren, Sinn hat das ja nur für jene, die das gleiche tun. Also eine eigene factory je Interface.

Mal sehen.

Feedback gerne, danke.

bepe 12. Sep 2017 18:33

AW: Entkoppeln von Forms
 
Das sieht ziemlich vertraut aus. Aber zusätzlich ist die Factory bei mir ein Singleton und wenn du keinen abgeleiteten Konstruktor verwendest, wäre mir
Delphi-Quellcode:
TCreateFormFunction
zu viel Tipparbeit :-D.

Delphi-Quellcode:
interface

uses
  System.Classes,
  System.SysUtils,
  System.Generics.Collections,
  Vcl.Forms;

type
  IFactory = interface
  ['{D91ADAAF-971F-497F-8EDA-A4644BA33087}']
    procedure RegisterForm(AClass: TCustomFormClass);
    procedure UnRegisterForm(AClass: TCustomFormClass);

    function GetInstance(AClass: TCustomFormClass; AOwner: TComponent): TCustomForm; overload;
    function GetInstance(const AClassName: String; AOwner: TComponent): TCustomForm; overload;

  end;

function FormFactory: IFactory;

implementation

type
  TFactory = class(TInterfacedObject, IFactory)
  class var
    FactoryInstance: IFactory;

  class constructor CreateClass;
  class destructor DestroyClass;

  strict private
    FClassList: TDictionary<String, TCustomFormClass>;

  public
    constructor Create;
    destructor Destroy; override;

    procedure RegisterForm(AClass: TCustomFormClass);
    procedure UnRegisterForm(AClass: TCustomFormClass);

    function GetInstance(AClass: TCustomFormClass; AOwner: TComponent): TCustomForm; overload;
    function GetInstance(const AClassName: String; AOwner: TComponent): TCustomForm; overload;

  end;

function FormFactory: IFactory;
begin
  Result := TFactory.FactoryInstance;
end;

{ TFactory }

constructor TFactory.Create;
begin
  inherited;

  FClassList := TDictionary<String, TCustomFormClass>.Create;
end;

class constructor TFactory.CreateClass;
begin
  TFactory.FactoryInstance := TFactory.Create;
end;

destructor TFactory.Destroy;
begin
  FClassList.Free;

  inherited;
end;

class destructor TFactory.DestroyClass;
begin
  TFactory.FactoryInstance := nil;
end;

function TFactory.GetInstance(AClass: TCustomFormClass;
  AOwner: TComponent): TCustomForm;
begin
  Result := GetInstance(AClass.ClassName, AOwner);
end;

function TFactory.GetInstance(const AClassName: String;
  AOwner: TComponent): TCustomForm;
var
  tmpClass: TCustomFormClass;
begin
  if FClassList.TryGetValue(AClassName, tmpClass) then
    Result := tmpClass.Create(AOwner)
  else
    raise Exception.Create('Unbekannte Formklasse');
end;

procedure TFactory.RegisterForm(AClass: TCustomFormClass);
begin
  FClassList.AddOrSetValue(AClass.ClassName, AClass);
end;

procedure TFactory.UnRegisterForm(AClass: TCustomFormClass);
begin
  FClassList.Remove(AClass.ClassName);
end;
Die Registrierung würde ich eher direkt beim Form machen. Sonst wird mal was übersehen oder es kommt eine "riesige" Unit mit unzähligen Abhängigkeiten dabei raus.

Delphi-Quellcode:
  TMyForm = class(TForm)
  class constructor ClassCreate;
  class destructor ClassDestroy;

  end;
...
{ TMyForm }

class constructor TMyForm.ClassCreate;
begin
  FormFactory.RegisterForm(TMyForm);
end;

class destructor TMyForm.ClassDestroy;
begin
  FormFactory.UnRegisterForm(TMyForm);
end;
Und der Aufruf wäre wie gehabt:
Delphi-Quellcode:
FormFactory.GetInstance('TMyForm', Self).Show;

TigerLilly 12. Sep 2017 20:24

AW: Entkoppeln von Forms
 
Ja, das ist auch sehr cool. :-)
Ich glaub, da schau ich mir was ab.

Aber - wenn du erlaubst - ich würde die Interfacedefinition in eine eigene Unit geben. Sonst bindet jede Verwendung des Interfaces die Klasse mit ein.
Und auch die Registrierung direkt beim Form finde ich nicht so super, weil das ja erst wieder die Form an die Factory koppelt. Und ich wollte ja Kontrolle über die Registrierung.

Bei "meiner" Variante sehe ich den Vorteil, dass ich Kontrolle über das Erzeugen des Forms habe - ich könnte hier auch so was machen:

FormFactory.RegisterForm('Form01',
function(Owner: TComponent): TForm
var
tmp:TForm1;
begin
tmp := TForm01.Create(Owner);
tmp.XY:= ....
tmp.DoSomething;
Result:=tmp;
end);

Stevie 12. Sep 2017 23:32

AW: Entkoppeln von Forms
 
Zitat:

Zitat von TigerLilly (Beitrag 1380916)
Wie sagt man so schön: Been there. Done that. :- )

Das löst mein Problem nicht, weil die Forms Creatables + nicht Injectables sind. Und ich möchte sie eben nicht alle zu Beginn erzeugen + dann via DI weiterreichen.

Forms sind mMn injectables - und man muss nicht alle zu Beginn erzeugen, dazu gibt es lazy initialization.

Zu den gezeigten Ansätzen kann ich nur sagen: das ist kein DI sondern Service Locator. Kann man machen, aber dass man sich damit wartbaren und testbaren durch entkoppelten Code erhält, ist ein Gerücht (Einkopplung von Code Teilen is ja schließlich kein Selbstzweck). Klar, man hat erstmal nur ein Interface und nicht die Klasse - augenscheinlich entkoppelt. Allerdings hat man nun überall die Abhängigkeit auf den Service Locator / Form Factory und auch implizite Abhängigkeiten (der Name). Wenn ich ein Form irgendwo wiederverwenden möchte, bin ich darauf angewiesen, dass ich auch das über die Form Factory angefragte Form da ist und kann nicht etwa einfach an dieser Stelle ein anderes Form reingeben.

Implizite Abhängigkeiten führen meiner Erfahrung nach zu viel vertrackterem Codeklump als die klassischen expliziten "TForm1 erzeugt TForm2 und das erzeugt TForm3" Abhängigkeiten.

Übrigens brauch man diese
Delphi-Quellcode:
TCreateFormFunction
nicht, dafür gibts ja in Delphi die wunderbaren Metaklassen. Also einfach die Forms als TComponentClass registrieren und das Create davon aufrufen.

TigerLilly 13. Sep 2017 06:47

AW: Entkoppeln von Forms
 
Hi Steve,

danke für die Hinweise. Ich lerne immer gern dazu - wie könnte das (in Spring4D?) aussehen?

BTW: Ich fand gerade diese TCreateFormFunction charmant, weil so beliebige Konstruktoren möglich sind.

bepe 13. Sep 2017 06:56

AW: Entkoppeln von Forms
 
Zitat:

Zitat von Stevie (Beitrag 1380968)
Allerdings hat man nun überall die Abhängigkeit auf den Service Locator / Form Factory und auch implizite Abhängigkeiten (der Name). Wenn ich ein Form irgendwo wiederverwenden möchte, bin ich darauf angewiesen, dass ich auch das über die Form Factory angefragte Form da ist und kann nicht etwa einfach an dieser Stelle ein anderes Form reingeben.

Das muss ja nicht so sein. Die Factory kann ja rein gegeben werden oder ein Merkmal anhand dessen das zu erzeugende Formular identifiziert wird. Die konkrete Implementierung ist ja von der konkreten Aufgabe abhängig. Außerdem binde ich mir doch die Hände, wenn ich das Form bzw. dessen Klasse übergebe.
Beispiel: FormA ist eine Maske die mir ein Suchergebnis, ein Grid anzeigt. Außerdem gibt es eine Combobox in der Tabellen stehen (Kunden, Lieferanten, Artikel etc.) und ein Button. Beim Buttonklick wird ein FormB zum Filtern der Daten geöffnet. Wurde mir das FormB von außen vorgegeben, bleibt mir keine Wahl. Über eine Factory, die ja auch ein generisches Fallback bereithalten kann, besteht die Möglichkeit auf die Benutzereingaben zu reagieren und z.B. eine Kundensuchmaske anzuzeigen.

Bei "Logikendpunkten" ist das keine Frage (alle Benutzereingaben verarbeitet => Datenklasse.Export(Exportklasse)). Aber ein Form in ein Form zu injizieren klingt nach Einschränkung und nach vorgelagerter Logik. Nach Funktionalität die aus der Klasse, in die sie Gehört, rausgeholt wurde.

Stevie 14. Sep 2017 23:04

AW: Entkoppeln von Forms
 
Zitat:

Zitat von TigerLilly (Beitrag 1380975)
Hi Steve,

danke für die Hinweise. Ich lerne immer gern dazu - wie könnte das (in Spring4D?) aussehen?

BTW: Ich fand gerade diese TCreateFormFunction charmant, weil so beliebige Konstruktoren möglich sind.

Wir nutzen die typischen TComponent Konstruktoren bei uns in der Anwendung fast gar nicht, wenn man interfaced Forms und Frames hat, braucht man keinen Owner.
Daher kann man nun Konstruktoren bauen, die alle ihre Abhängigkeiten injektet bekommen.

Gut, im Beispiel hatte ich halt nur die typischen Aufrufe des standard TComponent.Create gesehen.

Zitat:

Zitat von bepe (Beitrag 1380977)
Das muss ja nicht so sein. Die Factory kann ja rein gegeben werden oder ein Merkmal anhand dessen das zu erzeugende Formular identifiziert wird. Die konkrete Implementierung ist ja von der konkreten Aufgabe abhängig. Außerdem binde ich mir doch die Hände, wenn ich das Form bzw. dessen Klasse übergebe.
Beispiel: FormA ist eine Maske die mir ein Suchergebnis, ein Grid anzeigt. Außerdem gibt es eine Combobox in der Tabellen stehen (Kunden, Lieferanten, Artikel etc.) und ein Button. Beim Buttonklick wird ein FormB zum Filtern der Daten geöffnet. Wurde mir das FormB von außen vorgegeben, bleibt mir keine Wahl. Über eine Factory, die ja auch ein generisches Fallback bereithalten kann, besteht die Möglichkeit auf die Benutzereingaben zu reagieren und z.B. eine Kundensuchmaske anzuzeigen.

Bei "Logikendpunkten" ist das keine Frage (alle Benutzereingaben verarbeitet => Datenklasse.Export(Exportklasse)). Aber ein Form in ein Form zu injizieren klingt nach Einschränkung und nach vorgelagerter Logik. Nach Funktionalität die aus der Klasse, in die sie Gehört, rausgeholt wurde.

Du vermischt hier zwei Dinge:
1. es braucht keine Entscheidung getroffen werden -> die konkrete Abhängigkeit wird injektet. Hierbei ist die entscheidung bei der Komposition des Objektgraph im Komposition Root getroffen worden.
2. es wird anhand von Benutzereingaben eine Entscheidung getroffen, ob ich A oder B brauche. Entweder wurden mir A und B injektet oder die entsprechenden Factories bzw Lazy von A oder B um sie bei bedarf erst zu bekommen. Das Teil, was mir dann aber A oder B oder C gibt, kann mir nur A,B oder C geben und sollte deshalb nich einfach die Wundertüte Namens ServiceLocator, FormFactory oder sonstwas allgemeines sein. Denn das öffnet Tür und Tor, sich da einfach was raus zu angeln, was man nicht erwartet - einfach mal nach Misko Hevery Service Locator googeln und die Session schauen (englisch)

ayden88 15. Sep 2017 05:31

AW: Entkoppeln von Forms
 
Weshalb solltest du Instanzen der Formulare zu Beginn erzeugen müssen?

Stevie 15. Sep 2017 09:14

AW: Entkoppeln von Forms
 
Zitat:

Zitat von ayden88 (Beitrag 1381256)
Weshalb solltest du Instanzen der Formulare zu Beginn erzeugen müssen?

Aus demselben Grund, warum man per IDE Forms in die auto-create Liste packen kann?
Für den weitaus üblicheren Fall des "wird erst erzeugt, wenn gebraucht" hab ich ja schon
in beiden meiner vorherigen Kommentare das Schlüsselwort "lazy initialization" erwähnt.

Lazy initialization heißt aber ebend nicht "statische Abhängigkeit, hol dir doch den Krams selbst ab",
sondern: injection der Factory/einer möglichkeit, die Instanz beim ersten Zugriff erst erzeugen zu lassen.


Alle Zeitangaben in WEZ +1. Es ist jetzt 13:09 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