Über mich | Kontakt | Archiv

Thomas Bandt

Dieses Blog wird nicht mehr gepflegt. Zur neuen Website!

Null Verständnis

Na da habe ich ja was ausgelöst heute Morgen ;-). Grundsätzlich ist das Feedback richtig und ich denke auch, dass hier ein allgemeiner Konsens herrscht. Hätte ich die Anwendung, für die ich das Problem vermeintlich gelöst habe, auch einmal live ausgeführt, hätte ich den Code so wahrscheinlich auch nie veröffentlicht, da es mit dem Null-Result schon bei der ersten Verwendung gekracht hätte (übrigens ein netter Nebeneffekt loser Koppelung und der test-getriebenen Entwicklung: ich schraube stundenlang an einer Webanwendung, ohne sie auch nur einmal im Browser auszuführen und so die einzelnen, losen, Komponenten in Aktion zu sehen *).

Trotzdem möchte ich noch einmal kurz auf Ralf eingehen, der in einem Kommentar auf Ilker schreibt:

"Um der Diskussion willen sage ich deshalb mal: In 80% der Fälle, wo mir jmd sagt, seine Verwendung von Null sei nun aber wirklich unumgänglich, kann ich zeigen, dass es ohne Null nicht schlechter, sondern eher besser ist."

Okay, da bin ich jetzt gespannt :-). Nehmen wir den klassischen Fall:

   1:  public interface IUserRepository
   2:  {
   3:      User GetUserByID(Guid userID);
   4:  }

Was sollte die Methode GetuserByID nun zurückgeben, wenn kein User mit der angegebenen ID gefunden wird. Null oder nicht null, das ist hier die Frage?

Meine Antwort (zur Dikussion): null.

Begründung: ein "leeres" User-Objekt als Rückgabewert macht für den Aufrufer keinen wirklichen Sinn. Es muss ja sowieso geprüft werdne, ob der User nun gefunden wurde oder nicht - und ob ich nun auf user == null oder user.ID == Guid.Empty prüfe ist egal. Dafür schließe ich mit null aber Seiteneffekte bei unbedachter Verwendung aus, denn es besteht damit gar nicht erst die Gefahr, dass das Objekt noch irgendwo durchgeschliffen und fälschlicherweise verwendet wird, da jeder Zugriff sofort in einer NullReferenceException endet.

Andere Meinungen?

* Disclaimer: natürlich tue ich das abschließend schon, nur gab es in diesem Fall und in diesem Zeitraum dazu keinen Anlass.

Kommentare

  1. Thommy schrieb am Samstag, 1. Mai 2010 19:59:00 Uhr:

    Dies ist m.E. einer der klassischen Fälle für eine benutzerdefinierte Exception. Die Funktion soll einen User mit Hilfe einer gegebenen ID suchen und zurückgeben. Wenn es diesen User nicht gibt ist das für mich ein Fehler, der auch entsprechend zu behandeln ist.

    Just my 2 cents.

    Thommy
  2. TOM_MUE schrieb am Samstag, 1. Mai 2010 20:12:00 Uhr:

    Hi Thomas,

    also nur NULL als Rückgabe bei der Abfrage nach Usern in einem Repository wäre mir persönlich zu wenig. Was hältst Du denn von einer weiteren Methode in Deinem Repository, bei der Du abfragen kannst, ob der benötigte User existiert und wenn ja in welchem Zustand. Ich denke gerade bei einer Benutzerverwaltung kann das sehr sinnvoll sein. Hier mal mein Gedankenanstoß in Code (ich hoffe das, das hier im Kommentar funktioniert):

    public interface IUserRepository
    {
    bool ContainsUser(Guid userGuid, out SearchResult searchResult);

    IUser GetUser(Guid userGuid);
    }

    public enum SearchResult
    {
    NotFound,
    Deleted,
    NotPublic,
    NotAccessible
    }

    public interface IUser
    {}

    Ich denke das könnte die Auswahl der Antworten über das UI, für den Fall das kein User angezeigt werden kann, etwas vereinfachen. Oder?

    Gruß
    TOM
  3. Thomas schrieb am Samstag, 1. Mai 2010 20:17:00 Uhr:

    @Thommy:

    Ich erweitere auf:

    User GetUserByName(string name);

    Szenario:

    Benutzerregistrierung, check ob ein User mit diesem Namen schon existiert. Will man da eine Exception handlen?
  4. RalfW schrieb am Samstag, 1. Mai 2010 20:22:00 Uhr:

    Danke für die Herausforderung, Thomas! Die nehme ich an. Was sollte also zurückgeliefert werden, wenn ein User mit der Id nicht existiert?

    Ich bleibe dabei, dass Null zwar naheliegend ist, aber letztlich die falsche Lösung. Sie ist unr eben in unserem Entwicklerstammhirn ziemlich fest verdrahtet ;-)

    Aber wir sind ja keine Reflexmaschinen. Wir können umlernen.

    Also treten wir zurück und fragen erstmal: Wie steht es um die Kategorien? Was wird als Ergebnis erwartet? Bei der Mengenquery wurde ein Container erwartet und Null war keiner, also falsche Kategorie. Wie ist es aber hier? Es wird ein User, ein Objekt erwartet.

    Wenn Null zurückkäme, dann würde das signalisieren: "kein Objekt". Hört sich gut an. Aber es wäre weiterhin ein sehr unspezifisches Nicht-Objekt.

    Ich fände es daher in diesem Zusamenhang besser, einen NonExistentUser zurückzugeben. Der Vergleich wäre dann:

    var user = repo.GetUserById(...);
    if (user == User.NonExistent)
    ...

    Ist das nicht auch sprechender als if (user == null)?

    Dass so ein User fälschlicherweise irgendwo weiterverarbeitet würde, glaube ich nicht. All seine Eigenschaften/Methoden würden eine Exception werfen (throw new NullObjectAccessException<User>()). Das wäre sogar noch Aussagekräftiger als irgendwo eine Null Ref Exception zu sehen. Es wäre vielmehr sofort klar, was für eine Art Objekt da nicht existent ist.

    Auch wenn ich diese Lösung schon für ausreichend halte, gibt es aber noch eine andere: GetUserById() wirft eine Exception.

    Warum das? Weil der Zugriff auf eine Entität per Id etwas anderes ist als eine Query. Mengenabfragen liefern immer eine Menge zurück; die kann allerdings auch leer sein.

    Fragen nach Identitäten können aber eigentlich keine "leere Identität" zurückliefern. (Was wäre das? Ein Template?) Woher sollte denn auch die Identität kommen (die GUID), nach der man sucht? Es muss eine zugehörige Entität geben. Alles andere ist ein Fehler!

    Ergo: Wenn es keinen Datensatz zur GUID gibt, wird nicht Null und auch kein NonExistentUser zurückgegeben, sondern eine Exception geworfen. Ja, das halte ich sogar für die konsequentere, die bessere Lösung. Das entspricht der Semantik von Identität.

    Puh, Herausforderung gemeistert ;-)

    -Ralf
  5. Alex schrieb am Samstag, 1. Mai 2010 20:24:00 Uhr:

    Enum ist nicht so prickelnd im Kontext von OCP:
    http://www.tomdalling.com/software-design/solid-class-design-the-open-closed-principle

    @Thomas #3:
    Wenn ich das Problem einfach mal im Kontext des Use Cases "Benutzer meldet sich an" betrachte:

    Ist der User, der eine falschen Benutzernamen eingibt, der Normalfall oder eine Ausnahme?

    Von daher würde ich auch zur Exception tendieren. Zumal Du die mit z.B. IDataAccessException noch nach OCP aufbauen kannst, wenn Du Fallunterscheidungen liefern willst.

    Kann aber auch sein, dass ich ganz falsch liege - just my 2ct ;-)
  6. Carsti schrieb am Samstag, 1. Mai 2010 20:32:00 Uhr:

    Ich habe ja Exceptions noch bei Ada in den Neunzigern gelernt, und da gab es wenig Diskussion: Rückgabewerte sind keine Fehlerbehandlung.

    Natürlich gibt's 'ne UserNotFound-Exception. Warum haben wir den Exceptions, wenn wir nichtgefunde Dateien, falsche Indizes und alles andere auch durch null ausdrücken könnten? Ob die Exception oft oder weniger oft geworfen wird, steht dabei nicht zur Debatte.
  7. Golo Roden schrieb am Samstag, 1. Mai 2010 20:37:00 Uhr:

    Hallo zusammen,

    ich tendiere ebenfalls zu der Exception-Variante - einfach weil ich erwarte, dass wenn schon ein Benutzer mit einer bestimmten ID angefragt wird, dass dieser dann auch existiert. Wenn nicht => Fehler.

    Zu der Frage mit dem Login: Wenn man die Exception hier unbedingt vermeiden will, kann man immer noch eine Methode schreiben, die einen boolschen Rückgabewert hat und sinngemäß CanUserLogin oder ähnlich heißt. TryParse lässt grüßen ;-).

    Viele Grüße,


    Golo
  8. TOM_MUE schrieb am Samstag, 1. Mai 2010 20:42:00 Uhr:

    > Puh, Herausforderung gemeistert ;-) @RalfW: glaub ich nicht ;-)

    Vielleicht schaffe ich mit meinem Ansatz Anforderungen, die es nicht gibt. Dann würde ich natürlich sofort stoppen. Das kann aber nur Thomas beantworten ;-) Aber, warum sollte ich bei dem Versuch einen User zu ermitteln gleich eine Exception auslösen? Wenn ich in ein Kino gehe, Karten für eine Vorstellung kaufen möchte, dann bekomme ich doch von der Verkäuferin auch nicht direkt eine Backpfeife. Jede gute Kassiererin gibt mir Antworten, aus denen vielleicht Optionen entstehen.
    "Eine Antwort könnte sein: Ich hätte noch eine reservierte Karte. Wenn Sie 10 Minuten warten, können Sie diese haben."
    Es könnte aber auch die Antwort geben:
    Diese Vorstellung ist für 20:00 Uhr nicht öffentlich. Kommen sie doch morgen wieder, dann können Sie eine Karte kaufen.
    Oder man hat für das Ticket keine ausreichende Authentifizierung.

    Also, warum eine Frage nach einem Objekt gleich mit einer Exception bestrafen. Für den Zugriff auf die Eigenschaft eines Objekt, dass es vielleicht noch gar nicht gibt, ja dann bin ich mit einer Exception dabei.

    Fazit: Etwas Differenzierung bei der Wahl, Exception Ja/Nein, sollte es geben. Oder?

    Gruß
    TOM
  9. RalfW schrieb am Samstag, 1. Mai 2010 20:58:00 Uhr:

    @Tom: Ich kaufe meine Kinotickets nur noch online, insofern weiß ich nicht, wie die Gepflogenheiten dieser Tage an der Kasse sind ;-)

    Bei einem Zugriff via Identität bin ich mir hingegen sicher, dass es eigentlich nicht sein kann, dass mit einer "falschen Identität" versucht wird, eine Entität zu laden. So eine Identität kann nur durch eine Inkonsistenz in die Hand des abfragenden Code gekommen sein. Und diese Inkonsistenz ist durch eine Exception anzuzeigen.

    Aber du kannst gern eine neue Herausforderung formulieren. Sie sollte nur in einer anderen Klasse liegen. Also nicht Mengenabfrage und nicht Identitätszugriff. Dann schauen wir, ob dort Null Sinn ergibt.

    -Ralf
  10. Thomas schrieb am Samstag, 1. Mai 2010 21:18:00 Uhr:

    Ich versuche beim Design der Repositories stets darauf zu achten, Methoden mehrfach verwendbar zu machen.

    Beispiel:

    Für die Authentifizierung werden E-Mail-Adresse und Passwort benötigt. Urprünglich gab es dafür eine Methode GetUserPasswordByEmail(), später kam aus anderem Grund noch GetUserByEmail() im Repository hinzu.

    Also wurde aus:

    var hashedPassword = UserRepository.GetUserPasswordByEmail(email);

    if(hashedPassword != null)
    // Passwort checken
    else
    // Login existiert nicht

    Folgendes:

    var user = UserRepository.GetUserByEmail(email);

    if(user != null)
    // Passwort checken
    else
    // Login existiert nicht

    Aus diesem Grund versuche ich dann die Methoden so allgemeingültig und mehrfach verwendbar zu halten, wie es im Hinblick insbesondere auch auf Performance Sinn macht (ein Repository.GetAllusers().Single(u => u.Email == email) würde absolut tödlich enden). Auf ein ContainsUser() wie von Tom vorgeschlagen versuche ich also zu verzichten.

    Grundsätzlich finde ich hier eine Exception angebracht, aber ich weiß nicht ob ich mir damit einen Gefallen tue, wenn ich diese dann x-fach behandeln muss (grundsätzlich versuche ich Exceptions gar nicht zu behandeln, sondern sie soweit wie möglich nach oben durchzureichen ...).

    Glaube ich muss mal ne Nacht darüber schlafen ,-)
  11. Carsti schrieb am Samstag, 1. Mai 2010 21:28:00 Uhr:

    @Tom:
    Ich galube, wir Deutschen interpretieren zuviel Sonderfall in das Wort "Exception" hinein. Es ist ein ganz normaler Fehler. Eine Ausnahme vom erwarteten. Keine Übersonderoberkatastrophe. Ab welcher Größe wir den bei Dir aus einem Fehler einer Exception? Der Aufrufer entscheidet, für wie schwerwiegend er den Fehler in seinem Kontext hält. Ignore oder Throw. Du meldest nur, das was passiert ist. Eine Exception.
  12. Alex schrieb am Samstag, 1. Mai 2010 21:32:00 Uhr:

    @TOM #8:
    Ich denke, vielleicht ist es einfach übertrieben, eine Exception als Backpfeife zu betrachten.

    Wenn Du sie einfach als Ausnahme vom Erwarteten betrachtest - was eben auch mal passieren kann - fällt es Dir vielleicht leichter, sie in Deinem Code zu akzeptieren...
  13. Thomas Freudenberg schrieb am Samstag, 1. Mai 2010 22:27:00 Uhr:

    Ich verwende bei meinen Implementierungen gerne GetXxx und FindXxx Methoden. Um bei dem Beispiel zu bleiben also GetUserByName und FindUserByName. Bei GetXxx erwarte ich, dass ein User mit diesem Namen existiert. Ist dies nicht der Fall, so ist das "exceptional" und wird entsprechend geworfen. FindXxx ist dagegen "weicher", d.h. es rechnet damit, dass kein Benutzer dieses Namens existiert, und in diesem Fall wird null zurückgegeben.
    Beliebt ist auch das Muster, FindXxx als TryGetXxx implementieren, d.h. mit Rückgabewert von Typ bool, der über den Erfolg der Suche Auskunft gibt, und einem out-Parameter vom Typ User, der ggf. das Fundstück zurückliefert.
  14. Thommy schrieb am Samstag, 1. Mai 2010 22:42:00 Uhr:

    @Thomas: Ich bleibe dabei, die GetUser sollte eine Exception werfen. Um das in #2 angesprochene Szenario umzusetzen, wrappe ich eben diese Methode mit einem UserNameExists, die eben diese Exception handeln kann.

    Thommy
  15. Thomas Freudenberg schrieb am Samstag, 1. Mai 2010 22:44:00 Uhr:

    @Thommy: Nichts anderes habe ich gesagt: GetXxx soll eine Exception werden, FindXxx dagegen gibt null zurück.
  16. Thommy schrieb am Samstag, 1. Mai 2010 22:57:00 Uhr:

    @Thomas(2) Fast. Ich würde das mit einem boolschen Returnvalue bauen, ich empfinde NULL immer als unpassend und unnötig (äquivalent zu Datenbanken) - es entspricht halt einfach nicht der Signatur der Methode.

    Thommy

    PS: Ein GraffitiCMS-Fan, sehr gut :)
  17. Thommy schrieb am Samstag, 1. Mai 2010 22:59:00 Uhr:

    Ähm, #14 war Antwort auf #3 und nicht #2. Abendliche Konfusion macht sich breit.
  18. RalfW schrieb am Samstag, 1. Mai 2010 23:12:00 Uhr:

    @Thomas: Ich bleibe dabei, dass das Herausfischen über die Id (!) eine Exception werfen sollte.

    Wenn du allerdings eine andere Abfrage machst, z.B. über die Email, dann musst du dir überlegen, was da die Semantik ist. Ist bei einer Email sichergestellt, dass es auch nur 1 User gibt, der die Email-Adr hat? Gibt es immer einen User für eine Email, nach der du fragst?

    Ich würde sagen: 1. Du solltest eine Email nicht als Identitätsrepräsentant ansehen. Dafür gibt es eben die Id. 2. Deshalb solltest du nicht GetUserByEmail() definieren, sondern FindUsersByEmail() - und diese Funktion liefert wieder einen Container zurück. In dem sollte zwar normalerweise nur ein User sein, doch garantiert ist das halt nicht wie bei einer Id. Wir sind zurück am Ausgangspunkt.

    (Falls du bei 1. widersprichst und die Email als Id ansehen willst wie eine Guid, dann gelten dafür natürlich dieselben Regeln wie für GetUserById(). Also sollte eine Exception geworfen werden, wenn es den User zur Email nicht gibt.)

    -Ralf
  19. RalfW schrieb am Samstag, 1. Mai 2010 23:14:00 Uhr:

    @Thomas Freudenberg: Ein Find...(), dass Null zurückliefert, finde ich immer noch nicht gut. Mit dem TryGetUserByEmail() hingegen kann ich mich anfreunden. Wenn es so unsicher ist, ob ein Get...() ein Resultat liefert, dann kann man den Umgang damit so einfacher machen. Den out-Param finde ich dabei nicht schlimm. Ist halt ein Pattern.

    -Ralf
  20. Ilker Cetinkaya schrieb am Sonntag, 2. Mai 2010 02:23:00 Uhr:

    Hmm,

    Ich finde die Lösung von @Thomas Freudenberg gut. Konventionalisiert, klar, und angemessener Konsens zwischen Intuition, Lesbarkeit, Abhängigkeit und Performance. Ich habe das ab und an mal bei kleineren Projekten auch so gemacht, aber noch nie "im großen Stil" durchführen können - vielleicht weil solche weichen Konventionen auch so schwer über dutzende Entwickler vermittelbar sind.

    Ansonsten würde ich Exceptions vorziehen, wenn es ein trivialer Kontext ist. Da kann ich die Argumentation von @Carsti gut nachvollziehen.

    Aber es ist auch eine pragmatische Sache, NULL bei offensichtlichen Ausnahmen anzuwenden. Genauso, wie es notwendig ist, NULL im katastrophalen Kontext einzusetzen.

    Ich bin kein großer Freund von NULL, habe es aber dennoch geschätzt, es manchmal effektiv einsetzen zu können. Deswegen finde ich es wichtig, eine gewisses "Null Toleranz"-Niveau zu entickeln.

    Siehe Blog: http://www.gmbsg.com/null-toleranz/

    Alles in Allem tendiere ich zu @Thomas Freudenberg's pragmatischer Lösung als "Best Match".
  21. Thomas schrieb am Sonntag, 2. Mai 2010 10:40:00 Uhr:

    Moin, moin.

    @Ralf #18:

    "1. Du solltest eine Email nicht als Identitätsrepräsentant ansehen."

    Doch, die E-Mail-Adresse ist absolut zu 100% eindeutig. Von daher: ja, nichts anderes als bei der ID.

    Ganz allgemein:

    Wenn man mal vom Grundsätzlichen weggeht und sich nur auf das Repository konzentriert und dieses als reinen dummen Datenkasten betrachtet, der von außen ja nicht anders angesprochen und genutzt wird als wir es früher mit unserem Select * From Tabelle gegen die Datenbank direkt gemacht haben, dann könnte man (ich) eigentlich damit leben, dass es wirklich nichts anderes macht als Daten zu liefern. Oder eben nicht. Aber eben keine Fehler generiert, behandelt oder sonstige Logik der Art enthält. Die Konvention wäre dann also:

    + Bei erwarteten Resultsets immer ein (leerer) Container
    + Bei erwarteten Einzelergebnissen das der Signatur entsprechende Objekt oder null

    Im Prinzip also das, was ich bisher gemacht habe ;-). Aber das ist wohl eher eine philosophische Frage.

    Zurück zur Diskussion:

    Ich finde den Ansatz von Thomas Freudenberg auch gut, wie Ilker schreibt "Konventionalisiert, klar, und angemessener Konsens zwischen Intuition, Lesbarkeit, Abhängigkeit und Performance.".

    Was mir gar nicht gefallen mag ist TryGetXy() da ich die obligatorische Deklaration des out-Parameters vor dem Aufruf nicht leiden kann (ich finde das nicht besonders sexy in Bezug auf die Lesbarkeit).

    Dann in dem Fall doch wieder eher eine ContainsXyz()-Methode als Wrapper, wenn die Ausnahme eher die Regel ist. Z.B. beim Login, Alex fragte danach in #5 -> wenn ich den Nutzer für die Authentifizierung anhand seiner E-Mail-Adresse hole muss ich natürlich davon ausgehen, dass ich ihn nicht finde. Das ist dann kein Fehler in der Matrix sondern womöglich schlicht eine verschusselte Nutzereingabe. Hier wäre ein Contains() dann als Alternative zur (zu behandelnden) Exception sinnvoll.
  22. Thomas schrieb am Sonntag, 2. Mai 2010 10:46:00 Uhr:

    @Ilker #20 (ich bleibe mal hier damit die Diskussion nicht so zerrissen wird :-)), du schreibst:

    Account SignIn(string user, string password);

    "Es kann alles mögliche passiert sein – keine Verbindung zum Server, falscher Server, Verbindungsfehler, Connection Timouts, Benutzer nicht gefunden, Benutzer gesperrt, Passwort abgelaufen, falsches Passwort – all dies würde ich versuchen über Exceptions oder Return-Codes zu erledigen. Die meisten der Exceptions gibt es ja schon frei Haus vom Framework. Aber für das mich Unbekannte und Unerwartete gibt es immer noch eine Rückgabe, und die heisst NULL."

    Warum in diesem Fall dann null? Etwas Unbekanntes kann ja höchstens für den Aufrufer passieren, aber innerhalb von SignIn() kannst du ja jeden Fall der Ausnahme mit einer Exception behandeln. Gerade hier wäre ein Boolean als Rückgabewert imho angebrachter. Oder?
  23. Christoph Schmid schrieb am Sonntag, 2. Mai 2010 11:21:00 Uhr:

    Da würde ich gerne mal nach Eurer Meinung fragen, für eine Funktion in der Art
    GetAddressIDByAddressNumber(AddressNumber as string) as integer

    Wenn die Adressnummer nicht vorhanden ist,ist es dann ok, wenn eine 0 zurückgeliefert wird oder sollte das anders behandelt werden?

    Gruss Christoph
  24. RalfW schrieb am Sonntag, 2. Mai 2010 12:13:00 Uhr:

    @Thomas: Du wendest implizit die Regel an "Wenn im Zweifel, dann zumindest konsistent", wenn du sagst, Null sei bei Get...() ok. Darüber lässt sich natürlich reden, wenn wir bei "Null-Regeln" sonst nicht weiter kommen. Darauf kann sich auch jeder zurückziehen. Jeder kann seine Privatregeln aufstellen, die in seinem Anwendungsuniversum gelten. Das ist besser als nix. Keine Frage. Dein Muster ist verständlich. Damit könnte am Ende womöglich sogar ich leben ;-) Ich könnte die meinen Konsent geben. (Zum Begriff Konsent mehr hier: http://soziokratie.blogspot.com/2009/08/konsent.html)

    Warum darf ein Repository aber nicht bewusst Fehler generieren? Das verstehe ich nicht. Scheint ein Entscheidungskriterium für dich zu sein, Null zurückzugeben. Wir eröffnen also eine zweite Diskussion ;-)

    Mein Pflock im Boden: Ein Repo darf selbstverständlich einen Fehler werfen. Wenn eine Query syntaktisch/semantisch nicht korrekt ist, dann ist es die Aufgabe des Repo, das zu melden. Und es gehört für mich zur Semantik eines Repo, dass nach Entitäten nur mit gültiger Id gefragt wird. Ist die Id ungültig, d.h. die Entität nicht vorhanden, dann ist das ein Fehler.

    Aber über diese Semantik lässt sich streiten, äh, diskutieren ;-)

    Dann noch zu deiner Abneigung gegen den out-Param. Wo ist da das Problem? Ich kann mich dem kollektiven Widerwillen nicht anschließen. Nur weil Uncle Bob das doof findet, muss ich es nicht auch doof finden. Wir reden ja hier über ein Pattern: Try-Methode. Davon gibt es welche im Framework, an denen man kaum vorbei kommt. Also kennen es alle.

    Im Sinne einer saubere Command-Query-Separation mag das nicht so schön sein. Aber nun... So schön CQS ist, solange mir keine Sagen kann, wie ich damit einen Stack intuitiv beschreibe, erlaube ich mir, davon auch mal abzuweichen. (Nach CQS dürfte es keine T Pop() Funktion geben.)

    Ergo: Ich bin für das Try-Pattern, weil es elegant kurz ist. Und eine Exception ist der richtige Weg, um bei Nichtexistenz aus Get...() zu antworten.

    -Ralf
  25. Thomas schrieb am Sonntag, 2. Mai 2010 13:05:00 Uhr:

    @Ralf:

    Ich hätte vor die Ausführung zum dummen Datenkasten noch schreiben sollen, dass es eine Überlegung, nur ein Gedanke, ist - nicht meine (festgefahrene) Meinung.

    Grundsätzlich bin ich überzeugt, dass bei Zugriff via ID auf eine Entität eine Exception gerechtfertig ist, so diese nicht gefunden wird. Keine Frage, das passt und das werde ich so auch übernehmen.

    Dass ich es bisher nicht getan habe liegt wohl daran, dass ich allgemein sehr sparsam mit (Custom-) Exceptions umgehe. Woher das kommt - ich weiß es nicht. Da scheint mir mein Code konfliktscheuer zu sein als ich es bin ;-).

    Zu TryGet: ich habe jetzt mal den Vergleich zusammengetippt zwischen Contains() und TryGet() um zu demonstrieren, warum es mir nicht gefällt. Dabei ist mir aufgefallen, dass TryGet() gegenüber einem Get() mit null auch keinen Unterschied macht:

    1 ("doof"):

    if(rep.ContainsUserWithEmail(email))
    {
    var user = rep.GetUserByEmail(email);
    // Do something
    }

    2 ("richtig"):

    User user;
    if(rep.FindUserByEmail(email, out user))
    {
    // Do something
    }

    3 ("falsch"):

    var user = rep.GetUserByEmail(email);
    if(user != null)
    {
    // Do something
    }

    Gestört hat mich wie gesagt die zwingende Deklaration des out-Parameters vor dem Aufruf. Aber unterm Strich ist es auch mit einer Null-Prüfung nicht weniger bzw. besser zu lesender Code. Ich kann mit dem Pattern also leben.

    Okay ... Conclusion:

    1. Wenn das erwartete Ergebnis eine Liste ist, wird diese auch immer zurückgegeben, im Zweifelsfall leer. Niemals null.

    2. Wenn per Get() auf etwas mit einer ID zugegriffen wird, von dem man in diesem Moment ausgeht, dass es existiert, wird das Ergebnis so existent zurückgegeben und andernfalls eine spezifische Exception geworfen, die die Ausnahme beschreibt.

    3. Sofern die Ausnahme eher die Regel ist oder sein kann, wird nicht Get() sondern Find() mit einem Rückgabewert vom Typ Boolean verwendet sowie dem Ergebnis als out-Parameter.

    Damit kann ich sehr gut leben und kann abschließend sagen, dass es richtig war die Frage hier so offen zu formulieren. Gestern dachte ich noch "Was soll da schon kommen? Habe ich immer so gemacht und passt".

    Wieder was gelernt :-)
  26. Thomas schrieb am Sonntag, 2. Mai 2010 13:06:00 Uhr:

    Ach ja, man ersetze Find() durch TryGet() ... ganz durcheinander.
  27. Ken schrieb am Sonntag, 2. Mai 2010 13:06:00 Uhr:

    Eine echt spannende Diskussion, die hier entstanden ist. Ich hab mir die Frage nach NULL, Exception oder doch Fehlercode auch schon gestellt.
    Ich bin dabei letztendlich zu dem Entschluss gekommen, dass ganz prakmatisch zu behandeln.
    Es gibt 2 Fälle, die abgedeckt werden müssen.

    Fall 1: Abfrage anhand eines Identitätsparameters (ID bzw. E-Mail)
    Hier wird entweder ein User gefunden, oder es fliegt eine Exception. GetUserById() impliziert für mich, dass es diesen User geben MUSS! Woher sollte sonst die ID kommen?? Wird der User nicht gefunden, ist das ein unerwarteter Fehler -> also Exception die auch bis in Frontend durchfliegt.
    Wird mehr als ein User gefunden ist auch das ein unerwarteter Fehler. Dann hab ich nämlich in meiner Anwendung etwas gravierend falsch gemacht und das will ich auch sofort bemerken - eine Identität muss halt eindeutig sein.

    Fall 2: Abfrage anhand eines Nicht-Identitätsparameters (Vorname / Nachname etc.)
    Hier erwarte ich von Natur aus mehrere Treffer. Mehrere heißt: Es kann kein User, es kann ein User oder es kann eine Liste von User gefunden werden. Hier ist es somit kein Fehler, wenn nix gefunden wird.

    Somit kann ich für mich sagen, dass ich nie auf NULL prüfen muss - sondern nur auf eine leere Liste.
    Mein Repository bietet mir zur Abfrage nur folgende Methoden:
    GetUsers(UserFilter filter); -> liefert eine Liste der User, die mit dem übergebenen Filtern matchen.
    GetUserCount(UserFilter filter); -> liefert die Anzahl der User, die mit dem übergebenen Filtern matchen.

    Die Überprüfung, ob es sich nur um Fall 1 oder Fall 2 handelt und wie damit umgegangen wird, macht der BusinessLayer.

    Zu dem Beispiel mit der Benutzerregistrierung von Thomas (#3):
    Im BusinessLayer gibts die Validierung für die Eingabe. ValidateRegistration(). Innerhalb dieser Validierung wird das Repository befragt, wieviele Benutzer es mit der entsprechenden Eigenschaft kennt. GetUsersCount(). Liefert das Repository einen Wert größer 0 ist es ein Validierungsfehler und der Benutzer kann sich mit seinen Daten nicht registrieren.

    Man könnte das Repository auch noch dahingehend erweitern, dass es keinen COUNT liefert, sondern nur ein BOOL. UserExists(). Das wäre dann das ContainsUser() aus Beitrag #2.
  28. TOM_MUE schrieb am Sonntag, 2. Mai 2010 13:52:00 Uhr:

    Hallo @all ;-)

    Wow, so ein reger Austausch von Erfahrung und Ideen gefällt mir! Wenn diese Runden auch noch mehr in die UG-Treffen kommen, wäre ich ein echt glücklicher UG-Leader ;-) OK, ich schwuffte ab.

    Nach dem ich nun eine Nacht über das Thema geschlafen habe, möchte ich ganz gerne meinen ersten Ansatz etwas nachbessern und auch etwas besser erklären. Ich glaube in meinem zweiten Kommentar hatte ich mich unvollständig ausgedrückt 8-(

    @Carsten&@Alex: Mein Bild zu Exceptions als Backpfeife kommt aus der intensiven Entwicklungszeit mit dem Automatisierungs-Objektmodel von Visual Studio. Hier wurden in vielen Dictionaries und auch Repositories nur Get-Methoden angeboten. War das gesuchte Objekt nicht da oder entsprach nicht dem erwarteten Typ, wurde sofort Exception ausgelöst. Und das kostet viel Nerven und auch Performance. Code-technisch wird es am Ende die absolute try & catch Hölle. Der Wunsch nach einer Möglichkeit, eine Contains-Methode oder den as-Operator verwenden zu können ist in diesen Scenarios riesig und sollte meiner Meinung nach in keinem Repository vergessen werden.

    Contains/Find/Get

    @Aelx: OK, Enums != OCP ist gekauft ;-) Da steckte wohl der Lucky Luke in meiner Tastatur. Mein Lieblingsartikel zu OCP ist dieser hier: http://tinyurl.com/6gjw69

    Für mich gibt es drei „must-have“ Anforderungen an ein Repository.

    Contains: Die Methode sollte mir einfach den Hinweis geben ob das gesuchte überhaupt existiert. Da, wie bereits festgestellt, ein Enum als out-Parameter keine gute Wahl ist, sollte die Methode ein Boolean zurückgeben. Das ist für den Verwender die schnellste Variante herauszufinden, ob das Gesuchte überhaupt existiert.

    Find: Die Methode Find gibt mir die Möglichkeit ein Flyweight des gesuchten Typen als Referenz zu erhalten. Das ist meiner Meinung nach immer dann sehr wichtig, wenn noch gar nicht fest steht wann und von wem das oder die gesuchten Objekte verwendet werden. Findet die Methode Find nichts, gibt sie NULL zurück.

    Get: Für diese Methode finde ich es gut, wenn ich bei der Verwendung einen Typ in Verbindung mit der entsprechenden ID/oder des Keys angeben kann. Das würde die Wiederverwendung des Entwurfs erhöhen. Zusätzlich ist es meiner Meinung nach ein guter Ansatz, das Auslösen einer Exception über einen booleschen Parameter steuern zu können. True, es wird eine Exception ausgelöst und false, die Methode gibt einfach Null zurück. Wie schon angesprochen, Exceptions kosten viel Zeit und das kann den Entwurf für einige Szenarien unattraktiv machen.

    Fazit: Aus meiner Erfahrung heraus ist es für ein Repository, das nicht nur in einem Szenario verwendet werden soll, sehr wichtig, eine gewisses Maß an Flexibilität mitzubringen. Nebenbei weiß ich, dass eine strenge Verwendung von Exceptions aus Gründen der Performance dazu führen kann, dass die angebotenen Patterns und APIs gern umgangen werden. Und wir wissen doch alle, dass Architekten, die die Einhaltung von Architekturvorgaben überwachen müssen, eine aussterbende Spezies sind. Viel besser ist es doch, wenn sich die Entwicklerteams in ihren Sprints mit entsprechenden Frameworks, APIs etc. selbst organisieren können und Herausforderungen, wie diesen Post von Thomas, durch konstruktive Diskussionen alleine lösen. :-) >Träume?<
    TOM
  29. ndeuma schrieb am Sonntag, 2. Mai 2010 15:42:00 Uhr:

    Ich würde mich auch bei getUserById() trauen, null zurückzugeben. Jedenfalls in den meisten Fällen.

    In Java arbeite ich mit Assertions bzw. FindBugs (http://findbugs.sourceforge.net/) und der @CheckForNull-Annotation. Das ist dann so etwas wie eine maschinenlesbare API-Dokumentation, und FindBugs, das bei uns eh im Buildprozess mitläuft, kann überprüfen, ob eine Variable, die "potentiell null" ist, irgendwo später dereferenziert wird. Kann das FxCop nicht auch?

    Ausnahmen:
    1. "Verhaltens-Objekte" mit komplexer Logik und innerem Zustand (im Gegensatz zu "Werte-Objekten", die idR immutabel sind und deren Methoden hauptsächlich Getter und Setter sind). Dort kann man evtl. ein Null-Objekt zurückgeben, das immer im Default-Zustand bleibt, und nie "etwas tut". Beispiel aus meiner Praxis:

    public interface ISpellDictionary {
    public boolean isWordCorrect(String word);
    }

    public class NullSpellDictionary implements ISpellDictionary {
    public boolean isWordCorrect() {
    return true;
    }
    }

    Damit wird der Null-Fall sozusagen transparent (im Fall Rechtschreibwörterbuch wird ja letztlich auch nur eine Liste gewrappt)

    2. Wenn man sich aufgrund der Anwendungslogik sehr, sehr sicher ist, dass "id" existieren muss (wie oben schon erwähnt)

    Im Code des Projekts, an dem ich arbeite, gibt es an ein paar solcher Stellen sowohl ein
    @CheckForNull getUserCheckForNull()
    als auch ein
    getUser(), das eine Exception wirft.

    Das wäre dann auch die Lösung von Thomas Freudenberg. Aber immer eine Exception, wenn ich irgendetwas nicht finde, ist mir auch zu heftig.




  30. Thomas schrieb am Sonntag, 2. Mai 2010 18:05:00 Uhr:

    Hier noch ein Beispiel,

    was mein Problem mit TryGet() ganz gut visualisiert:

    User user;
    if (UserRepository.TryGetUserByEmail(clientID, email, out user) && SecurityService.CheckPassword(password, user.Password))
    {
    return true;
    }

    (Geschweifte Klammern für bessere Lesbarkeit hier eingefügt)

    "User user" steht da scheinbar unbenutzt und gelangweilt in der Ecke rum, man muss schon genauer weiterlesen um zu sehen dass es als out-Parameter eine Zeile später verwendet wird. Das liest sich imho nicht sehr intuitiv und sieht auch nicht gut aus.

    Aber mei, irgendwas is ja immer. Ich arrangiere mich damit ;-)
  31. RalfW schrieb am Sonntag, 2. Mai 2010 18:54:00 Uhr:

    @Thomas: Schönes Beispiel mit Try... && Check...

    Wir könnten das mit "Mei, irgendwas is ja immer" abtun und den Rest des Abends in der Sonne sitzen.

    Aber, hey, wir sind Entwickler. Also nicht lang sonnen, stattdessen nachdenken und kommentieren ;-) Also:

    Wenn es hier ein Problem gibt, dann keines, dass sich mit Null als Rückgabewert leichter lösen ließe.

    User user = repo.GetUser...(...);
    if (user != null)
    {
    if (sec.CheckPwd(user.Password, ...))
    ...
    }

    sieht nicht wirklich verständlicher aus, seien wir mal ehrlich.

    Also liegt das Problem woanders. Ich sehe es beim Seiteneffekt. Es kommt halt darauf an, dass die Anweisungen in einer bestimmten Reihenfolge abgearbeitet werden. user ist für beide ein globaler Zustand. Das (!) macht es doof - unabhängig von der Null-Problematik.

    Wie könnte es denn aber besser aussehen? Hm... Der NonExistentUser könnte hier helfen:

    User user = repo.GetUser...(...);
    if (sec.CheckPwd(user.Password, ...))
    ...

    Das ist einfach zu lesen und die Passwortprüfung würde immer fehlschlagen. Das ist ja, was passieren soll.

    Wenn wir aber noch einen Schritt weiter zurücktreten, dann sehen wir hier ein Muster. Es geht um eine Sequenz von Operationen, um einen Fluss. Der geht sogar noch weiter:

    1. User laden
    2. Passwort prüfen
    3. Irgendwas tun (auch wenn das nur "Prüfungsergebnis zurückgeben" ist)

    Warum können wir das nicht überhaupt als Sequenz notieren? Ich fabuliere mal:

    return GetUser...(...).CheckPasswort(expectedPassword);

    Das wäre doch cool, oder? In F# ginge das so ähnlich. In C# geht das auch - aber nur mit einigem Umdenken und Klimmzügen. Dazu brauchts ne Monade, würd ich sagen.

    Darüber können wir mal sinnen. Bis dahin jedoch find ich deine Lösung aber völlig ok. Wenn das der einzige Code in einer Funktion bool CheckUser...(Guid id, string expectedPassword) {} wäre, dann könnte ich damit leben.

    -Ralf
  32. GENiALi schrieb am Sonntag, 2. Mai 2010 19:11:00 Uhr:

    Ohne jetzt jede Antwort gelesen zu haben. Ich tendiere auch auf null wenn es den User nicht gibt.
    Auser es interessiert mich wie so es ihn nicht gibt. Dann muss eine andere Lösung her.
    Aber nicht mit Exceptions. Irgend wie sieht das nach Programmsteuerung mit Exceptions aus. Allerdings, wie kriege ich den User zurück und gleichzeitig eine Info über das "Wie so nicht gefunden". Ein out Parameter oder so?
    Aber eher null und danach halt noch abklären wie so. Nicht mit Exceptions das Programm steuern. Schlussendlich ist es ja kein Fehler das der User nicht gefunden wurde.
  33. Thomas schrieb am Sonntag, 2. Mai 2010 23:30:00 Uhr:

    @Geniali: hättest du mal die Kommentare gelesen ;-)

    @Ralf: Schön, dann haben wir einen Konsens. Ich habe es heute Nachmittag auch schon soweit angewendet, als dass ich die bestehenden Repositories des (zum Glück noch jungen) Repositories entsprechend refaktorisiert habe.

    Bzgl. möglichen Alternativen: das fluent zu gestalten wäre sicher nett. Aber vielleicht ließe sich das auch schon innerhalb von C# viel einfacher lösen, in dem der Out-Parameter einfach an Ort und Stelle deklariert werden würde (so es denn ginge):

    if(rep.TryGetUserByEmail(email, out< User > user)
    Response.Write(user.FirstName);

    Aber egal ... ich bin ja kein Software-Philosoph noch stecke ich tief in den Grundlagen des Frameworks drin - mit dem Ergebnis dieser Diskussion bin ich zufrieden und kann nun weiter (besser) meine Arbeit verrichten :-).
  34. Jens Hofmann schrieb am Montag, 3. Mai 2010 01:08:00 Uhr:

    Erstmal: Tolle Diskussion, hoffe sowas gibts des Öfteren :)

    Nachdem ich mich durch die Kommentare hier gelesen habe setze ich mal meine persönliche Meinung ab:

    1. Ich mag den out-Parameter nicht. Für mich gehört ein "Teil-Ergebnis" einer Methode in die Ergebnismenge und somit in den Rückgabe-Typ der Methode. Ich würde deshalb eher eine komplexe (generische) Klasse zurückliefern.
    2. Wenn eine Methode eine Menge von Elementen zurückliefert, würde ich niemals erwarten, dass diese Menge "Null" ist. Eine Menge ist höchstens leer, aber immer existent.

    Zum eigentlichen Thema:

    Sind wir mal ehrlich - wie oft interessiert einen wirklich, warum ein bestimmtest Objekt was man angefordert hat nicht existiert?
    In den meisten Fällen interessiert es mich nur, dass es nicht existiert, denn um den eigentlichen Grund für ein nicht Existieren zu finden bedarf es meist eh einer extra Recherche.

    Nehmen wir mal ein GetUserByID(...):

    Klar kann man da eine Exception werfen und sagen die ID existiert nicht. Und wie viele hier argumentieren: Woher kommt die ID? Da muss was faul sein wenn die ID nicht gefunden wird, also werf ich eine Exception!
    Ich seh das nicht so. In Systemen die hoch performant arbeiten müssen gibt es Szenarien in denen IDs durch Löschung verloren gehen, während andere Prozesse, eventuell aus Performance Gründen, mit gecachten Daten arbeiten. In solchen Szenarien kommt es dazu, dass IDs angefragt werden die eventuell nicht mehr existieren. Wenn man für dieses Szenario jetzt mal darüber nachdenkt was einem das werfen einer Exception im GetXXX() bringt, komm ich zu dem Schluss, dass mir das verarbeiten der Exception nur mehr Arbeit bringt. Sie hat für den reinen Fall der Aussage: "Du ich hab da nix mit deiner ID" keinen Mehrwehrt gegenüber einem Null-Return. Im Gegenteil:

    1. Muss der Entwickler, der die benutzte Methode nicht selber geschrieben hat, bescheid wissen, dass da 'ne Exception für kommt
    2. Muss ich nen Try-Catch für schreiben der X Zeilen bringt und den Code unleserlich macht sowie das Nesting erhöht.
    3. Unnötige Performanceeinbußen durch Exception-Handling


    Meine ganz eigene Regel ist: Get-Methoden dürfen null zurückliefern. Sollte beim "Besorgen" des Objekts selber ein Fehler auftreten werden Exceptions geworfen. Wird hingegen das Objekt einfach nicht gefunden, wird "Null" zurückgeliefert. Eine Exception, dass das Objekt nicht existiert, bringt mir keinen Mehrwert.

    Wie gesagt das ist meine Meinung. Für Diskussionen bin ich offen :)
  35. Thomas goes .NET schrieb am Montag, 3. Mai 2010 01:10:00 Uhr:

    Na das war ja ein turbulentes Wochenende, ich bin gespannt
    wie viele sich am Montag verwundert die Augen reiben werden, beim Blick auf den
    Kommentar-Counter unter diesem Post ;-).

    Bevor ich ihn veröffentlichte habe ich kurz innegehalten und
    übe ...
  36. Thomas schrieb am Montag, 3. Mai 2010 01:35:00 Uhr:

    @Jens: Wenn du für dich entscheidest, dass du auf out verzichten willst, finde ich das absolut okay.

    Allerdings finde ich inzwischen persönlich eine Unterteilung in GetUserByID(id) [muss einen User liefern oder ne Exception schmeißen] und TryGetUserByID(id, out user) [gibt klar zurück ob der User existiert] besser weil sprechender und semantisch korrekter.

    Ich muss ja beim Aufruf immer wissen ob der User definitiv in diesem Moment vorhanden sein muss, oder ob er auch bereits gelöscht sein kann und entsprechend verwende ich eine der beiden Methoden. Daraus könnte man eine Regel für alle Zugriffe erstellen, was den Code auch besser les- und wartbar macht.

    Verzichtet man darauf wird beim Lesen immer erst dann klar, ob ein User in dem Moment auch nicht gefunden werden kann, wenn man eine Null-Prüfung anhängt.

    Just my 2 cents.
  37. Jens Hofmann schrieb am Montag, 3. Mai 2010 01:53:00 Uhr:

    Worauf ich eigentlich hinaus wolte war:

    Im Normalfall will ich beim Auftreten einer Exception soviel Informationen mitgebene wie ich nur kann. Heißt: Traces anhängen und Zuständ von Objekten, die im Zusammenhang stehen warum ich ein bestimmtes Objekt überhaupt "Besorgen" wollte, mitgeben.

    Es geht doch meistens nicht nur um das eine Objekt, sondern um eine Verkettung von Abhängigkeiten. Was nützt mir also diese eine Information (exception), dass das eine Objekt nicht geladen werden kann? In den meisten Fällen macht es wenig Sinn diese eingeschränkte Aussage (Exception) hochrumpeln zu lassen, da mir dann beim Lesen der Exception (z.Bsp. in einer Mail) der Zusammenhang fehlt. Du kannst jetzt sagen: "Dann catch ich halt die Exception oben". Aber dann sind wir wieder bei der Frage: Mehrwert?

    Deswegen meine Meinung: Null ist gut! :)
  38. Thomas schrieb am Montag, 3. Mai 2010 01:59:00 Uhr:

    Nee, nix catchen. Einfach krachen lassen und benachrichtigen. Ich finde die Informationen einer spezifischen Custom-Exception (UserWithIDNotFoundException) samt Message ("User with ID '{GUID}' not found") und anhängendem Stacktrace, der den Aufruf protokolliert, schon recht aussagekräftig.
  39. Jens Hofmann schrieb am Montag, 3. Mai 2010 02:10:00 Uhr:

    Wie gesagt, da fehlt mir aber der Zusammenhang.

    Beispiel:
    - Für Objekt X soll eine Aktion ausgeführt werden
    - Dafür wird Objekt Y zusätzlich benötigt
    - Objekt Y existiert nicht

    Mit deiner Methode des hochrumpeln lassens seh ich nur durch den Stacktrace und die Exceptions, wie der Weg war und welche ID das Objekt Y hat. Sehr interessant ist jedoch auch welche ID das Objekt X hat. Denn dieses Objekt hat es eventuell verbockt :)

    Das sind die Abhängigkeiten die ich meinte.
  40. RalfW schrieb am Montag, 3. Mai 2010 08:33:00 Uhr:

    Ich finde es interessant, wie sich "das Thema" jetzt entwickelt. Am Anfang war Null allein völlig ok - und jetzt ist eine rumpelnde spezifische Exception schon nicht mehr genug.

    Mir scheint, dass es da lohnt, den Blick etwas zu weiten. Die allgemeinere Frage lautet: Was tun, wenn etwas nicht klappt?

    Da kommt für mich das Thema Instrumentierung um die Ecke. Exceptions allein machen nicht glücklich ;-) Die können zwar einige Infos transportieren, doch im Zweifelsfall fehlt dann wahrscheinlich doch Kontext.

    Wir können uns zwar über Null oder Exception als Meldung für einen Fehlschlag unterhalten. Und wir können uns über Standardexception vs custom Exception unterhalten. Das sind dann Gespräche über ein Mittel zur Meldung von Fehlschlägen.

    Doch wenn es an die Interpretation der Umstände geht, dann kann ich heute Exceptions nicht mehr allein sehen. Die sind nur eine rote Lampe, die angeht. Die sagen nur "Jetzt ist etwas passiert!" Was aber passiert ist, das zu beschreiben, ist nicht ihre Aufgabe. Die Umstände soll (!) eine Exception nicht transportieren (auch wenn sie gern einen Stacktrace haben darf und einen aussagekräftigen Meldungstext).

    Die Umstände sind vielmehr aus einem Log zu entnehmen. Das ist bei Problemen mit einem Flugzeug oder einem Schiff nicht anders. Da gibt es Black Box und Logbuch, die den Kontext automatisch bzw. manuell protokollieren. Wenn was schief geht (wie immer das angezeigt werden mag), dann schaut man in diese Aufzeichnungen, um die Umstände zu rekonstruieren.

    @Jens: Eine Exception als Signal ist für mich also das richtige Mittel. Die Id von X, also den Kontext, in dem Y nicht gefunden wurde, den zu dokumentieren, das ist nicht die Aufgabe der Exception. Das ist Aufgabe eines Log.

    Nicht nur sollten wir uns also Gedanken über den Umgang mit Misserfolgssituationen machen, sondern auch darüber, wie wir den allgemeinen Verlauf der Operationen unserer Software dokumentieren. Instrumentierung ist für mich da inzw. der Weg, über den ich nicht mal mehr nachdenken muss.

    Derzeit favorisiere ich dafür Gibraltar (http://www.gibraltarsoftware.com/). In den Code streue ich Trace.TraceInformation() oder Trace.TraceError() oder Debug.Print() usw. ein. (Alternativ kann ich auch z.B. mit log4net protokollieren.) Und dann referenziere ich nur in der "Hauptassembly" Gibraltar mit ein paar App.Config Einstellungen.

    Und anschließend kann ich entweder den Verlauf der Verarbeitung jeder Instanz (!) der Anwendung, die auf irgendeinem Rechner vor sich hinrumpelt, auf meinem Entwicklungsrechner verfolgen. Oder ich kann mir z.B. im Fehlerfall autom. ein Log zuschicken lassen.

    Einfach mal ausprobieren. Das ist so unaufwändig, dass keiner es auslassen sollte. Und mit den TraceSource-Methoden des .NET Fx pinselt man sich auch nicht so schnell in eine Toolecke.

    -Ralf
  41. Jens Hofmann schrieb am Montag, 3. Mai 2010 09:59:00 Uhr:

    log4Net kenn ich und setz ich bei einigen Projekten selber auch ein. Gibraltar ist mir neu, danke für den Link :)
  42. Thomas schrieb am Montag, 3. Mai 2010 10:43:00 Uhr:

    Was zur nächsten Frage führt - wie und wo man Logging in Form des konkreten Trace()-Aufrufs genau einsetzt. Wenn man nämlich beginnt einfach alle Methoden mit mehr als 3 Zeilen damit vollzupflanzen, hat man am Ende vielleicht schöne Logs, aber unleserlichen Code.

    Ich denke da kommt's dann auf den Anwendungsfall an - im Fall einer normalen Webanwendung reicht mir häufig einfach das Ergebnis der Exception samt Stack trace im (Windows-) Log um im Anschluss vernünftig debuggen zu können. Da verzichte ich bisher tatsächlich auf eigenes Logging.

    Da wo komplexere Dinge vonstatten gehen oder wo man nicht so leicht "ran kommt" (z.B. Windows-Dienste), wird natürlich ausgiebig geloggt :-).
  43. RalfW schrieb am Montag, 3. Mai 2010 10:53:00 Uhr:

    @Thomas: Meine ersten einfachen Regeln fürs Logging sind: 1. Beim Übergang zwischen Komponenten, 2. In hervorhebenswerten Situationen, z.B. wenn ich eine Exception abfange oder werfe.

    Darüber hinaus dann noch "Performance Counter" für beobachtenswerte Laufzeitparameter, z.B. Queries oder Kommandos zählen, die ein Repo bedient. Mit Gibraltar ist das total einfach.

    Natürlich soll nicht jede Methode instrumentiert werden.

    -Ralf
  44. Ilker Cetinkaya schrieb am Montag, 3. Mai 2010 11:33:00 Uhr:

    Tolle Diskussion und super Thread!

    Ich bleibe mal beim Ausgangsthema, der NULL Rückgabe. Exceptions, Logging und alles sind schöne Themen, trotzdem erstmal nur NULL :-)

    @Thomas #22 - Wie willst Du bei der Signatur bool zurückliefern? Account SignIn(string username, string password). Da bleibt nur Exception oder NULL. Schreiben wir mal:

    public Account SignIn(string username, string password)
    {
    Account account = null;

    if (this.authority.Authenticate(username, password))
    {
    account = this.repository.GetAccountByUsername(username);
    }

    return account;
    }

    Das ist ein pragmatischer Ansatz. Natürlich sollte und kann man einen else-Branch mit einer AuthenticationException machen. Aber hier ist der Konsens eben NULL auf Failure. Schließlich könnte Authenticate und GetAccountByUsername auch Exceptions werfen. Das ist jetzt keine schöne Sache und ich würde es auch vermeiden wollen. Gesehen habe ich solchen Code aber schon zigfach.

    Besser wäre da der TryXXX-Ansatz, wie schon in vielen Kommentaren erwähnt. Also sowas wie bool TrySignIn(string username, string password, out Account account) für mein Beispiel oder diese TryGetUser-Geschichte.

    Das finde ich in vielen Fällen absolut ok und ausreichend. Die Syntax mit dem out ist zwar nicht das schönste, tut aber was es soll. Ich habe als Alternative auch ab und an mal Envelopes gesehen, was ich perönlich vor Allem bei service-orientiertem Code nicht schlecht finde:

    interface IResult<T>
    {
    bool Success { get; }
    T Value { get; }
    }

    IResult<Account> SignIn(string username, string password);

    Das geht so ein wenig in die Nullable und Option-Ecke, wie es Ralf ja schon erwähnt hatte. Auch in Ordnung, vor Allem, wenn IResult<T> über struct als Valuetype durchgeht und dadurch abgesichert wird.

    Zu dem TryXXX möchte ich noch etwas hinzufügen, was vor Allem für Webleute und die Hochlastfreunde unter uns wichtig ist. das TryXXX-Pattern hat den Nachteil CQRS nicht zu fördern. Das wirfd ganz besonders schwierig, wenn Du die R/W's auf auf die Repository-Infrastruktur runterbrechen musst, weil die Last dadurch besser gestemmt wird. Aber das ist ein Sonderfall und kann in 90% der Fälle "unbeachtet" bleiben.

    My 2c,
    Ilker
  45. Thomas schrieb am Montag, 3. Mai 2010 12:33:00 Uhr:

    Auf TryXxx() wollte ich hinaus bzw. deiner Frage zu #22.

    Es noch einmal mit IResult zu verpacken finde ich eigentlich auf den ersten Blick auch recht elegant. Wobei:

    var result = rep.GetUserbyEmail< User >(email);
    if(result.Success)
    {
    Console.Write(result.Value.FirstName);
    }

    Auch nicht besonders hübsch (=> Value). Ich denke ich habe mit TryGet() für mich vorerst eine Variante gefunden, mit der ich mehr als gut leben kann.
  46. Christian schrieb am Freitag, 7. Mai 2010 08:24:00 Uhr:

    Hallo zusammen,
    ich hätte da auch eine kleine Frage: wie macht ihr das bei Validierungen?
    Angenommen ihr habt ihn eurer Business Logik Schicht eine Validierungsklasse die die Eingaben aus einem Formular validieren soll (Datumsprüfung usw.). Wirft eure Validierungsmethode eine Exception wenn die Validierung fehl schlägt oder arbeitet ihr da mit return codes?
    PS: Es erfolgen mehrere Validierungen in dieser Methode (für jedes Property des übergebenen Formular-Objektes)
  47. RalfW schrieb am Freitag, 7. Mai 2010 09:21:00 Uhr:

    Bei Validierungen ist der Fehlschlag/Ungültigkeit kein Sonderfall. Also halte ich Exceptions für das falsche Mittel, ihn anzuzeigen.

    Validierungen dürfen gern bool zurückliefern. Aber besser wohl eher eine Liste von Beanstandungen, die ein Benutzer dann abarbeiten kann.

    -Ralf
  48. Thommy schrieb am Freitag, 7. Mai 2010 09:26:00 Uhr:

    Ich denke, dass an dieser Stelle eine Exception nicht das Mittel der Wahl ist, da der User dann immer nur den ersten aufgetretenen Fehler visualisiert bekommt, was die Frustration beim nächsten Versuch und einem evtl. auftretenen Fehler vergrößert. Wir arbeiten in solchen Fällen immer mit einer List() von ValidationResult-Objekten, die u.a. einen Fehlercode, den lokalisierten Fehlertext und einen Schweregrad enthalten. Diese Auflistung ist dann sehr hübsch zu visualisieren.

    Grüße
    Thommy
  49. Christian schrieb am Freitag, 7. Mai 2010 09:52:00 Uhr:

    Super danke. Ich werde nun auch eine Liste mit ValidationResults zurück liefern.
  50. Thomas schrieb am Freitag, 7. Mai 2010 10:40:00 Uhr:

    Da hab' ich neulich meinen Weg zu beschrieben (genau wie hier gesagt):

    http://blog.thomasbandt.de/39/2327/de/blog/validierung-gehoert-in-den-business-layer.html


« Zurück  |  Weiter »