piątek, 18 września 2009

Design by contract i wstępna walidacja danych, część 3

Mając przygotowany już kod obsługujący naszą składnię, możemy przejść analizy przekazanego wyrażenia.

Korzeniem wyrażenia jest zawsze obiekt LambdaExpression, którego Body jest wywołaniem metody lub konstruktora, których argumenty będą poddane sprawdzeniu. Wywołanie metody powoduje odłożenie na drzewie obiektu MethodCallExpression, zaś sama metoda i informaje o niej (MethodInfo) jest dostępna przez property Method. Wywołanie konstruktora (np. new Service(...)) odkłada na drzewie obiekt NewExpression, umieszczając informacje o konstruktorze (ConstructorInfo) w property Constructor.


Argumenty przekazane do wywołania metody lub konstruktora dostępne są poprzez property Arguments zarówno w MethodCallExpression jak i NewExpression.

Tyle wstępu i rozróżniania przypadków - przejdźmy do najciekawszej, choć, jak się okaże, banalnej części - sprawdzania argumentów.

W tej chwili interesują nas wyłącznie argumenty przekazane do funkcji/konstruktora. Każdy argument jest typu Expression - co jest dosyć naturalne, bo w końcu możemy naszą funkcję wywołać na wiele sposobów:

public void Function(string arg1, User arg2, int arg3)
{
  this.CheckArg(x => x.Function(arg1, new User(), 5));
  this.CheckArg(x => x.Function(arg1, arg2, arg3))
  ...
};

Pierwsze wywołanie spowoduje utworzenie następujących argumentów:
  • MemberExpression
  • NexExpression 
  • ConstantExpression
Z drugiego wywołania otrzymamy z kolei trzy argumenty typu MemberExpression.

Jeśli przyjrzymy się tym obiektom, to okazuje się, że tylko ConstantExpression posiada property Value - a jak odczytać pozostałe? W zasadzie należałoby przejść po całym drzewie za każdym razem patrząc na to, jakiego typu jest wyrażenie i odpowiednie przetwarzanie:
  • NewExpression wiemy jak przetworzyć - wywołać konstruktor tak jak przy refleksji
  • MemberExpression zawiera w sobie Expression, które może być dowolnego typu z podanych powyżej (i nie tylko)
  • ConstantExpression prawie od razu daje wartość
Na szczęście .Net został wyposażony w automat, który zrobi to za nas:
Expression.Lambda(expression).Compile().DynamicInvoke();

Iterując po wszystkich argumentach bez problemu już sprawdzimy czy dany argument nie jest nullem.

Pozostaje kwestia wyświetlania błędów - jeśli po prostu rzucimy wyjątek, to na stacktrace będziemy w funkcji przetwarzającej wyrażenia lambda, a nie w funkcji, której argumenty sprawdzamy (chociaż ta też się pojawi, ale głębiej). Tego nie przeskoczymy - możemy jednak podać ładny komunikat o błędzie, w którym będzie podana nazwa funkcji - a to już wystarczy do łatwego i szybkiego zorientowania się o co chodzi, w przypadku wystąpienia błędu.

Na początku tej części wspomniałem, że informacje o metodzie znajdują się w property Method klasy MethodCallExpression, dzięki czemu otrzymujemy MethodInfo. Zwykły ToString() daje nam dosyć dobrą nazwę funkcji, więc proponuję ją zostawić.
A co w przypadku konstruktora? Spróbujmy Constructor.ToString() - przykładowy wynik:
Void .ctor(System.String, Int32, Some.User)

Hm.. nie za dobrze - skąd w runtime po otrzymaniu błędu będziemy wiedzieć którego obiektu to konstruktor? Na szczęście NewExpression (a właściwie to klasa bazowa Expression) posiada property Type (czyli typ obiektu reprezentowanego przez wyrażenie), z którego możemy odczytać FullName:
newExpression.Type.FullName + " " + newExpression.Constructor
co daje w wyniku:
Some.HelperWithConstructor Void .ctor(System.String, Int32, Some.User)

Wydaje mi się, że jest to akceptowalne.

Przy rzucaniu ArgumentNullException należałoby podać również nazwę argumentu, który był nullem - wystarczy wywołać expression.Member.Name.

W tej chwili kod nam bardzo ładnie działa - jest dokonywana pełna walidacja nieobecności nulla, wyjątek jest komunikatywny zarówno pod względem nazw argumentów jak i treści, a co najlepsze - sprawdzanie jest dokonywane z pełną kontrolą typów na poziomie kompilatora - jeśli dodamy nowy argument do funkcji bez rozszerzenia walidacji, to kompilator wyrzuci błąd. Z kolei samo rozszerzenie walidacji jest banalne - po prostu dopisujemy kolejny argument do wywołania.

Dociekliwy czytelnik zauważy, że na początku rozmawialiśmy o zastąpieniu np. takiej konstrukcji:
if (string.IsNullOrEmpty(actionName))
{
  throw new ArgumentNullException("actionName");
}
a kod, który napisaliśmy sprawdzi wyłącznie obecność null - w przypadku stringów przepuści wartość string.Empty (lub po prostu ""). Co więc należy zrobić? Oczywiście dopisać warunek specyficzny dla stringów - wiemy jak pobrać typ argumentu (jak i funkcji :)), więc poprzez zwykłe sprawdzenie typów, możemy dodatkowo sprawdzać:
if (argument.Type == typeof(String))
{
  if (string.IsNullOrEmpty(value as string))
  {
    throw ...
  }
}

Brak komentarzy:

Prześlij komentarz