Implementacja Singleton’a w Javie

mstachniuk.blogspot.com 1 dekada temu
Ostatnio, podczas z jednej z rozmów rekrutacyjnych, dostałem pytanie, "A jak by Pan zaimplementował singleton w Javie?". Pytanie dość standardowe, mógł by je zadać każdy. Odpowiadam więc: „Klasa, z prywatnym konstruktorem, z polem statycznym o typie zadeklarowanej klasy, metoda getInstnce(), itd.”. Rekruter na to: "No dobra, a jakiś inny pomysł?".

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;
}
});
}
}
Idź do oryginalnego materiału