![]() |
Globale Variablen/Abhängigkeiten = Böse... Und nu?
Hi,
Man liest ja immer wieder, das globale Variablen böse sind. Nun, sie sind ja gar nicht böse, diese unschuldigen Dinger, aber die Verwendung. Warum? Wegen der Abhängigkeiten, die man sich damit einhandelt, d.h. die Methode/Klasse, die globale Variablen verwendet, ist ohne diese Variablen nicht lebensfähig. Bestimmt gibt es noch andere Gründe und man könnte ganze Seiten füllen, aber das mache ich hier nicht. Also soll man gar keine globalen Variablen verwenden? Blöderweise geht es (nicht immer) ohne sie nicht. Wie also dann? Was sind 'best practice'? Beispiel (nicht von mir): Businessobjekte müssen prüfen, ob der angemeldete Benutzer über das entsprechende Recht verfügt, die Aktion des BO auszuführen. Also sieht die Aktion vielleicht so aus:
Delphi-Quellcode:
Hier ist der Fehler der, das ich damit das BusinessObject in seiner Verwendung einschränke. Ich werde es nie mit einem anderen User durchführen können, immer nur mit dem angemeldeten Benutzer. Lösung: Der Benutzer, der die Aktion durchführt, wird per DI zur Verfügung gestellt. Einfach ausgedrückt: Das BO bekommt eine Property 'User', die auch im Konstruktor übergeben wird. Der Anwender des BO übergibt dann vielleicht den 'CurrentUser' (und hat dann die Abhängigkeit an der Backe),
Procedure TMyBusinessObject.Action();
Begin If not GlobalStuff.CurrentUser.IsGranted (CanExecuteMyBusinessAction) then Raise ERightsException.Create('You are not allowed to do this'); PerformAction(); End; aber vielleicht gibt es auch Szenario, in dem gar kein User angemeldet ist (Test, Batch processing etc.) Anderes Beispiel: Eine Meldung ausgeben. Eine Methode soll bei Erfolg eine Messagebox anzeigen.
Delphi-Quellcode:
Hier habe ich zwei Abhängigkeiten ('ShowMessage') und 'ShowError'. Nehmen wir an, die sind als Prozeduren so in Vcl.Dialogs deklariert. Schon habe ich mich bei 'TMyCommand' auf die Verwendung der VCL festgelegt. Ich kann das zwar auch abstrahieren, aber hier wird es dann albern, weil ich nicht finde, das man -nur weil Gurus sagen, das globale Abhängigkeiten böse sind- alles nachmachen muss, was angeblich richtig ist. Denn irgendwann wird es unübersichtlich und -jetzt kommts- unnötig kompliziert. Ich fange dann nämlich an, einen Wrapper zu schreiben, der z.B. die Messageboxen kapselt. Gut, wenigstens hätte ich dann nur noch eine Abhängigkeit:
Procedure TMyCommand.Execute();
Begin Try BusinessObject.Action(); ShowMessage('Success'); except On e:Exception do ShowError(e.Message); end End;
Delphi-Quellcode:
So, und nun könnte ich den UIWrapper wieder als DI/Property angeben und wäre dann irgendwie aus dem Schneider. Nur müsste das Kommando nun zwei Abhängigkeiten haben, denn -logisch- der User wird ja auch von außen übergeben.
Procedure TMyCommand.Execute();
Begin Try BusinessObject.Action(); GlobalStuff.UIWrapper.ShowMessage('Success'); except On e:Exception do GlobalStuff.UIWrapper.ShowError(e.Message); end End;
Delphi-Quellcode:
Nun soll das Kommando noch loggen und einen Report als Resultat der Business Action ausgeben. Puh, noch zwei globale Abhängigkeiten (Logger und ReportEngine). Gut, das Pattern ist ja bekannt, ergo
Constructor TMyCommand.Create (aUser : IUser; aUIWrapper : IUIWrapper ...)
begin BusinessObject := TMyBusinessObject.Create (aUser); UIWrapper := aUIWrapper; End; Procedure TMyCommand.Execute(); Begin Try BusinessObject.Action(); UIWrapper.ShowMessage('Success'); except On e:Exception do UIWrapper.ShowError(e.Message); end End;
Delphi-Quellcode:
Ach, das BO persistiert die Daten ja auch noch in eine Datenbank... Kein Problem, dann wird die Datenbankverbindung hinzu, und zwar ein Wrapper, ist ja klar, denn auf einen Provider will man sich ja nicht festlegen. Und auf die globale Connection will man nicht zugreifen, weil ... böse.
Constructor TMyCommand.Create (aUser : IUser; aUIWrapper : IUIWrapper; aLogger : ILogger; aReportEngine : IReportEngine)
begin BusinessObject := TMyBusinessObject.Create (aUser); UIWrapper := aUIWrapper; ... End;
Delphi-Quellcode:
Ist das wirklich so gewollt?
Constructor TMyCommand.Create (aUser : IUser; aUIWrapper : IUIWrapper; aLogger : ILogger; aReportEngine : IReportEngine; aDatabaseConnection : IDbConnection)
begin BusinessObject := TMyBusinessObject.Create (aUser, aDatabaseConnection); UIWrapper := aUIWrapper; ... End; Procedure TMyCommand.Execute(); Begin Try BusinessObject.Action(); ReportEngine.PrintReport(BusinessObject); UIWrapper.ShowMessage('Success'); Logger.Success('Action'); except On e:Exception do UIWrapper.ShowError(e.Message); end End; Und wenn nicht: Wie macht man das denn richtig? Ich z.B. habe bemerkt, das ich eigentlich fast immer etwas falsch designt habe, wenn ich eine globale Abhängigkeit einführe. Aber in diesem Beispiel? Ich könnte alles, was es so an globalem Zeugs gibt, auch in eine einzige Klasse packen. Dann würde ich nur einen Parameter an den Konstruktor übergeben, aber genau genommen ist das genauso bescheuert wie die Verwendung eines globalen Scopes. Wie macht ihr das? Oder schreibt ihr Anwendungen, bei denen die Klassen eigentlich nicht/selten wiederverwendet werden, weil sie ja doch sehr problemspezifisch sind? |
AW: Globale Variablen = Böse... Und nu?
Albern ist das richtige Wort. Theorie und Praxis wäre eventuell die sachlichere Zusammenfassung der Problematik.
Also: Globales vermeiden wo möglich, nutzen wo nötig. Sherlock |
AW: Globale Variablen = Böse... Und nu?
Ja, das ist wirklich so gewollt.
Du willst das aber - aus guten Gründen - nicht immer alles von Hand zusammenstöpseln. Ein guter IoC-Container nimmt Dir diese Arbeit ab und löst die Abhängigkeiten selber auf und instanziert Dir diese Klassen mit den Monster-Konstruktoren von alleine. Du kannst natürlich auch dort, wo es Sinn macht, Argumente zusammenfassen. z.B. der UIWrapper und den Report (Ein- / Ausgabe für den User) und den CurrentUser, die Connection und das technische Logging (Context). Die Datenbankverbindung / Transactionhandling und das Logging würde man im übrigen vermutlich eher über Aspekte als über DI lösen, aber prinzipiell passt das sonst so. |
AW: Globale Variablen = Böse... Und nu?
Kann es sein, dass der Titel falsch ist?
Bis jetzt geht es um globale Abhängigkeiten und nicht um globale Variablen wie der Titel vermuten lässt. :gruebel: |
AW: Globale Variablen = Böse... Und nu?
Zitat:
Zitat:
D.h. (Hab kein aktuelles Delphi mit Generics, d.h. vielleicht Syntaxfehler)
Delphi-Quellcode:
Dann ist der Anwender des Containers wieder auf eine Globale Instanz des IocContainers angewiesen. Außer, ich gebe ihm das wieder per DI mit, oder wie?
Function TMyIocContainer.CreateCommand<TCommand : ICommand> : TCommand;
Begin result := TCommand.Create(CurrentUser, VCL_UIWrapper, TextFileLogger, FastReportReportingEngine, AnyDACConnection); End; Zitat:
|
AW: Globale Variablen = Böse... Und nu?
Zitat:
Delphi-Quellcode:
oder bei neueren Delphi Versionen
unit FooUnit;
interface type TFoo = class end; //var // Foo : TFoo; function Foo : TFoo; implementation var _Foo : TFoo; function Foo : TFoo; begin if not Assigned( _Foo ) then _Foo := TFoo.Create; Result := _Foo; end; finalization _TFoo.Free; end.
Delphi-Quellcode:
Trotz allem bleibt aber immer noch die Abhängigkeit bestehen.
unit FooUnit;
interface type TFoo = class private class var _Foo : TFoo; class destructor Destroy; public class function Default : TFoo; end; implementation class function TFoo.Default : TFoo; begin if not Assigned( _Foo ) then _Foo := TFoo.Create; Result := _Foo; end; class destructor TFoo.Destroy; begin _Foo.Free; end; |
AW: Globale Variablen/Abhängigkeiten = Böse... Und nu?
Entschuldigt, aber insbesonders das Problem mit dem "User" kann ich nicht nachvollziehen.
Der Benutzer sitzt vor der Tastatur und ein Anwendungsprogramm hat mit ihm selten etwas zu tun. Es sei denn es gibt eine Benutzerverwaltung. Und diese sollte den Benutzer "global" bevorraten, und auf Anfrage des "Arbeitsteils" die gewünschten Informationen liefern. Nicht mehr und nicht weniger. Gruß K-H |
AW: Globale Variablen/Abhängigkeiten = Böse... Und nu?
Zitat:
Was ist nun, wenn Du den Code automatisch testen willst? Du hast dann keinen User der sich eingeloggt hat, der hier global bereitstehen könnte. Du willst den Test ggf. mit mehreren gemockten Usern und entsprechenden mehreren Rechte-Kombinationen testen um sicher zu gehen, dass der Code in allen Fällen mit allen notwendigen oder mit fehlenden Rechten wie entsprechend erwartet funktioniert. Dazu ist es am einfachsten, den jeweils entsprechend präparierten Fake-User für den Test reinzugeben. Bei einer globalen Abhängigkeit ist es schwierig bis unmöglich das so zu instrumentieren dass für jeden Test der korrekte User geliefert wird. |
AW: Globale Variablen/Abhängigkeiten = Böse... Und nu?
Zitat:
Gruß K-H |
AW: Globale Variablen/Abhängigkeiten = Böse... Und nu?
Abhängigkeit und schwer testbar
Delphi-Quellcode:
unit FooUnit;
interface type TFoo = class property Value : string; end; function Foo : TFoo; implementation var _Foo : TFoo; function Foo : TFoo; begin if not Assigned( _Foo ) then _Foo := TFoo.Create; Result := _Foo; end; end.
Delphi-Quellcode:
Das sollte man besser so machen:
unit BarUnit;
interface uses FooUnit; type TBar = class function GetValue : string; end; implementation function TBar.GetValue : string; begin Result := Foo.Value; end; end.
Delphi-Quellcode:
Jetzt kann
unit BarUnit;
interface uses FooUnit; type TBar = class private FFoo : TFoo; public constructor Create( AFoo : TFoo ); function GetValue : string; end; implementation constructor TBar.Create( AFoo : TFoo ); begin inherited; FFoo := AFoo; end; function TBar.GetValue : string; begin Result := FFoo.Value; end; end.
Delphi-Quellcode:
auch mit unterschiedlichen
TBar
Delphi-Quellcode:
-Instanzen getestet werden.
TFoo
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 20:36 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