![]() |
Trennung von GUI und Logik
Seit vielen Jahren, um nicht zu sagen Jahrzehnten programmiere ich hobbymäßig und vorwiegend für den Eigenbedarf. Das begann 1987 mit dem 64er (Basic, Assembler). Es folgte der Amiga (Basic, GFA-Basic, Assembler) und schließlich diverse PCs ( Assembler,TP 6 , BP 7, Delphi 1, 2, 4, 10). Meist waren das kleinere Anwendungen, für die natürlich vorher kein Konzept erstellt wurde, sondern nach Erstellung der Infrastruktur (GUI, IO-Routinen) mit möglichst wenig Zeitaufwand die gewünschte Funktionalität implementiert wurde. Immer wieder kam es allerdings vor, dass gerade solche q&d hingerotzte Sachen dann im Laufe der Zeit immer weiter wuchsen, um nicht zu sagen wucherten. Dabei versuchte ich natürlich immer, gewisse Mindeststandards einzuhalten, was Strukturierung und Modularisierung betraf, sodass ich auch nach Jahren weitgehend die Übersicht behielt. Da ich das Ganze, wie gesagt, hobbymäßig betreibe, konnte ich auch viel Zeit mit 'Refactoring' verbringen, wobei sowohl GUI als auch der Programmcode immer wieder optimiert und 'verschönert' , soll heißen 'profimäßiger' ( oder was ich dafür hielt) gestaltet wurde. Dennoch fürchte ich, ein professioneller Entwickler würde beim Anblick mancher meiner Sources einen Schreikrampf erleiden, aber damit kann ich leben. Denn für mich ist der Hauptzweck erfüllt: Die Funktionalität ist wunschgemäß, die Optik ist ansprechend und das GUI funktional und gut bedienbar. Ich kann das Programm nutzbringend für mich einsetzen und nicht nur IT-Laien damit schwer beeindrucken ;-)
Wie sicher mancher schon ahnt, bin ich auch nicht der Großmeister der OOP. Daher rührt auch mein aktuelles Problem. In meinem aktuellen Projekt möchte ich eine weitgehende Trennung von GUI und Logik durchführen. Mich stört vor allem die grotesk aufgeblähte Deklaration des Hauptformulars, das einen großen Teil der Logik in den OnClick - Handlern bzw. deren Implementation enthält. Bei eienem großen Teil von ihnen ließ sich der Code leicht in separate Units bzw. Klassen auslagern, was das Ganze schon erheblich übersichtlicher macht. Aber jetzt sollen doch Nägel mit Köpfen gemacht und sämtliche Logik aus der Mainform - Deklaration und -Implementierung entfernt werden. Ein Problem sind für mich die Routinen, die während der oft sekunden- oder gar minutenlangen Dauer ihrer Ausführung auf Komponenten des Hauptformulars zugreifen und etwa Zählerstände in TLabels oder Textausgaben in TMemos aktualisieren, was als Lebenszeichen der Anwendung, quasi als Fortschritts-Anzeige, und auch zur Information des Anwenders unerläßlich ist. Beispiel:
Delphi-Quellcode:
Wie schaffe ich es nun, den Code aus dem OnClick - Handler auszulagern und dennoch Zugriff auf Form1 zu ermöglichen?
unit DemoForm;
interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TForm1 = class(TForm) Button1: TButton; Label1: TLabel; Memo1: TMemo; // 128 Deklarationen procedure Button1Click(Sender: TObject); // 100 Event- Handler private { Private-Deklarationen } // 116 Deklarationen (schon teilweise reduziert) public { Public-Deklarationen } // 2 Deklarationen end; var Form1: TForm1; implementation //Beispiel: Durchsuche eine iTunes Mediathek mit 10000 Tracks nach Tracks ohne Albumcover (Pseudocode) procedure TForm1.Button1Click(Sender: TObject); var i: Integer; begin { for i := 1 to 10000 do begin label1.caption:= inttostr(i); // erhöhe den Zählerstand im Label If Track(i) ohne Albumcover Then Memo1.lines.add(Titel + Artist + Album) // Wenn einer gefunden, Track (Titel, Artist, Album) im Memo ausgeben end; } end; end. Auf globale Instanz - Variable Form1 zugreifen? Jeder ausgelagerten Prozedur/ Methode Form1 als Parameter übergeben? Ausgelagerte Methoden in Klasse zusammenfassen und in deren Constructor Form1 einer Feldvariablen (z.B. 'FMainform') zuweisen? Windows-Messages? Oder was ganz anderes? Was käme da als halbwegs saubere Lösung in Frage? (außer alles in die Tonne kloppen und von vorne anfangen) :( Gruß LP |
AW: Trennung von GUI und Logik
Hallo,
spendiere doch deiner Main Form für alle diese Fortschrittsanzeigen und Ausgabebedarfe entsprechende Methoden, welche die GUI controls aktualisieren. Diese Methoden bekommen die auszugebenden Daten als Parameter übergeben. Dann erstelle eine neue Unit und erstelle ein Interface, welches genau diese Methodendeklarationen enthält. Das Hauptformular nutzt diese Unit im interface uses block und in class(TForm) wird das Interface ergänzt: class(TForm, IMainFormInterface). Die Geschäftslogikklasse bekommt mittels Constructor dieses Interface übergeben und speichert sich das lokal ab: constructor TBusinessLogic.Create(MainFormInterface: IMainFormInterface); begin inherited Create; FMainFormInterface := MainFormInterface; end; Überall wo die geschäftslogik etwas auf dem GUI ausgeben muss, ruft sie die entsprechende Methode über das Interface auf: FMainFormInterface .DisplayProgress(50); // 50% Fortschritt... Dadurch hängt die Geschäftslogik nur noch von dem Interface ab und nicht mehr vom GUI und damit nicht mehr von internen Änderungen des GUI. |
AW: Trennung von GUI und Logik
Der 'Button1click' macht nur etwas mit der GUI. Den würde ich nicht woanders hin auslagern. Wenn man die Liste noch sonst noch braucht, könnte man diese als Stringlist z.B. in einem Datenmodul definieren.
So wie der Code aussieht, würde wohl nur das Endergebnis für den User sichtbar werden, die Zwischenergebnisse sieht man so wohl nicht. |
AW: Trennung von GUI und Logik
Zitat:
Der von Turbomagic skizzierte Weg scheint mir sehr vielversprechend. So etwas schwebte mir auch vor, nur hatte ich keinen Schimmer, wie das zu realisieren wäre. Interfaces kenne ich nur dem Namen nach, sie werden ja auch vom iTunes COM-Interface verwendet, aber da ist ja schon alles fertig in der iTunes Typelibrary vorhanden. Gruß LP |
AW: Trennung von GUI und Logik
Ich bin in Delphi/Lazarus selbst ein Anfänger, weil ich nur hin und wieder etwas damit mache. Hauptsächlich arbeite ich mit PHP und mache dort OOP. Deshalb vielleicht den "Ratschlag" nicht so ganz ernst nehmen. Aber ich mache es so.
1. Ich erstelle eine Unit mit Klasse-Name-XXX. Das enthält meist die Logik für meine Software. ZB.:
Delphi-Quellcode:
2. Ich verwende die Unit in meinem Hauptformular, oder einem anderem Formular, mit den Componenten im Formular.
unit UDesktopFile;
{$mode objfpc}{$H+} interface uses Classes, SysUtils, FileUtil; type TDesktopFile = class private public function GetAllFiles() : TStringList; end; implementation function TDesktopFile.GetAllFiles(): TStringList; var StrList: TStringList; pathToFiles: String; begin pathToFiles := GetUserDir + '/.local/share/applications/'; StrList := TStringList.Create; FindAllFiles(StrList, pathToFiles, '*.desktop'); GetAllFiles := StrList; end; end.
Delphi-Quellcode:
Das ist jetzt kein Delphi, sondern Lazarus aber funktioniert für mich gut. Aber ob es so richtig ist und so gemacht wird, weiß ich nicht :lol:
procedure TFrmMainWindow.FormCreate(Sender: TObject);
var DesktopFile: TDesktopFile; begin DesktopFile := TDesktopFile.Create; try LbDesktopFiles.Items.Assign(DesktopFile.GetAllFiles); finally DesktopFile.Free; end; end; Jedenfalls sind meine Formulare dadurch sauberer, bzw. übersichtlicher und für Änderungen muss ich die entsprechenden Units besuchen. MfG |
AW: Trennung von GUI und Logik
Moin...:P
Ich bin für folgendes: 1. Trennung in die Form und die Logic (seperate Units) 2. Die Eventhandlandler gehören in die Form. 3. Die GUI Sachen gehören in die Form. 4. Die Logic kennt die Form nicht! :warn: ...auch kein Interface. :zwinker: 5. Die Form gibt im Eventhandler den "Befehl" an die Logic. " 6. Die Logic holt die Daten und übergibt sie an ein selbst erstelltes Event oder gibt die Daten an das Funktionsresult zurück. 7. Mit Event: Die Form hat den Eventhandler der Logic implementiert und übergibt die Daten an die GUI. ...fertsch. PS: Der Kreativität sind keine Grenzen gesetzt. :zwinker:
Delphi-Quellcode:
Das mit dem Interface halte ich für gewagt. Wenn sich an dem GUI Interface was ändert, mußt du auch immer an die Logik dran...Das zu verhindern ist ja der Sinn der Trennung von GUI Und Logik. :zwinker:
constructor TBusinessLogic.Create(MainFormInterface: IMainFormInterface);
PS: Ausnahmen bestätigen die Regel... |
AW: Trennung von GUI und Logik
Hallo,
Delphi-Quellcode:
Dann musst Du die Klasse im Formular nicht mal erstellen,
[B]class [/B]function GetAllFiles() : TStringList;
sondern per
Delphi-Quellcode:
aufrufen.
LbDesktopFiles.Items.Assign([B]T[/B]DesktopFile.GetAllFiles);
Wobei ich mir nicht sicher bin, ob die hier einen memory leak erzeugst. Das Assign kopiert den Inhalt der TStringList. Ich bin immer dafür, dass der der ein Objekt erzeugt, auch für dessen Freigabe verantwortlich ist. Das Formular würde also die StringList erzeugen, an die GetAllFiles als Parameter übergeben, und wenn es die StringList nicht mehr braucht, wieder freigeben. |
AW: Trennung von GUI und Logik
Das ganze Thema schreit für mich aber auch nach 'Threads'. Dementsprechend sollte vielleicht solche Dinge in eigene Threads ausgelagert werden, die dann auch nur periodisch die GUI aktualiesieren und nicht ständig.
|
AW: Trennung von GUI und Logik
Man muss hier m.E. drei Dinge separat betrachten...
1) Trennung GUI/BL: Dazu reicht es aus, sämtliche Logik in eine Klasse TMyBL zu stecken und dort so viele Eigenschaften und Methoden zu veröffentlichen, dass die GUI sich dort umfassend bedienen kann. Die GUI darf also nie z.B. einen Kontostand erhöhen, indem sie den aktuellen Wert abruft, einen Betrag dazu rechnet und das Ergebnis wieder speichert, sondern sie muss TMyBL.AddMoney(x) aufrufen. ALLES was an Daten zu ändern oder abzufragen ist, muss über Methoden und Eigenschaften möglich sein. TMyBL muss also in sich das vollständige Projekt abbilden und komplett funktionsfähig sein. Natürlich ist TMyBL hilflos ohne Ansteuerung von außen, aber alle Erfordernisse müssen dort vorhanden und nutzbar sein. Die GUI (Egal ob VCL, FMX oder anderes) stößt dann lediglich Aktionen an und ruft Daten ab für die eigene Darstellung. Wenn man mit Interfaces umgehen kann ist das sicherlich hilfreich, aber ein klassisches TMyBL-Objekt würde auch reichen. Eine Trennung von GUI und BL wäre damit schon erreicht. 2) Threads: Egal, ob eine längere Berechnung im Eventhandler des Formulars erfolgt oder in der oben beschriebenen TMyBL-Klasse, das Formular wird während dieser Zeit hängen bleiben. Um das zu vermeiden, muss man mit Threads arbeiten oder notfalls mit Timern oder gar Application.ProcessMessages. Hier ist zu beachten, dass die TMyBL davon nicht abhängig sein darf. Am besten sollte sie davon gar nichts mitbekommen. 3) Kommunikation Vor allem, wenn GUI und BL in getrennten Threads oder gar Prozessen laufen, muss man sich über die Kommunikation Gedanken machen. Die GUI darf die BL kennen und dort Änderungen anschieben aber sie sollte gar nicht wissen, was sie da eigentlich tut und warum. Die BL sollte aber gar nichts von der GUI wissen. Es muss aber einen abstrakten Informationsaustausch geben, also am besten über einen Framework, das zwischen beiden Seiten (möglichst automatisiert) vermittelt. Die Frage ist, wie man das am besten organisiert (je nach Anspruch und Möglichkeiten). |
AW: Trennung von GUI und Logik
Zitat:
Und ich denke auch das ich ein Memory Leak erzeuge, jedenfalls nach meinem bisherigem Verständnis. Das TStringList, nach meinem Verständnis, wird nicht (sauber) geleert. Vermutlich nur das DesktopFile. Meine bisherigen Bemühungen schlugen aber bisher Fehl. Aber dafür mache ich mal bei Gelegenheit einen eigenen Thread auf. Danke! Wieder was gelernt! Tolles Forum. MfG |
AW: Trennung von GUI und Logik
Warum der Umweg über eine separate StringList mit Assign? Man kann der Datenklasse doch auch direkt das Memo (bzw. dessen Property Strings) übergeben.
Delphi-Quellcode:
Funktioniert wunderbar, und man kann die Methode FillList auch für andere Zwecke nutzen, wenn man z.B. die Daten nicht in einem Memo haben will.
type
TData = class public procedure FillList(aStrings: tStrings); end; TForm2 = class(TForm) Memo1: TMemo; procedure FormCreate(Sender: TObject); private { Private-Deklarationen } aData: TData; public { Public-Deklarationen } end; var Form2: TForm2; implementation {$R *.dfm} { TData } procedure TData.FillList(aStrings: tStrings); begin aStrings.Add('eins'); aStrings.Add('zwei'); aStrings.Add('drei'); aStrings.Add('vier'); aStrings.Add('fünf'); end; procedure TForm2.FormCreate(Sender: TObject); begin aData := TData.Create; aData.FillList(Memo1.Lines); end; Für Fortschrittsanzeigen etc. könnte man z.B. mit Messages arbeiten (der Datenklasse übergibt man dann einmalig das Handle der MainForm, die die Nachrichten dann abarbeitet), oder man erstellt eigene Events mit passenden EventHandlern. Dann hat man im Code der Datenklasse regelmäßig etwas wie
Delphi-Quellcode:
wobei fOnProgress eine Variable vom Typ Procedure( parameterliste ) of Object ist, die in der MainForm implementiert ist und entsprechend zugewiesen wird.
if assigned(fOnProgress) then
fOnProgress(param1, param2, param3, ...); |
AW: Trennung von GUI und Logik
Na dann ergänze ich mal paar Ideen. Wirkt gerade alles sehr kompliziert.
Wo wir uns einige sind: Die Logik darf das Oberfläche nicht kennen und muss somit auch ohne Funktionieren. Die Oberfläche interessiert sich aber für neue und geänderte Daten, damit diese ggf. Dargestellt werden können. Die Geschäftsobjekte sollte es nun an einer Stelle im Quelltext geben. "single point of truth" Daher werfe ich mal eine DIY Lösung ein, welche mit Ereignissen arbeitet, welche die Logik zur Verfügung stellt. Der klassischen Ansatz "Observer Pattern". Vielleicht auch das "Publisher-Subscriber Pattern". Siehe auch: ![]() ![]() |
AW: Trennung von GUI und Logik
Zitat:
Dazu dann eine dumme(?) Frage: Gegeben sei eine Logik die mit Hilfe von Threads irgendwelche Daten bearbeitet.Innerhalb des Treads wird regelmäßig ein Postmessage mit dem aktuellen Stand der Bearbeitung abgesetzt. Was passiert, wenn die GUI diese Messages nicht kennt? Gruß K-H |
AW: Trennung von GUI und Logik
Zitat:
Zitat:
|
AW: Trennung von GUI und Logik
Seitdem ich viel mit Angular und RxJS mache weiß ich die Eleganz des Observer-Patterns erst richtig zu schätzen. Allerdings ist dort die GUI auch threadsafe, da müsste man in Delphi halt etwas aufpassen, machbar ist das aber auch.
|
AW: Trennung von GUI und Logik
Zitat:
als erste Anweisung nach dem begin das hier reinschreibt: ShowMemoryLeaksOnSHutdown := true; Dann Programm ausführen, die entsprechende Aktion aufrufen und Programm beenden. Kommt keine Meldungsbox beim Beeenden gibt's kein leak, kommt eine ist eines vorhanden. |
AW: Trennung von GUI und Logik
Hallo,
seit ein paar Tagen bin ich nun dabei, die hier vorgeschlagenen Methoden der Kommunikation zwischen GUI und Logik auszuprobieren. Dabei wird für mich immer deutlicher, dass eine absolute Trennung garnicht möglich ist, wenn die Logik während der Abarbeitung des Auftrages, der vom GUI erteilt wurde, Informationen an das GUI übermitteln bzw. Aktionen des GUI auslösen soll. Die absolute Trennung funktioniert doch eigentlich nur, wenn ausschließlich der Rückgabewert einer Funktion ausgewertet werden soll. Denn sowohl beim Absetzen von Windows-Messages als auch beim Feuern von Events muss die Logik doch wissen, wohin 'gezielt' werden soll, um eine bestimmte Aktion des GUI auszulösen. Wie soll das gehen, wenn Logik und GUI überhaupt nichts voneinander 'wissen'? Hier meine Implementierung eines Update - Events, das tadellos funktioniert. Die Deklaration in der Logik-Klasse sieht folgendermaßen aus:
Delphi-Quellcode:
So wird das Event in der Logik-Klasse ausgelöst:
type
TUpdateGUIEvent = procedure(Sender: TObject; index:Integer; s: string) of Object; TGlExLogic = Class private FUpdateGUI : TUpdateGUIEvent; Procedure UpdateGUI(Index:Integer; s: string); [...] public property OnUpdateGUI: TUpdateGUIEvent read FUpdateGUI write FUpdateGUI; End;
Delphi-Quellcode:
Die Implementierung im GUI sieht dann so aus:
Procedure TGlExLogic.UpdateGUI(Index:Integer; s :string);
begin If Assigned(FUpdateGUI) then begin FUpdateGUI(Self, Index, s); end; end;
Delphi-Quellcode:
Die Logik muss doch z. B.wissen, welche Parameter mitgegeben werden müssen, um bei einem bestimmten Control der Mainform ein Update auszuführen?! Es ist dann aber auch nie auszuschließen, das bei Änderung des GUI das eine oder andere nicht mehr funktioniert.procedure TGL4Mainform.FormCreate(Sender: TObject); begin GLMainLogic := TGlExLogic.create(self); GlMainLogic.OnUpdateGUI := UpDateGUI; end; Procedure TGL4Mainform.UpdateGUI(sender:TObject; Index:Integer; s :string); begin case index of 1: Memo1.Lines.Add(s); 2: Memo2.Lines.Add(s); 8: label8.caption := s; [...] usw. end; end; Da wird man wohl notgedrungen Kompromisse machen und von der reinen Lehre der OOP abweichen müssen. Oder habe ich da irgendwas nicht verstanden? :? Gruß LP PS: Die oder Das GUI? Nach den geltenden Regeln heißt es doch im Deutschen das Interface? Oder doch nicht?? |
AW: Trennung von GUI und Logik
.. die Oberfläche muss doch wissen, welches Element es updaten muss.
Das hat nicht zwingend etwas mit dem Wissen der Businesslogik zu tun. Wie das Ergebnis zustande kam, welches die Oberfläche aktualisieren muss darüber weiß die Oberfläche nichts. Ich weiß nicht, ob es nicht vielleicht sinnvoller wäre ein Ereignis pro GUI Element zu haben? Grüße Klaus |
AW: Trennung von GUI und Logik
Die Logikschicht muss oder gar darf von der Oberfläche nichts wissen. Im Übrigen kann es ihr ja auch egal sein, ob ein String jetzt in einem Label, einem Edit oder als ListBox-Item dargestellt wird. Sie stellt einfach nur Ereignisse zur Verfügung, an die sich die Darstellungsschicht einklinken kann oder auch nicht.
|
AW: Trennung von GUI und Logik
Zitat:
Zitat:
Muß die Logik sich darum kümmern was aus ihren Messages wird oder nicht? Gruß K-H |
AW: Trennung von GUI und Logik
Wieso sollte sie? Die Darstellung muss die Logik kennen, aber nicht umgekehrt.
|
AW: Trennung von GUI und Logik
Zitat:
Möglichkeiten, so etwas zu programmieren gibt es mehrere. Mir gefällt es aber nicht, irgendwelche Methoden händisch zu schreiben, die den Austausch abwickeln. Statt dessen wünsche ich mir ein Framework, dass das abstrakt im Hintergrund erledigt. Z.B. habe ich in der BL Personen in einer Liste "Familie". Dann will in der GUI (für mich: "DIE Schnittstelle") eine Listbox binden an "Familie" und dort einstellen, dass die Items die Eigenschaft "Vorname" verwenden sollen (natürlich etwas verkürzt dargestellt). Jetzt soll die Liste Zitat:
Ist in der Listbox oder der Liste keine Sperre eingestellt, muss diese auch bearbeitet werden können (ähnlich DBControls). Dazu müssen BL und GUI sich austauschen können, ohne dass beide sich wirklich kennen. Es muss eine Kommunikationsschnittstelle geben, die mit beiden Seiten klar kommt, ohne selbst wieder etwas von BL und GUI zu kennen. Sie muss also sehr abstrakt arbeiten. Ich habe mich mal an einem Framework versucht: ![]() Du kannst ja mal etwas stöbern. Aus meiner Sicht war der Ansatz schon sehr gelungen. Das Projekt habe ich aber eingestellt und arbeite an etwas neuem (was noch länger dauern wird, wovon ich aber die grundsätzliche Funktionalität schon mal festgestellt habe). |
AW: Trennung von GUI und Logik
Hallo,
![]() Vielleicht hilft das Video weiter. Bis bald Chemiker |
AW: Trennung von GUI und Logik
Oder
![]() |
AW: Trennung von GUI und Logik
Moin...8-)
Zitat:
![]() Zitat:
Zitat:
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 21:06 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz