Service Locator jako anti-pattern

W wielu publikacjach service locator podawany jest jako wzorzec projektowy, doskonale nadający się do implementacji inversion of control. W poście jednak chciałbym przedstawić drugą szkołę, która uważa, że ten wzorzec jest “brzydki” i powoduje ogromne zamieszanie.

Przede wszystkim odpowiedzmy sobie kiedy używamy podejścia IoC? W aplikacjach tymczasowych? Prototypach? Raczej nie… Początkowy czas na napisanie aplikacji IoC może okazać się dłuższy a korzyści nadchodzą dopiero po kilku miesiącach implementacji. IoC jest zatem doskonały albo nawet niezbędny dla aplikacji rozwijanych już trochę dłużej a szczególności dla projektów wykorzystujących testy jednostkowe.  Innym przykładem są różnego rodzaju biblioteki przeznaczone dla programistów. Wtedy gruntowne przetestowanie różnych przypadków użycia jest niezbędne.

Wady service locator’a widać bardzo w przypadku implementacji właśnie bibliotek zewnętrznych.  Zobaczmy przykładową implementację SL:

public static class SampleServiceLocator
{
    private readonly static IDictionary<Type, Func<object>> _services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        _services[typeof(T)] = () => resolver();
    } 
    public static T Resolve<T>()
    {
        return (T)_services[typeof(T)]();
    }
}

Następnie napiszmy jakiś kod oparty o Service Lcoator:

class DocumentManager
{
    public void Print(IDocument document)
    {
        IPritnManager manager = SampleServiceLocator.Resolve<IPrintManager>();
        manager.Print(document);
    }
}

Aby wykorzystać kod należy PAMIĘTAĆ o wcześniejszej rejestracji IPrintManager – to jest największa wada. Wyobraźmy sobie, że opublikowaliśmy kod i użytkownik próbuje wywołać metodę Print:

var documentManager = new DocumentManager();
documentManager.Print();

Powyższy kod wyrzuci wyjątek ponieważ nie zarejestrowano danego obiektu. Sygnatura nie wymusiła prawidłowego wykorzystania metody. Bez przeczytania dokumentacji nie ma możliwości prawidłowego zarejestrowania obiektów – użytkownik nie ma o tym zielonego pojęcia. Zobaczmy jakby wyglądał DocumentManager z wykorzystaniem wstrzyknięcia implementacji w konstruktorze:

class DocumentManager
{
    private IPrintManager _printManager;
    
    public DocumentManager(IPrintManager printManager)
    {
        _printManager = printManager;
    }
    public void Print(IDocument document)
    {
        _printManager.Print(document);
    }
}

W tej chwili nie ma możliwości złego wywołania kodu. Użytkownik tworząc DocumentManager wie, że musi przekazać implementację PrintManager.

Inna sprawa to testy jednostkowe. W takiej architekturze łatwo zobaczyć jakie obiekty należy przekazać. Wiadomo, że DocumentManager zależy od PrintManager i pierwszym etapem powinna być implementacja stub’a dla PrintManager.

Kolejną wadą jest modyfikacja istniejących metod. Przypuśćmy, że pewnego dnia Print został zmodyfikowany i teraz wykorzystuje kolejną zależność. Wszystkie aplikacje klienckie od tego momentu zostały złamane a co gorsze w czasie run-time a nie compile-time. Wszystko podczas kompilacji będzie działać a potem okaże się, że brakuje tej nowej zależności. Naszym celem jest sprawdzenie jak największej liczby błędów na etapie kompilacji a nie podczas działania już aplikacji. W przypadku wstrzyknięcia zależności za pomocą konstruktora byłoby to możliwe.

Wiem, że czasami SL jest jedynym rozwiązaniem ale warto zastanowić się na architekturą oprogramowania, które piszemy i minimalizować użycie tego wzorca\antywzorca.

Wyrażenia lambda i niespodziewany rezultat

Co poniższy kod zwróci na ekranie?

var lambdas = new List<Func<int>>();
for (int index = 0; index < 5;index++ )
{
    lambdas.Add(() =>index);
}            
Console.WriteLine(lambdas[0]());
Console.WriteLine(lambdas[1]());
Console.WriteLine(lambdas[2]());

Spodziewać się można 0,1,2. Jednak na ekranie ujrzymy 5,5,5. Dlaczego?  Aby odpowiedzieć na te pytanie zajrzymy do Reflector’a:

private static void Main(string[] args)
{
    List<Func<int>> lambdas;
    Func<int> CS$<>9__CachedAnonymousMethodDelegate1;
    <>c__DisplayClass2 CS$<>8__locals3;
    lambdas = new List<Func<int>>();
    CS$<>9__CachedAnonymousMethodDelegate1 = null;
    CS$<>8__locals3 = new <>c__DisplayClass2();
    CS$<>8__locals3.index = 0;
    goto Label_003C;
Label_0017:
    if (CS$<>9__CachedAnonymousMethodDelegate1 != null)
    {
        goto Label_0028;
    }
    CS$<>9__CachedAnonymousMethodDelegate1 = new Func<int>(CS$<>8__locals3.<Main>b__0);
Label_0028:
    lambdas.Add(CS$<>9__CachedAnonymousMethodDelegate1);
    CS$<>8__locals3.index += 1;
Label_003C:
    if (CS$<>8__locals3.index < 5)
    {
        goto Label_0017;
    }
    Console.WriteLine(lambdas[0]());
    Console.WriteLine(lambdas[1]());
    Console.WriteLine(lambdas[2]());
    return;
} 
));

Jak widać, tworzona jest w miejsce lambdy delegata. Aby przekazać parametry do niej, została stworzona klasa <>c__DisplayClass2() , która jest po prostu wrapperem dla parametru index. Innymi słowy, została wygenerowana delegata, która jako parametr wejściowy bierze klasę DisplayClass2, która z kolei zawiera parametr index. Wszystko byłoby w porządku gdyby instancja DisplayClass2 była tworzona wewnątrz pętli. Z kodu jasno jednak wynika, że instancja jest tworzona przed Label_0017 (jest to początek pętli). Używamy zatem tej samej instancji, a w każdej iteracji wywołujemy CS$<>8__locals3.index += 1 (locals3 to instancja obiektu DisplayClass2), co zwiększa indeks. W ostatniej iteracji osiągnie się zatem wartość 5.

Przykład pokazuje, że wyrażenia lambda są łatwe w użyciu ale jeśli nie zna się w pełni zasady działania, mogą stworzyć niespodziewane efekty. Rozwiązaniem może być przekopiowanie indeksu do pomocniczej zmiennej. C#:

var lambdas = new List<Func<int>>();
for (int index = 0; index < 5;index++ )
{
 int tmp = index;
 lambdas.Add(() => tmp);
}
Console.WriteLine(lambdas[0]());
Console.WriteLine(lambdas[1]());
Console.WriteLine(lambdas[2]());
}

Reflector:

List<Func<int>> lambdas;
int index;
<>c__DisplayClass1 CS$<>8__locals2;
lambdas = new List<Func<int>>();
index = 0;
goto Label_002D;
Label_000A:
CS$<>8__locals2 = new <>c__DisplayClass1();
CS$<>8__locals2.tmp = index;
lambdas.Add(new Func<int>(CS$<>8__locals2.<Main>b__0));
index += 1;
Label_002D:
if (index < 5)
{
   goto Label_000A;
}
Console.WriteLine(lambdas[0]());
Console.WriteLine(lambdas[1]());
Console.WriteLine(lambdas[2]());
return;

Na zakończenie dodam, że w c# 5.0 usprawniono trochę wyrażenia lambda ale o tym kiedyś indziej…

Visual Studio 11 – pierwsze spojrzenie

Zamierzam napisać kilka krótkich postów o nowych funkcjonalnościach w Visual Studio 11. Na dobry początek zacznijmy po prostu od screenu:

image

Co od razu rzuca się w oczy? Czarno białe ikony. Dla porównania zobaczymy jak wygląda “stary” Visual Studio:

image

Na pierwszy rzut oka to poprzednia wersja wygląda dużo nowocześniej ze względu na kolorowe, bardziej 3d ikonki. Dlaczego Microsoft zdecydował się na taki krok? Z tego co wiem, doszli do wniosku, że środowisko programistyczne nie powinno być zabawką i nie może rozpraszać od pracy nad kodem. W pełni się z tym zgadzam. Wolę szybkie IDE bez zbędnych wodotrysków. Tak samo mam z Windows – zawsze ustawiam tryb klasyczny. Dlatego lubię nowe ikonki chodź osobiście wolałbym dużo bardziej  ikony  z Visual C++ 6.0 niż te co dodali teraz…