środa, 16 września 2009

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

W części pierwszej poznaliśmy założenia zadania. Tym razem zaczniemy budować implementację. W tym miejscu wspomnę, że w przeważającej większości zabaw z wyrażeniami lambda oraz składnią imitującą język naturalny warto najpierw napisać przykładowe użycie kodu (jak to zrobiliśmy wcześniej), a dopiero później próbować zrealizować pomysł.

Korzystamy z funkcji rozszerzających, więc pierwszą czynnością jest przygotowanie statycznej klasy, w której zamieścimy implementację - nie ma tu nic odkrywczego:

public static class ArgumentValidator
{
...
}

Nazwa klasy statycznej nie ma tutaj żadnego znaczenia z punktu widzenia korzystania z kodu - jedynie pomaga się zorientować w strukturze projektu.

Następnie deklarujemy funkcję CheckArg - również musi być statyczna, a jej pierwszym argumentem jest obiekt, na którym będą wykonywane operacje (czyli obiekt, który rozszerzymy o funkcję CheckArg). W naszym przypadku chcemy rozszerzyć każdy obiekt, więc korzystamy z funkcji z typami generycznymi:


public static void CheckArg<T>(this T me, Action<T> action)
{
...
}

Action<T> oznacza, że oczekujemy tam wyrażenia lambda, któremu jako argument będzie przekazany obiekt typu T (w tym wypadku dowolny).

W tej chwili możemy już wywołać nasze funkcje:

this.CheckArg(x => x.Sth());
string.CheckArg(x => x.IsNullOrEmpty("tekst"));

Wpisujemy powyższe w IDE i co widzimy? Błąd kompilacji :). Dzieje się tak dlatego, że funkcja string.IsNullOrEmpty zwraca jakieś dane - w wyrażeniach lambda takie funkcje są reprezentowane przez Func<T,TR> (gdzie TR to typ zwracany), a nie Action<T>. Dodajmy zatem analogiczną funkcję do ArgumentValidator:

public static void CheckArg<T,TR>(this T me, Func<T,TR> action)
{
...
}

Od strony składniowej mamy już spełnione wszystkie warunki. Działa jak chcieliśmy.

Przejdźmy zatem do analizy przekazanego wyrażenia. Analizujemy zmienną action i co widzimy? A no nic. Jedyne co można zrobić to wykonać action(me) oraz sprawdzić co za funkcję otrzymaliśmy - nie ma natomiast możliwości sprawdzenia przekazanych w argumentach wartości. Trochę googlania i już wiemy, że dostęp do drzewa wywołania zapewnia klasa Expression. Zmieniamy więc nasze funkcje na następujące:

public static void CheckArg<T>(this T me, Expression<Action<T>> expression)
{
...
}
public static void CheckArg<T,TR>(this T me, Expression<Func<T,TR>> expression)
{
...
}

Teraz jeśli zajrzymy do expression to zobaczymy Body, a to jest to co nas interesuje. Jest to pełne drzewo wywołania przekazane jako lambda expression, zawierającego w sobie podwyrażenia, które zostały użyte jako argumenty. Powyższa deklaracja funkcji jest potrzebna tylko w momencie pisania (IntelliSense pięknie pomaga wykorzystać powyższy kod) oraz kompilacji - tylko tyle już wystarczy do zapewnienia pełnej kontroli typów. Analiza wyrażenia będzie następowała w runtime, więc nie potrzebujemy już takiej typowalności - spokojnie możemy zrzutować expression na LambdaExpression w obu funkcjach, co sprawi, że ciała obu funkcji będą praktycznie identyczne, więc zgodnie ze sztuką wydzielamy oddzielną funkcję przyjmującą jako argument wyłącznie LambdaExpression, dzięki czemu uzyskujemy następujący kod:

public static void CheckArg<T>(this T me, Expression<Action<T>> expression)
{
  CheckArgument(expression);
}
public static void CheckArg<T,TR>(this T me, Expression<Func<T,TR>> expression)
{
  CheckArgument(expression);
}
public static void CheckArgument(LambdaExpression expression)
{
...
}

W tym momencie polecam zabawę z debuggerem oraz Reflectorem, aby zapoznać się z tym, jak zbudowane jest to drzewo i jak się po nim poruszać. Namespace System.Linq.Expressions zawiera m.in. wszystkie klasy, których możemy się spodziewać w Body oraz w poszczególnych gałęziach drzewa.

Brak komentarzy:

Prześlij komentarz