HowToCode: Keep it simple & was fliegt, dass fliegt!
Vor einer ganze Weile habe ich über eine Idee geschrieben, die wir eine ganze Weile auch in einem Projekt so verfolgt haben. Ein anderes geniales Konzept, wo ich mir nicht ganz sicher war, war das Exception Handling. Nachdem ich beide Ideen in einem Projekt umgesetzt hatte, kommt jetzt mein Fazit: Keep it simple!
Hintergrund:
Ich und ein Kollege von mir arbeite gerade an einer kleinen (bis mittelgroßen bzw. riesigen!
) Social Network Platform – bzw. geht es in diese Richtung. Nähere Infos zum konkreten Projekt folgen später. Wir haben uns hohe Ziele in das Design der Applikation gesteckt:
- 3 sauber getrennte Schichten
- Testbarkeit möglichst hoch
- Fetziges UI
- Gute Wartbarkeit – DRY!
Eigentlich also ein Projekt, mit SOLIDen Prinzipien und schön aussehen soll es auch
Da ich solch ein Projekt in dieser Größe noch nie gemacht habe, habe ich das ein oder andere mal bereits darüber gebloggt. Nun möchte ich schreiben, wie es bisher weiterging. Diese Projekt ist zudem ein 100%tiges Privatprojekt, wo Zeitdruck zwar da ist, allerdings nicht in dem Maße wie bei einem Kundenprojekt.
GenericResponse<AbsoluterFußschuss> <-> GenericRequest<IDEE>
Ich hatte vor einer ganzen Zeit eine Idee, unseren ServiceLayer (der die Businesslogik bei uns übernimmt) mit "Response" & "Request" Objekten anzusprechen bzw. dass er diese zurückgibt:
Die Idee dahinter war, dass man im Response z.B. reinschreiben kann, dass der Aufruf erfolgreich war oder den Fehlergrund wieder zurückgibt.
Leider muss ich sagen, dass ich zu diesem Zeitpunkt das Exceptionhandling wohl noch nicht ganz geschnallt hatte. Daher hat sich bei uns jetzt folgender Spruch eingeprägt:
Was fliegt, dass fliegt!
Sinnlos Exceptions versuchen abzufangen, die man sowieso nicht behandeln kann, bringt nichts. Das klingt einfach, aber ich wette man findet an vielen Stellen Code der so aussieht:
try
{
// Evil Things Happen Here!
}
catch(Exception ex)
{
Logger.Log(ex);
}
Nach dem Loggen wird entweder noch ein ReturnCode zurückgegeben oder es bleibt einfach so. Allerdings ist das nicht Sinn und Zweck der Sache. Im ASP.NET Umfeld würde ich einfach empfehlen, dass man unbehandelte Exceptions im Application_Error Event loggt, dem User auf die Error.aspx umleitet und eine Mail an die Entwickler rausschickt (oder ELMAH sich mal näher anschaut).
Back to the Basics
Da der Hauptgrund für diese Responseklassen eigentlich das Exceptionhandling war und wir dies jetzt ganz leicht nutzen, brauchen wir diese nun nicht.
Wenn der Nutzer was eingibt, was nicht valide ist, z.B. Logindaten oder Registrierdaten, dann werfen wir eine Exception vom Typ "UserException" mit einem Errorcode.
Keine perfekte Lösung
Die Errorcodes sind ein großes Enum und in einer UserException können auch mehrere Errorcodes untergebracht werden. Gebraucht werden diese ErrorCodes um ein genaues Feld angeben zu können, welches fehlt oder einen Fehlerstatus zurückgegeben. Momentan sieht das Enum so aus:
public enum ErrorCodes
{
DuplicateEmail,
InvalidEmail,
InvalidLogin,
InvalidPassword,
InvalidPrename,
InvalidSurname,
InvalidCompanyName,
InvalidCity,
InvalidState,
InvalidLongitude,
InvalidLatitude,
InvalidTime,
OutOfTime,
NoEmployee
}
In unserem (ASP.NET MVC) Controller können wir dann den Service Aufrufe in einem Try packen und der Catch sieht so aus:
catch(UserException ex)
{
if(ex.ErrorCodes.Any(er => er == ErrorCodes.InvalidEmail))
ModelState.AddModelError("email", "Ungültige Email-Adresse");
...
}
Dass das Enum sicher immer weiter anwachsen wird ist uns bewusst, aber eine andere Methode um ein definiertes Element im Servicelayer zur nächsten Schicht weiterzugeben war uns nicht eingefallen, wenn jemanden ohne große Magie was einfällt, dann nur zu
Ebenso ist die generelle Verwendung von Exceptions für Usereingaben (z.B. bei der Registrierung) laut MSDN auch nicht gern gesehen, allerdings muss ich sagen, dass da die Alternativen aus meiner jetzigen Sicht nicht sehr schön sind. Natürlich sollten Exceptions nur für "Ausnahmen" genommen werden, allerdings erhöhen sie meiner Meinung nach doch etwas die Lesbarkeit, anstatt X-Returncodes im Model zu definieren.
Die UserExceptions werden allerdings auch nur dort eingesetzt, wo wir das Backend definitiv ansprechen müssen (z.B. ob ein Login schon vorhanden ist).
GenericResponse
Die "GenericResponse" Klasse blieb trotzdem noch eine ganze Weile weiter im Projekt bestehen und wurde weiterhin verwendet, auch wenn wir die Features nicht mehr wirklich gebraucht haben. Dabei war allerdings der Schreibaufwand enorm.
Wir nehmen als Beispiel eine Klasse, welche einfach einen Bool zurückgibt. Mit unserer Variante musst man daraus sowas machen:
public GenericResponse<bool> DoSomething()
{
bool result = this.Repository.Get(BLABLABLA);
return new GenericResponse<bool> { Value = result };
}
Das hat die Lesbarkeit nicht sonderlich erhöht und war auch sehr bescheiden zu entwickeln. Daher ist es nun auch rausgeflogen
(nach 1,5h Refactoring)
Ganz am Anfang, ohne die Exceptions, mussten wir den Code so schreiben:
public GenericResponse<bool> DoSomething()
{
bool result = this.Repository.Get(BLABLABLA);
return new GenericResponse<bool> { Value = result, Result = ServiceResult.Succeeded };
}
Da finde ich nun die Exceptions charmanter, allerdings lassen wir uns gerne eines besseren Belehren
GenericRequest
Die Request Klasse ist allerdings aktuell noch drin, auch wenn wir uns derzeit über die Sinnhaftigkeit streiten. Einmal gibt es den eigentlich Parameter den die Methode haben will und wir schleifen in diesem Objekt immer noch den aktuellen Benutzer durch, sodass wir im ServiceLayer auch prüfen können, ob der Nutzer diese oder jene Aktion ausführen kann oder darf. Das Konzept steht soweit noch, allerdings werden wir das wohl etwas vereinfachen.
Mein Fazit
Je einfacher etwas ist, desto besser. Auch wenn die Konzepte und Ideen vielleicht gut sind, muss man sich immer fragen, ob es der Aufwand wert ist alles in X Objekte unterzuverschachteln, denn den puren Tippaufwand sollte man nicht vernachlässigen – selbst mit Resharper und allen Tricks ist ein "return true" schneller geschrieben als "return new GenericResponse<bool> { Value = true}".
So richtig glücklich bin ich mit unserem Exceptionhandling noch nicht, allerdings erfüllt es meine Anforderungen: Es ist einfach, von der Wartbarkeit her zu verschmerzen und ich kann zwischen den Schichten auch definierte "Fehler" weitergeben (durch die Errorcodes).







Alexander Groß
28. April 2009
Ist ErrorCodes ein [Flags]-Enum sein, damit man mehrere Fehler auf einen Rutsch ausgeben kann? Von Codes oder Exceptions als Validierungsergebnis würde ich absehen und lieber eine Collection mit allen Fehlern (auch in Textform) zurückgeben. Dafür gibt es auch Frameworks, wie z. B. xVal.
Andreas
28. April 2009
Es gibt dazu wirklich viele Möglichkeiten. Ich verwende meistens Hashtable die allen falsch befüllten Feldern und Gründen zurück liefert.
Der Schreibaufwand ist aber sicher genau so groß, ein kleiner Vorteil ist vielleich, dass ich pro Seitencontroller Enums definiere und nicht alle gesammelt in ErrorCodes.
Mein Hauptziel beim User-Exception-Handling ist, Fehler so nahe wie möglichen bei Fehlerquelle abzufangen. Dadurch finde ich, bleibt der Code auch relativ übersichtlich.
Dirk
28. April 2009
Bei der Validierung von Benutzereingaben unterscheide ich zwischen zwei Arten:
Formale Validierung
Prüfen, ob die Eingaben einfachen Vorgaben standhalten, wie z.B. Required, IsEmainAddress, …
Also Prüfungen, die unabhängig von Zuständen meines Domänenmodells sind.
Dies lässt sich sehr leicht mit Hilfe von Validierungsattributen (Castle.Validators, NHibernate.Validators, DataAnnotations oder andere) direkt am ViewDataModel ausdrücken
(siehe dazu auch das xVal Framework, welches diese Attributierung auch auf die Client Seite bringen kann um dort die Validierung schon mit jQuery auszuführen.)
Ich nutze die DataAnnotations inkl. eines speziellen ModelBinders (siehe auch http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html), der die formalen Fehler bereits im ModelState erfasst!
Damit kann der Controller sofort zum View zurückkehren, ohne den ServiceLayer zu bemühen.
Kontextbezogene Validierung
Diese führe ich selbstverständlich in ServiceLayer aus!
Dort wird ggf. eine Exception geworfen, deren Details dann im Controller auf den ModelState übertragen werden. Jedoch enthält die Exception bereits eine Auflistung über die betroffenen Properties und die Fehlertexte. Dies erst im Controller zu tun finde ich etwas spät.
Peter Bucher
28. April 2009
Ich habe ELMAHÂ in mehreren Projekten im Einsatz und kann es nur wärmstens empfehlen.
Vorallem im Bereich ASP.NET treten die Fehler meist dann auf, wenn man sie nicht sieht.
Hier hilft ELMAH ungemein.
Ken
28. April 2009
Erstellt man die Fehlertexte erst im Controller, ist es stark Viewbezogen. Dadurch hält man sich die Möglichkeiten offen, für gleiche ErrorCodes entsprechend dem View Fehlermeldungen zu generieren.
Passiert dies schon im ServiceLayer, ist die Fehlermeldung immer die gleiche.
Das ist ne Designfrage – genau ein ErrorCode pro Fehler – dann kann man es im Service machen.
Wird ein ErrorCode an mehreren Stellen verwendet, ist es denke ich im Controller besser aufgehoben!
Thomas
30. April 2009
Also ne Exception für ne falsch formatierte E-Mail-Adresse ist schon ziemlich hart! Und dass das Enum anwachsen wird, weißt du zwar, aber ich glaube du hast keine Vorstellung wie groß es werden wird. Erinnere dich an meine Worte, wenn du fertig bist.
Ich mache es aktuell (…) so:
Exceptions werden in der Anwendung nur dort abgefangen, wo ich sie sinnvoll behandeln kann. Also fast nie. Alles andere rutscht komplett durch und landet in Application_Error() in der Global.asax, wo ich mitlogge. Ausgegeben wird dann eine allgemeine bzw. spezifische Error-View.
Außerdem gehe ich immer fest davon aus, dass alle Daten, die ich an einen Service übergebe, vorher validiert wurden. Da sich Daten auf unterschiedlichste Weisen vorbelegen/befüllen/editieren lassen, überprüfe ich das nicht nach festen Regeln im Service, weil das letztlich immer in Abhängigkeit zum UI geschehen würde – ich überlasse das also quasi immer direkt dem Controller, und habe da auch alle Flexibilität.
Eine Ausnahme mache ich gerade bei einm Customer-Objekt, was oft verwendet wird, da gibt’s im CustomerService ne Methode ValidateCustomer die ein Dictionary für Feldname und Fehlermeldung zurückgibt, was ich hervorragend in den MVC-ModelState packen kann.