Usereingaben validieren, durch die Businesslogik schicken, dem Nutzer evtl. Fehler wieder anzeigen - fast jedes System macht sowas, doch wie macht man es "richtig"? Definiert man Errorcodes? Was ist mit Mehrsprachigkeit? In dem Post zeige ich meine Variante und bitte auch um Feedback :)
Ausgangssituation:
Ich arbeite gerade in einem Projekt mit einem Kollegen (worum es geht, werde ich später auch nochmal genauer beschreiben - spielt aber jetzt keine Rolle) und nutzen eine klassische 3-Schichten Architektur mitsamt ASP.NET MVC als Frontend.
Unser Business Layer haben wir, wie bei meinem ReadYou Projekt (was ich so nie beendet hab, aber die Ideen leben in dem anderen Projekt weiter) als "Service" Layer implementiert.
Service Layer?
Jeder Service hat ein Request und Response Objekt was jeweils rein bzw. raus geht. Dadurch hab ich dies etwas gekapselt und kann neben dem eigentlich Wert noch weitere Information rausgeben.
Warum und wie ich das mache, habe ich in diesem Post auch noch näher erklärt.
Ich hab ein kleines Demoprogramm entwickelt, welches stark vereinfacht (keine Interfaces, kein DI, keine Tests) zeigt, wie meine Variante momentan ist.
Ich bin mir nicht ganz sicher, ob es so toll ist - daher bitte ich im Feedback, wie ihr sowas löst.
Demoprojekt:
ConApp: Frontend
Model: Das Application Model - umfasst hier nur einen User
Repository: Datenzugriffsschicht - hier werden ein paar User zurückgegeben
Service: Business Logik
Das Model & das Repository:
Das Model ist sehr einfach gehalten:
public class User { public string Name { get; set; } public Guid Id { get; set; } }
Das UserRepository erstellt mir einfach 5 User:
namespace Repository { public class UserRepository { public IList<User> GetUsers() { List<User> returnList = new List<User>(); returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester0" }); returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester1" }); returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester2" }); returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester3" }); returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester4" }); return returnList; } } }
Der Service:
Der Service enthält generische Request und Response Klassen:
Request:
public class ServiceRequest<T> { public T Value { get; set; } }
Response (inklusive 2 Enums) :
public class ServiceResponse<T> { public ServiceResult Result { get; set; } public ErrorCodes ErrorCodes { get; set; } public Exception Exception { get; set; } public T Value { get; set; } } /// <summary> /// Usererror Codes? /// </summary> public enum ErrorCodes { InvalidName, NothingFound } /// <summary> /// Succeeded = Everything worked /// Failed = Userinput incorrect or business logic has detected an error /// Fatal = exception occured (e.g. db down) /// </summary> public enum ServiceResult { Succeeded, Failed, Fatal }
Hier eine kleine Erklärung:
Das ServiceResult gibt einfach an, ob der Call geklappt ("Succeeded") oder fehlgeschlagen ist. Wenn er fehlgeschlagen ist, dann kann das aufgrund von Fehleingaben sein, oder weil die Business Logik sagt, dass z.B. der Login inkorrekt ist ("Failed"). Die verschiedenen Userfehler gründe speichere ich in einem ErrorCode Enum.
Ein "Fatal Error" wäre, wenn tatsächlich das Repository z.B. eine SqlExpcetion werfen würde. Diese Exception speicher ich in einer Variable.
Code smell
So ganz bin ich mit der momentanen Lösung allerdings nicht zufrieden - ein ErrorCodes Enum? Das wird sicher irgendwann ziemlich groß.
Beim Entwickeln der Anwendung fiel mir bereits auf, dass man die Exceptions auch in die nächste Schicht über ein throw befördern könnte - ist das gut oder eher schlecht? Wenn ich es im Service mache, dann brauch ich die Exceptionbehandlung nicht im Frontend zu erledigen, sondern kann ein "Fatal" weitergeben und dem Nutzer sagen, dass die Anwendung gerade einen Fehler verursacht hat.
Soweit, so gut - hier jetzt der UserSerivce
Der UserService:
public class UserService { public ServiceResponse<User> GetUser(ServiceRequest<string> userName) { // simulate Usererror if (String.IsNullOrEmpty(userName.Value)) { return new ServiceResponse<User>() { Result = ServiceResult.Failed, ErrorCodes = ErrorCodes.InvalidName }; } // simulate Systemerror if (userName.Value == "Tester0") { return new ServiceResponse<User>() { Result = ServiceResult.Fatal, Exception = new ArgumentException() }; // or should I throw it??? } // everything works UserRepository rep = new UserRepository(); User result = rep.GetUsers().ToList().Where(x => x.Name == userName.Value).SingleOrDefault(); // nothing found if (result == null) { return new ServiceResponse<User>() { Result = ServiceResult.Failed, ErrorCodes = ErrorCodes.NothingFound }; } else { return new ServiceResponse<User>() { Value = result, Result = ServiceResult.Succeeded }; } } }
</strong>
Hier hab ich mal die verschiedenen Fälle implementiert.
Als erstes prüft man, ob der Nutzer überhaupt was eingegeben hat, wenn nicht, dann gibt er "Failed" mit dem passenden ErrorCode zurück.
Um ein Exception (also einen Fatal Error) zu simulieren, übergibt man Tester0. In diesem Fall reicht man die Exception weiter. Hier war ich mir selber auch unsicher - sollte man die Exception nach oben befördern?
Ich hab mich für diese Variante entschieden, da der Service bei mir mit der Kern ist. Hier kann der Fehler entsprechend behandelt werden und der, der die Methode Aufruft, bekommt die Exception mit, die es ausgelöst hat - um es z.B. zu loggen.
Danach ruf ich das Repository auf und wenn etwas gefunden wurde, dann gibt es ein "Succeeded", ansonsten ein "Failed".
ConApp
class Program { static void Main(string[] args) { Console.WriteLine("Enter Username"); Console.WriteLine("(Tester0 == Systemerror)"); Console.WriteLine("(enter nothing == Usererror)"); string username = Console.ReadLine(); UserService srv = new UserService(); ServiceResponse<User> result = srv.GetUser(new ServiceRequest<string>() { Value = username }); if (result.Result == ServiceResult.Succeeded) { Console.WriteLine("Service Call Succeeded"); Console.WriteLine(result.Value); } if (result.Result == ServiceResult.Failed) { Console.WriteLine("Service Call Failed"); Console.WriteLine(result.ErrorCodes.ToString()); } if (result.Result == ServiceResult.Fatal) { Console.WriteLine("Service Call Fatal Error"); Console.WriteLine(result.Exception.ToString()); } Console.ReadLine(); } }
Hier sieht man das ganze mal in Action. Durch den Einsatz der ErrorCodes könnte man an dieser Stelle sogar noch einen anderen Service aufrufen, der den Code in ein passenden, Userfreundlichen (in in seiner Sprache geschriebenen) Satz umwandeln könnte.
Kann man sich die ErrorCodes nicht sparen?
Natürlich könnte ich auch Exceptions dafür nehmen, allerdings steht dort in den Messages nicht wirkich Userfreundliche Informationen drin. Ich wollte auch eine Art Trennung zwischen "Systemfehlern" und "Userfehlern" dadurch erzwingen.
Die Applikation kann man natürlich runterladen. Ich würde mich freuen wenn ihr Feedback darauf gebt - sind Enums die Erfüllung? Geht es nicht "schöner"? Was haltet ihr von dem Gesamtkonstrukt? Wenn ihr selber ein Demoprojekt habt, dann schickt es an meine Emailadresse (siehe Impressum) - für Anregung bin ich immer dankbar :)