Na szybko nie przychodziło mi jednak nic do głowy... Wtedy padło pytanie, które zmotywowało mnie do przygotowania tego wpisu: "A czytał Pan Effective Java, Joshua Bloch?". A no nie czytałem i nie sądzę, abym ją przeczytał.
Dlaczego?
Przede wszystkim uważam że ta książka jest trochę przestarzałą. Drugie wydanie pochodzi z 2008 roku i informacje w niej zawarte są trochę nieaktualne. To nie książka o wzorcach projektowych Bandy Czworga, która nie traci na swojej aktualności, a jedynie pozycja o tym jak coś tam dobrze zrobić w Javie. I tak przykładowo rozdział: "Item 51 Beware the performance of string concatenation", traktujące o tym, aby lepiej używać StringBuilder’a niż + do sklejania tekstów, jest już dawno nieaktualne! Chciałem kiedyś pisać posta o tym, ale na stackoverflow i jeszcze gdzieś tam już dawno o tym było. Nie wiem tylko od kiedy dokładnie istnieje ten mechanizm zamiany + na StringBuilder’a w Javie.
O innych dobrych praktykach, można się dowiedzieć zawsze z innych źródeł, niekoniecznie czytając wspomnianą książkę.
Dobra wróćmy do tematu wpisu. W innym rozdziale Effective Java (Item 3: Enforce the singleton property with a private constructor or an enum type), jest zalecenie, aby Singletona implementować za pomocą Enuma. Z tym rozwiązaniem spotkałem się po raz pierwszy podczas review kodu kogoś, kto czytał tę książkę. Użycie wówczas enuma w roli singletonu było dla mnie zupełnie niezrozumiałe! Musiałem dopytać autora o co chodzi.
Dlaczego nie lubię tego rozwiązania? Spójrzmy na przykładowy kawałek kodu (inspirowany Joshuą Blochem, co bym za dużo nie musiał wymyślać, jak tu mądrze użyć singletona). Kod jest i tak kiepski (obliczanie czasu), ale chodzi mi o zaprezentowanie działania omawianego wzorca.
public enum Elvis { INSTANCE; private final int ELVIS_BIRTHDAY_YEAR = 1935; public int howOldIsElvisNow() { return new GregorianCalendar().get(Calendar.YEAR) - ELVIS_BIRTHDAY_YEAR; } }
public class ElvisProfitService { public double ELVIS_SALARY_YEAR = 70_000; public double calculateElvisProfit() { return Elvis.INSTANCE.howOldIsElvisNow() * ELVIS_SALARY_YEAR; } }
No i weź tu panie przetestuj taki kod! Możemy jeszcze Elvisa statycznie zaimportować, to linijka 6 skróci się do jeszcze mniej czytelnej formy.
return INSTANCE.howOldIsElvisNow() * ELVIS_SALARY_YEAR;
Ktoś ma jakieś pomysły jak taki kod przetestować? Da się oczywiście, z wykorzystaniem PowerMock’a [link do rozwiązania na końcu ^1], ale chyba nie o to chodzi aby pisać nietestowany kod?
Dlaczego wolę starszą wersję tegoż rozwiązania:
public class Elvis { private static final Elvis INSTANCE = new Elvis(); private final int ELVIS_BIRTHDAY_YEAR = 1935; private Elvis() { } public static Elvis getInstance() { return INSTANCE; } public int howOldIsElvisNow() { return new GregorianCalendar().get(Calendar.YEAR) - ELVIS_BIRTHDAY_YEAR; } }
Dochodzi prywatny konstruktor, statyczna metoda getInstance() i inicjalizacja pola klasy wraz z deklaracją.
W tym przypadku kod korzystający z tego singletonu, mógłby być następujący:
public class ElvisProfitService { private final double ELVIS_SALARY_YEAR = 70_000; private Elvis elvis = Elvis.getInstance(); public double calculateElvisProfit() { return elvis.howOldIsElvisNow() * ELVIS_SALARY_YEAR; } // For tests void setElvis(Elvis elvis) { this.elvis = elvis; } }
W linii 4 wywołałem getInstance(), aby w przykładowym kodzie produkcyjnym było wszystko cacy. Dzięki temu, zależność ta jest definiowana jako pole w kasie i mamy setter do tego, więc możemy sobie bardzo ładnie przetestować tą funkcjonalność, bez hackowania z PowerMockiem:
public class ElvisProfitServiceTest { @Test public void shouldCalculateElvisProfit() { // given ElvisProfitService service = new ElvisProfitService(); Elvis elvis = mock(Elvis.class); when(elvis.howOldIsElvisNow()).thenReturn(1); service.setElvis(elvis); // when double elvisProfit = service.calculateElvisProfit(); // then assertEquals(70_000, elvisProfit, 0.1); } }
W sekcji given mamy bardzo ładnie zdefiniowane zachowanie, za pomocą Mockito, jakie ma przyjmować masz singleton na potrzeby tego testu.
A Ty jak definiujesz (o ile to robisz) swoje singletony? Które rozwiązanie uważasz za lepsze?
[1] Co do rozwiązania zagadki, jak przetestować Singletona jako Enum’a, to tutaj jest odpowiednia rewizja na github’ie: SingletonInJava e714fb a kod poniżej:
@RunWith(PowerMockRunner.class) @MockPolicy(ElvisMockPolicy.class) public class ElvisProfitServiceTest { @Test public void shouldCalculateElvisProfit() { // given ElvisProfitService service = new ElvisProfitService(); // when double elvisProfit = service.calculateElvisProfit(); // then assertEquals(70_000, elvisProfit, 0.1); } }
public class ElvisMockPolicy implements PowerMockPolicy { @Override public void applyClassLoadingPolicy(MockPolicyClassLoadingSettings settings) { settings.addFullyQualifiedNamesOfClassesToLoadByMockClassloader("com.blogspot.mstachniuk.singletoninjava.Elvis"); } @Override public void applyInterceptionPolicy(MockPolicyInterceptionSettings settings) { Method method = Whitebox.getMethod(Elvis.class, "howOldIsElvisNow"); settings.proxyMethod(method, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return 1.0; } }); } }
Pomysł jeden:
OdpowiedzUsuńEnum może implementować jakiś interfejs, także można go wstrzykiwać w inne obiekty po abstrakcji. I tu już łatwiej o testowanie.
Nie sprawdzałem nigdy jak Spring wspiera koncept "Enumy jako Bean-y"....
Tak, interface zawsze pomaga, aby rozluźnić zależności pomiędzy klasami. Często jednak o tym zapominamy i wstawiamy singletona na sztywno w kodzie.
UsuńJuż nie zapomnimy, po Waszych komentarzach! Dziękować.
UsuńDzięki również za artykuł. Dobrze się go czyta i poleciłbym go do "rozluźnienia" myślenia od Enuma jako jedynego słusznego singletona.
Kilka drobnych poprawek ku "lepszejszości":
* s/Effektive/Effective
* s/tą książkę/tę książkę
Jacek
Thx Jacku za poprawki, już wdrożone.
UsuńTen komentarz został usunięty przez autora.
OdpowiedzUsuńPiszesz, ktoś to potem przeczyta i co, bzdury.
OdpowiedzUsuńPrzeczytaj Effective Java i Java Puzzlers, jak już napiszesz erratę do tego posta uwzględniającą wiedzę stamtąd, to Ci kupię butelkę wybranego przeze mnie czegoś dobrego.
(sorry za podwójny, kawałek się wyciął)
O jakie konkretnie bzdury masz na myśli? Nie twierdzę, że w tej książce jest samo zło, a jedynie wytykam jej nieaktualność.
Usuńa co to wracasz do PL, do WRO
OdpowiedzUsuńWróciłem :P
UsuńOzesz w morde! Nawet jak mnie nie zapytaja to sam na rozmowie tak zakrece zeby o singlu opowiedziec i sie wykazac. Robote mam na bank ;)
OdpowiedzUsuń