Enum w warunkach jak zrobić to lepiej
Kiedyś dawno temu poruszałem temat enumów. Opisałem jak można ich używać do różnych celów i jak za ich pomocą enkapsulować logikę. Java 8 dostarczając nam lambd pozwoliła na prostsze (mniej rozwlekłe) użycie enumów wszędzie tam gdzie mamy do czynienia z logiką warunkową.
Klasyka gatunku
Bardzo często enumy służą jako warunki wykonania pewnych operacji. W linkowanym wyżej poście opisałem jak enum może robić za „dostawcę strategii”. Niestety takie rozwiązanie ma tą wadę, że nadużywane spowoduje iż w jednym enumie będziemy mieli wiele niezwiązanych ze sobą operacji. Wróćmy zatem do podstawowego problemu jakim jest użycie enuma w instrukcjach warunkowych:
Listing 1. Drabinka if-ów
public <T> QueryCondition build(Operator op, T left, T right) {
if (Operator.LT == op)
return new LessThan(left, right);
if (Operator.EQ == op)
return new Equal(left, right);
if (Operator.GT == op)
return new GreaterThan(left, right);
throw new ServiceException(format("Operator %s not supported", op));
}
Bardzo brzydkie rozwiązanie. Co ciekawe czasami stosowane ponieważ mogą istnieć reguły jakości kodu, które zabraniają użycia instrukcji switch…
Listing 2. Switch
public <T> QueryCondition build(Operator op, T left, T right) {
switch (op) {
case LT:
return new LessThan(left, right);
case EQ:
return new Equal(left, right);
case GT:
return new GreaterThan(left, right);
}
throw new ServiceException(format("Operator %s not supported", op));
}
Wygląda to trochę lepiej. Choć nadal sonar będzie krzyczał, że metoda jest zbyt złożona. Szczególnie przy dużej liczbie warunków może okazać się, że złożoność takiej metody gwałtownie rośnie.
EnumMapa jako opakowanie logiki
Zaradzić temu można za pomocą odpowiednio stworzonej mapy. Jest to podstawowa metoda na delegowanie tego typu logiki poza nasz kod.
Listing 3. Mapa
public <T> QueryCondition build(Operator op, T left, T right) {
Map<Operator, BiFunction<T, T, ? extends QueryCondition>> operations = new EnumMap<>(Operator.class);
operations.put(LT, LessThan::new);
operations.put(EQ, Equal::new);
operations.put(GT, GreaterThan::new);
return operations.getOrDefault(op, ($, $$) -> {
throw new ServiceException(format("Operator %s not supported", op));
})
.apply(left, right);
}
Co więcej w tym przypadku możemy też tworzenie mapy operacji przesunąć poza metodę oraz dodać trochę analizy kodu. Wtedy kod będzie wyglądał następująco:
Listing 4. Mapa inicjowana na zewnątrz
public QueryCondition build(@Nonnull Operator op, T left, T right) {
return operations.get(op).apply(left, right);
}
na koniec jeszcze uwaga o metodzie getOrDefault jest to bardzo pomocne narzędzie, które pozwala na przeniesienie części logiki z naszego kodu do mechanizmów mapy. Zatem warto się zainteresować tą metodą.