środa, 30 listopada 2011

O pewnym anty-wzorcu projektowym

    W swojej pracy zawodowej niejednokrotnie napotykałem kod stosujący „wzorzec” projektowy Singleton. Prace związane z utrzymaniem takiego kody napsuły mi dużo krwi i wystawiły na próbę moją cierpliwość. Moi pracodawcy również zapłacili słono za Singletony, bo prace nad źle napisanym kodem idą mi i moim kolegom bardzo, bardzo powoli. Powyższe doświadczenia zmotywowały mnie do napisania tego tekstu.
    Uzasadnieniem użycia Singletona jest potrzeba zapewnienia, że dana klasa ma co najwyżej jedną instancję dla całej aplikacji. Skuteczne zaspokojenie tej potrzeby jest podstawową zaletą tego „wzorca”. Niektórzy za jego zaletę uważają też to, że instancji Singletona można użyć w kodzie dowolnej innej klasy bez deklarowania pola o typie klasy-Singletona. Według mnie, jest to niewątpliwa wada... Zauważmy, że dokładnie taką cechę w programowaniu proceduralnym miały zmienne globalne, czyli Singletony można nazwać ich obiektowym odpowiednikiem (nie muszę chyba nikogo przekonywać, że należy szerokim łukiem omijać zmienne globalne?).
    Wychodzi więc na to, że Singletony mają dokładnie jedną pozytywną cechę. Przejdźmy teraz do wad. Jedną już wymieniłem: deklarujemy zmienną globalną. Dodam do tego, że „globalność” jest przechodnia tj. każda składowa klasy-Singletona staje się globalna i tak dalej (rekurencyjnie).
Drugą wadą jest całkowite uniemożliwienie testowania jednostkowego metod używających Singletona (Singletona nie można w środowisku testowym podmienić mock-iem)
Trzecią bardzo poważną wadą jest to, że każda klasa używająca Singletona kłamie w sprawie swoich zależności. Ma np. konstruktor domyślny (co sugeruje, że jest niezależna), podczas gdy w rzeczywistości jest zależna od klasy-Singletona.
Jeszcze inne wady:
  • naruszenie zasady SoC (klasa-Singleton jest odpowiedzialna za utworzenie swojej własnej instancji, czyli ma dodatkową odpowiedzialność swojej własnej fabryki)
  • naruszenie prawa Demeter (gdy piszemy: Singleton.GetInstance().SomeMethod())
Żeby wymieniona na początku zaleta nie przesłoniła komuś następującej po niej litanii wad, dodam, że praktycznie do każdego współczesnego języka obiektowego mamy do wyboru kilka framework-ów do wstrzykiwania zależności do klas. Jeśli używamy C#-a i Ninject-a, możemy napisać:

Bind<IService>().To<ConcreteService>().InSingletonScope();

... i już mamy rozwiązanie, gwarantujące, że IService (tak, interfejs) będzie utworzony tylko raz dla całej aplikacji. Tam gdzie go potrzeba dodamy pole typu IService. Co więcej nie wiążemy klas z konkretną implementacją (ConcreteService), tylko z interfejsem (trzymamy się zasady otwarty/zamknięty i umożliwiamy mock-owanie w środowisku testowym).

3 komentarze:

  1. Staram się zwalczać Singletony jak mogę, ale mimo to uważam, że jest sposób na UT w kodzie używającym Singletona. Oczywiście o ile używa go w sposób dobry :)
    Wystarczy wyciągnąć z singletonu interfejs, a następnie wszystkie Singleton::Instance() podmienić na jakąś zewnętrzną klasę. Np. w taki sposób, używać jakiegoś getInstance będącego wskaźnikiem do funkcji.

    W kodzie produkcyjnym wskaźnik ten (gdzieś) przypisujemy na Singlegon::Instance a w UT na instancję mocka (implementującego ten sam interfejs co nasz singleton).

    Co Ty na to?

    Żeby nie wyjść na "tego złego" - oczywiście tak jak napisałem na początku walczę z singletonami ;] Tak czy owak jest to jakiś sposób na UT.

    OdpowiedzUsuń
  2. Teoretycznie, testowanie klas używających Singletona jest możliwe pod warunkiem trzymania się określonej konwencji. Ty piszesz o C++, c C# można też osiągnąć testowalność, np. tak:

    class MyClass
    {
    private IService service;

    public MyClass(IService service)
    {
    this.service = service;
    }

    public MyClass()
    {
    service = Service.Instance;
    }
    }

    Tylko, że w takich rozwiązaniach Service.Instance jest MOŻLIWE do użycia wszędzie, a testowalność miałaby narzucać konwencja, a nie API klasy. A konwencje w dużych aplikacjach mają to do siebie, że są nagminnie łamane...

    OdpowiedzUsuń
  3. do Marcina:
    Do poprzedniego komentarza dorzucę jeszcze bibliografię :-)

    Misko Hevery ma w tej sprawie jednoznaczną opinię:
    http://misko.hevery.com/code-reviewers-guide/flaw-digging-into-collaborators/

    Podczas gdy Martin Fowler pisze, że na dwoje babka wróżyła:
    http://martinfowler.com/articles/injection.html

    OdpowiedzUsuń