Switch, czyli bardziej czytelna sterta ifów, która jest najczęściej łączona w javie z enumami. Switch sam w sobie nie jest niczym złym, tak samo jak if. Niestety bardzo często wpadamy w pułapkę, ponieważ odpowiedzialność switcha jest zbyt duża. Powstaje wtedy "switch'ologia" czyli kolejny sposób na sprawienie żeby nasz kod był coraz bardziej paskudny. Standardowe użycie switcha może wyglądać mniej więcej tak:
public enum DiscountType {
NORMLAL, VIP, EMPLOYEE;
}
public int calculateDiscount(DiscountType discount) {
switch (discount) {
case NORMLAL:
return 10;
case VIP:
return 30;
case EMPLOYEE:
return 40;
default:
throw new IllegalArgumentException();
}
}
Na pierwszy rzut oka wszystko jest ok. Pojawia się tylko jedno małe pytanie: "Ile rzeczy w kodzie zależy od tego enuma? Ile takich switchów z tym enumem jest w kodzie?". Jeśli jest to prosta aplikacja, gdzie jest jedno miejsce to wszystko wydaje się ok, ale jeśli mamy taki kodzik w kilku miejscach to pojawia się problem z utrzymaniem takiego kodu ponieważ:
- co zrobić jeśli do enuma dojdzie nowa wartość? Musimy wyszukać wszystkie switche i obsłużyć nową wartość, co jak się domyślacie na pewno spowoduje problemy. Może nie za pierwszym razem, może nie za drugim, ale kiedyś ktoś może czegoś nie dopilnować.
- czemu dodanie czegoś do enuma musi skutkować zmianą kodu biznesowego aplikacji? To też może powodować problemy i jest kolejnym miejscem gdzie można popełnić błąd. Dodając nową wartość, musimy zobaczyć gdzie enum jest używany, co od niego zależy i teraz odpowiedzieć na pytanie - jak ten kod powinien się zachować przy nowych wartościach?
- jak czytelny będzie nasz kod jeśli switch będzie użyty z dużym enumem (kilkanaście/dziesiąt elementów), a logika jaka będzie od niego zależeć będzie trochę bardziej skomplikowana? Pewnie wielu z was będzie uciekać wtedy w prywatne metody, ale to nie jest w 100% dobre wyjście w tym przypadku.
- no i testy - ile testów potrzeba by taki kawałek kodu przetestować?
Pytanie więc - jak możemy zrobić to inaczej? Czy w ogóle potrzebujemy switcha? No właśnie, może switch nie jest nam potrzebny. Czemu nie skorzystać z możliwości jakie tkwią w enumie? Przecież to jest coś więcej niż tylko zbiór pewnych stałych. Wystarczy trochę chęci i nasze rozwiązanie może wyglądać tak:
public int calculateDiscount(DiscountType discount) {
return discount.calculateDiscount();
}
public enum DiscountType {
NORMLAL(10), VIP(30), EMPLOYEE(40);
private int discountInPercent;
DiscountType(int discountInPercent){
this.discountInPercent = discountInPercent;
}
public int calculateDiscount() {
return discountInPercent;
}
}
Zastosowanie takiego rozwiązania dość mocno usprawnia nasz development. Nasz kod jest o wiele bardziej czytelny, a sam enum posiada jakieś biznesowe wartości, które nie są rozsmarowane po aplikacji. Wszystko w jednym miejscu. Ale pojawia się kolejne pytanie - co jeśli każdy element w enumie musi posiadać pewną logikę? Np. algorytm do wyliczenia zniżki będzie trochę bardziej skomplikowany? Pisanie przy każdym elemencie metody nie jest dobrym rozwiązaniem, ponieważ kod enuma stanie się mega nieczytelny. O wiele lepiej jest stworzyć jakiś wspólny interfejs. Najprostsza wersja tego rozwiązania może wyglądać w ten sposób:
public interface DiscountProvider {
int calculateDiscount();
}
public class SimpleDiscountProvider implements DiscountProvider {
private final int discountInPercent;
public SimpleDiscountProvider(int discountInPercent) {
super();
this.discountInPercent = discountInPercent;
}
@Override
public int calculateDiscount() {
return discountInPercent;
}
}
public enum DiscountType {
NORMLAL(new SimpleDiscountProvider(10)),
VIP(new SimpleDiscountProvider(30)),
EMPLOYEE(new SimpleDiscountProvider(40));
private DiscountProvider provider;
DiscountType(DiscountProvider provider){
this.provider = provider;
}
public int calculateDiscount() {
return provider.calculateDiscount();
}
}
Oczywiście każdy element może mieć swoją własną implementacje, które mogą być o wiele bardziej złożone, np:
public class VipDiscountProvider implements DiscountProvider {
private DayHolder dayHolder;
public VipDiscountProvider(DayHolder dayHolder) {
this.dayHolder = dayHolder;
}
@Override
public int calculateDiscount() {
if(dayHolder.getSaleDay().contains(LocalDate.now())){
return 50;
}
return 35;
}
}