ś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).