Na gwałtownie 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 iż 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ć dzięki 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, dzięki 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;
}
});
}
}