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

}

sobota, 23 sierpnia 2014

Typy generyczne w kontekście kolekcji

Typy generyczne w javie są powszechnie znane i używane. Praktycznie każdy programista, który używa javy, używa typów generycznych. Znaczna większość z nas wykorzystuje je świadomie, ale znajdą się też tacy, którzy używają ich przez przypadek, bo po prostu skopiowali przykład użycia z jakiegoś forum :). Dziś krótkie przypomnienie jak korzystać z typów generycznych w kontekście kolekcji.

Zaczniemy od najprostszego przykładu, czyli listy w której określamy jakie obiekty będą się tam znajdować. Taka konstrukcja sprawdza się w 99,9% sytuacj z jakimi mamy do czynienia na co dzień. Lista persons jest w pełni użyteczna, można do niej dodawać elementy, usuwać, pobierać i modyfikować. Jedyne na co nie pozwoli nam kompilator, to przypisanie jednej listy do innej, która zawiera typ pokrewny, np. listy teachers nie możemy przypisać do listy persons, mimo tego, że obiekt Teacher dziedziczy po Person. Poniżej kawałek kodu, który pokazuje to zachowanie.
 List<Person> persons = createList();
 persons.add(new Person());
 Person p = persons.get(0);

 List<Teacher> teachers = createList();
// persons = teachers; error
Oczywiście jeśli będziemy potrzebowali takiego przypisania, możemy lekko zmodyfikować nasz kodzik poprzez powiedzenie kompilatorowi, że nasza lista będzie zawierać obiekty, które dziedziczą po obiekcie Person. Po modyfikacji nasz kod prezentuje się następująco.
 List<? extends Person> persons = createList();
// persons.add(new Person()); error
 Person p = persons.get(0);
 
 List<Teacher> teachers = createList();
 persons = teachers;
Możemy teraz przypisywać listę do listy i nie mamy błędu, ale w momencie, gdy chcemy coś wstawić do listy persons dostajemy błąd kompilacji. Na początku może nam się wydawać, że jest to dziwne zachowanie, ale po dłuższym zastanowieniu uświadamiamy sobie, że jest to nawet logiczne :). Kompilator zgłosi błąd w tym przypadku dlatego, że nie może jednoznacznie stwierdzić, jaki typ znajduje się w liście. Wiadomo tylko, że lista zawiera obiekty, które będą dziedziczyć po Person. Żeby lepiej zrozumieć ten przypadek, napiszmy sobie przykładową implementację metody createList().
 private List<Teacher> createList() {
  List<Teacher> teachers = new ArrayList<>();
  teachers.add(new Teacher());
  return teachers;
 }
Metoda zwraca listę obieków Teacher i taką listę możemy z powodzeniem przypisać do listy persons. Teraz jeśli kompilator pozwoliłby nam dodać do listy jakikolwiek obiekt, który dziedziczy po Person to mielibyśmy niespójność, bo na liście, w której powinny znajdować się obiekty Teacher znalazłby się obiekt innego typu, np. Person.

Typy generyczne posiadają jeszcze jedną magiczną odsłonę, a mianowicie możemy zadeklarować listę z nadtypami pewnej klasy. Wygląda to mniej więcej tak:
 List<? super Person> persons = createList();
 persons.add(new Person());
// Person p = persons.get(0); error
Do listy, która jest zadeklarowana w ten sposób, możemy dodawać obiekty, które dziedziczą po Person, ale nie możemy z takiej listy pobierać elementów. Tutaj znowu musimy się chwilę zastanowić, ponieważ cała sytuacja jest dość dziwna i niecodzienna. Dla lepszego jej zrozumienia napiszmy sobie kolejną implementację metody createList().
 private List<Base> createList() {
  List<Base> bases = new ArrayList<>();
  bases.add(new Base());
  bases.add(new Teacher());
  return bases;
 }
Załóżmy, że klasa Teacher dziedziczy po Person, a klasa Person dziedziczy po Base. Metoda zwraca listę obiektów Base, w naszym przykładzie jest jeden obiekt Base i jeden Teacher. Taka lista może być przypisana do listy persons, ponieważ konstrukcja List<? super Person> zakłada, że możemy tutaj przypisać każdą listę, na której są obiekty znajdujące się w hierarchii dziedziczenia nad Person (Base znajduje się nad Person). Jeśli wstawimy do takiej listy obiekt Person to lista będzie cały czas spójna. Niestety taka lista nie nadaje się do odczytu elementów, ponieważ nie wiemy jakiego typu obiekty się tam znajdują, wiemy tylko, że typ Person jest nadtypem do typów na liście.

Jeśli macie jakieś przemyślenia albo pytania, to zachęcam do umieszczania ich w komentarzach.