Po dość długiej przerwie urlopowej, o której pewnie (choćby sam dla siebie w przyszłości) coś skrobnę, ponownie istnieję dla świata. Do mocków wrócę wkrótce, póki co – coś z zupełnie innej beczki.
Znajomość Enterprise Library, bądź szpanersko EntLiba, była kiedyś wyznacznikiem “döjrzałego programisty .NET”. Wydaje mi się że teraz już tak nie jest, a to szczególnie ze względu na coraz bogatszy ekosystem alternatywnych rozwiązań dla .NET üzüpełniających mechanizmy dostępne ÖÖTB. EntLib staje się… paße? Ale, umówmy się, jeśli do wykorzystania najprostszych nawet czynności oferowanych przez bibliotekę zmuszeni jesteśmy napisać tonę konfigüracji w XML, do czego stworzone zostało nawet specjalne narzędzie, to coś jest zdecydowanie nie hälö…
Konfiguracja to jedno: w końcu piszemy to raz, modyfikujemy rzadko, więc nie ma jeszcze aż takiej tragedii. Jeżeli jednak za przykład weźmiemy nawet najprostszy blok wchodzący w skład EntLiba, czyli Exception Handling Application Block (EHAB), i zobaczymy co wyprawia on z naszym kodem, to mina może zrzednąć:
1: try 2: { 3: // Run code. 4: } 5: catch(DataAccessException ex) 6: { 7: bool rethrow = ExceptionPolicy.HandleException(ex, "Data Access Policy"); 8: if (rethrow) 9: { 10: throw; 11: } 12: }
A jest to przykład z MSDN, czyli należy go chyba traktować jak best practice! Wyobrażacie sobie jak wyglądałby kod po zastosowaniu wszędzie takiej konstrukcji? Na pewno nawet najbardziej wyposzczony programista by na takie coś nie poleciał.
Na jego poprawienie jest kilka sposobów, na przykład:
- zastosowanie Code Snippets
- wyciągnięcie nazw polityk (tu: “Data Access Policy”) do osobnej klasy
- wykorzystanie Aspect-Oriented Programming
Każda z nich jednak ma swoje wady. Snippety są nie tyle lekiem na chorobę, co morfiną dla cierpiącego. Wyeliminowanie stringów z każdego odwołania z pewnością pomoże, jednak to tylko połowa sukcesu: i tak za każdym razem musimy się do jakiejś zmiennej odwołać, co wśród tak (wbrew pozorom) skomplikowanej i powtarzalnej konstrukcji może prowadzić do błędów i pomyłek. No i oczywiście trzeba pamiętać że taka zbiorcza klasa istnieje, bo sygnatura metody HandleException mówi nam tylko tyle, że potrzebny jest string. Stosowanie AOP tylko w celu walki z EntLib także nie wydaje się super-pomysłem (zresztą stosowanie AOP w jakimkolwiek scenariuszu wcale och-i-ach być nie musi).
Cóż więc proponuję? Może najpierw – co chciałbym uzyskać? Otóż zamiast powyższego kodu chciałbym mieć coś takiego:
1: HandleExceptions.DataAccessPolicy(() => _usersRepository.SaveUser(user));
I takiego:
1: double result = HandleExceptions.BusinessLogicPolicy(() => Calculator.Divide(2, 0));
Na co warto zwrócić uwagę: te dwa scenariusze różnią się od siebie dość znacząco. Pierwszy z nich wykonuje tylko jakąś operację, podczas gdy drugi dodatkowo zwraca wynik owej operacji! Jak szaleć to szaleć, dlaczego nie zapragnąć takiej możliwości?:
1: try 2: { 3: // Run code. 4: } 5: catch(Exception exc) 6: { 7: HandleExceptions.DataAccessPolicy(exc); 8: }
Też przydatne.
Jak można się łatwo domyślić, teraz nastąpi wklejenie klasy rozwiązującej te problemy:
1: public static class HandleExceptions 2: { 3: public static void BusinessLogicPolicy(Action operation) 4: { 5: Handle(operation, Policies.BusinessLogic); 6: } 7: 8: public static T BusinessLogicPolicy<T>(Func<T> operation) 9: { 10: return Handle(operation, Policies.BusinessLogic); 11: } 12: 13: public static void BusinessLogicPolicy(Exception exception) 14: { 15: Handle(exception, Policies.BusinessLogic); 16: } 17: 18: public static void DataAccessPolicy(Action operation) 19: { 20: Handle(operation, Policies.DataAccess); 21: } 22: 23: public static T DataAccessPolicy<T>(Func<T> operation) 24: { 25: return Handle(operation, Policies.DataAccess); 26: } 27: 28: public static void DataAccessPolicy(Exception exception) 29: { 30: Handle(exception, Policies.DataAccess); 31: } 32: 33: /// <summary> 34: /// Performs a given function handling exceptions that might occur. 35: /// </summary> 36: /// <returns>Result of the operation or a default value for a given type.</returns> 37: private static T Handle<T>(Func<T> operation, string policyName) 38: { 39: T ret = default(T); 40: 41: Handle(() => 42: { 43: ret = operation(); 44: }, policyName); 45: 46: return ret; 47: } 48: 49: /// <summary> 50: /// Performs a given procedure handling exceptions that might occur. 51: /// </summary> 52: private static void Handle(Action operation, string policyName) 53: { 54: try 55: { 56: operation(); 57: } 58: catch (Exception exc) 59: { 60: bool rethrow = ExceptionPolicy.HandleException(exc, policyName); 61: if (rethrow) 62: throw; 63: } 64: } 65: 66: /// <summary> 67: /// Handles a given exception. 68: /// </summary> 69: private static void Handle(Exception exception, string policyName) 70: { 71: bool rethrow = ExceptionPolicy.HandleException(exception, policyName); 72: if (rethrow) 73: throw exception; 74: } 75: 76: private static class Policies 77: { 78: public const string BusinessLogic = "BLL Policy"; 79: public const string DataAccess = "Data Access Policy"; 80: } 81: }
Szczególnej uwadze polecam sposób wykorzystania metody Handle(Action…) przez Handle(Func<T>…).
Takie opakowanie EHABa zdecydowanie ułatwiło mi to pracę z EntLibem i jego wyuzdanym uwielbieniem dla nadmuchiwania licznika linii kodu.
Sugestie/opinie?
rozwiazanie sweet, sam z tegiego (bardzo podobnego) korzystam.
Ja jeszcze dodaje sobie do logowania cos takiego:
/// <summary>
/// Map log category into Ent Lib Log Category.
/// </summary>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
/// <param name="logType">Log Category.</param>
/// <returns>Name of Ent Lib Log Category.</returns>
public static string GetKeyValue<TAttribute>(LogCategrory logType)
where TAttribute : Attribute
{
Type type = typeof(LogCategrory);
MemberInfo[] memInfo = type.GetMember(logType.ToString());
if(memInfo != null && memInfo.Length > 0)
{
object[] attrs = memInfo[0].GetCustomAttributes(typeof(TAttribute), false);
if(attrs != null && attrs.Length > 0)
{
return (attrs[0]).ToString();
}
}
return logType.ToString();
}
a nastepnie tworze sobie atrybuty i w enum mam na przyklad tak:
/// <summary>
/// Represents logging categories that can be used in code.
/// </summary>
public enum LogCategrory
{
/// <summary>
/// Category for xxx.Api project.
/// </summary>
[LogCatKey("xxx.Api")]
xxxApi,
/// <summary>
/// Category for xxx.Gui project.
/// </summary>
[LogCatKey("xxx.Gui")]
xxxGui
}
nie opieram sie na const string gdyz potem moge sobie wykorzystywac enum tam gdzie jest mi on potrzebny.
Gutek
Ja zaczynałem od podobnego kodu, ale ostatecznie skończyłem na PIAB-ie, ponieważ ilość metod moich fasad rosła, a czytelność malała. Z drugiej strony uważam, że przechwytywanie wyjątków to aspekt (tak, jak np. wyświetlanie użytkownikowi przyjaznej wersji "404"-ki). Zwykły programista nie powinien nigdy troszczyć się o "catch" lub "handle" w kontekście wyjątków. Wszystko powinno jakoś "samo" działać.
W kolejnym projekcie wykorzystałbym wzorzec command (który jakoś ostatnio opisywałem) do tego, żeby zrealizować "poor man’s AOP" oraz przechwytywanie wyjątków.
I jeszcze jedno (z ciekawości): gdzie wykorzystujesz DataAccessPolicy?
@simon:
W moim przypadku "zwykly programista" i "architekt" to jedna i ta sama osoba – ja:). Do AOP, po poczatkowym zachlysnieciu sie tą ideą, podchodze dosc sceptycznie i szczerze mowiac nigdy w prawdziwym projekcie z tego nie skorzystalem – i raczej nie skorzystam. Moim zdaniem im wiecej rzeczy dziala "samo", tym wiecej jest miejsc ktorych "zwykly programista" prawdopodobnie nie rozumie (bo nie musi, bo to nie jego sprawa, bo nikt mu nie wytlumaczy o co chodzi, bo mu sie nie chce w to zaglebiac, bo nie ma na to czasu…), co sprowadza go do roli glupiego wciskacza klawiszy tak, by sie wszystko kompilowalo.
Generalnie ten post mial na celu pokazanie innej drogi prowadzacej do tego samego celu, bo zbyt wiele razy w roznych projektach widzialem kod podobny do tego z MSDN.
DataAccessPolicy wykorzystuje w klasie wykonujacej zapytania/komendy przez LLBLGenPro.