poniedziałek, 17 listopada 2014

Czego nie robić w kodzie - switch + enum

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;
 }

}