Enumy dla średnio-zaawansowanych
Warszawski JUG ma przerwę wakacyjną, ale gorące trwają. Skrzynki mailowe zapełniają się, a serwery poczty zapychają. W ostatnim czasie pojawiło się kilka (pierwsza, druga, trzecia) dyskusji, w których przewinął się temat użycia enumów w różnych kontekstach.
W tym wpisie postaram się ogarnąć część tematu.
Przypomnienie podstaw
Zakładam, że każdy wie co to jest enum. Jak go zdefiniować i jak można go używać. Dla porządku przypomnę tylko, że zostały one wprowadzone w javie 5 jako rozwiązanie problemu statycznych stałych znacznikowych typu int (notabene programiści zapominają o tym i nawet w Vaadin jest radosne stosowanie intów zamiast enumów).
Warto sobie jednak uzmysłowić czym w kodzie javy tak naprawdę jest enum. W procesie kompilacji jest on zamieniany na klasę rozszerzającą klasę Enum dodawane są metody takie jak values czy valueOf oraz co najważniejsze zadeklarowane wartości są zamieniane na statyczne finalne pola typu takiego jak klasa, a konstruktor jest oznaczany jako private. Sam JVM jak i kompilator mają wbudowane jeszcze kilka ciekawostek, które pozwalają np. używać enumów w switchach, ale to już są fajerwerki.
Podobny efekt można by było uzyskać w starszych wersjach Javy tworząc coś w ten deseń:
Listing 1. Dwa podejścia
// współczesny enum
public enum SimpleEnum {
BOTTOM, LEFT, RIGHT, TOP
}
// emulowany enum
public abstract class SimpleEnumAsClass {
public static final SimpleEnumAsClass BOTTOM = new SimpleEnumAsClass() {
@Override
public String name() {
return "BOTTOM";
}
};
public static final SimpleEnumAsClass LEFT = new SimpleEnumAsClass() {
@Override
public String name() {
return "LEFT";
}
};
public static final SimpleEnumAsClass RIGHT = new SimpleEnumAsClass() {
@Override
public String name() {
return "RIGHT";
}
};
public static final SimpleEnumAsClass TOP = new SimpleEnumAsClass() {
@Override
public String name() {
return "TOP";
}
};
public static final SimpleEnumAsClass[] values() {
return new SimpleEnumAsClass[] { BOTTOM, LEFT, RIGHT, TOP };
}
private SimpleEnumAsClass() {
}
public abstract String name();
// reszta metod
}
Wygląda to trochę dziwnie, ale z ciekawości odpaliłem w javie 1.4 i śmiga. Oczywiście nie mamy dostępu do pełnej funkcjonalności oryginalnych enumów, ale… w javie 1.4 jedyne co mi przychodzi do głowy to użycie w switch, bo wraz z enumami w javie 5 pojawiło się dużo ciekawych rzeczy typu pętle foreach czy kolekcje generyczne, gdzie rzeczywiście można wykorzystać enumy.
Po tej małej wycieczce przyjrzyjmy się pierwszej funkcjonalności enumów jaką jest zastąpienie wzorca Singleton.
Enum jako singleton
Ze względu na swoją specyficzną konstrukcję enum może zostać wykorzystany jako baza do tworzenia obiektów – singletonów. Prywatny konstruktor oraz ukryta po stronie kompilatora metoda tworzenia obiektu pozwala nam na dość szybkie tworzenie singletonów jako enumów.
Oczywiście należy wziąć pod uwagę, że zarówno singleton jak i enum jeżeli przechowują stan to robią to globalnie. Stąd też sam wzorzec singleton nie jest fajny. Istnieje jednak pewna grupa klas, które mogą okazać się znacznie wygodniejsze jeżeli zastąpimy je za pomocą enuma. Są to helpery.
Enum jako helper lub klasa narzędziowa
Ze swojego założenia helper nie przechowuje stanu. Zazwyczaj do tej roli awansowane są różnej maści klasy narzędziowe czy klasy operujące na jakimś typie biznesowym w określony sposób. Jednym z ciekawszych zastosowań jest to przedstawione przez Lasu na liście, czyli enum jako dawca pewnych elementów funkcyjnych:
Listing 2. Enum jako implementacja helpera do sortowania
public class Name {
public final String fName;
public final String lName;
public final Integer age;
public Name(String fName, String lName, int age) {
super();
this.fName = fName;
this.lName = lName;
this.age = age;
}
public static enum NameComparator implements Comparator<Name> {
byFName {
@Override
public int compare(Name o1, Name o2) {
return o1.fName.compareTo(o2.fName);
}
},
byLName {
@Override
public int compare(Name o1, Name o2) {
return o1.lName.compareTo(o2.lName);
}
},
byAge {
@Override
public int compare(Name o1, Name o2) {
return o1.age.compareTo(o2.age);
}
}
}
}
Przy czym funkcyjność oznacza, że nie mówimy JAK chcemy coś zrobić, a CO chcemy zrobić. Dostarczone API nam to zapewnia.
Na tej samej zasadzie można tworzyć np. enumy zbierające popularne walidatory w jakieś rozsądne zestawy. W chwili wolnej udostępnię odpowiedni jar 😀
Jednak osoba zdolna szybko dojdzie do wniosku, że skoro enumy mogą implementować interfejsy to można zmusić je do pracy jako elementy jakiś predefiniowanych strategii.
Enum jako strategia i stan
Załóżmy, że mamy formularz do edycji danych użytkownika. Róże elementy systemu wykorzystują ten formularz w różny sposób. Przykładowo moduł edycji pozwala na zmianę niektórych pól, administracyjny na dodanie nowego użytkownika, a do tego jeszcze można oglądać poszczególnych użytkowników.
Można zatem dostarczyć kilka klas, które będą posiadały różne sposoby budowy formularza. W praktyce kilka różnych formularzy. Można jednak pokusić się o implementację jednej klasy, która będzie wykorzystywać enumy do zmiany zachowania (stan). W samych enumach można w końcu zaszyć już niejawnie(zasięg pakietowy) odpowiednie metody budujące. Wybór enuma jako stanu formularza pociągnie niejawny wybór strategii jego budowy.
Listing 3. Enum jako implementacja stanu i strategii
public class Form {
public enum FormState {
ADD {
@Override
public void build(Form form) {
form.enabelAll();
}
},
EDIT {
@Override
public void build(Form form) {
form.enableFName();
}
};
abstract void build(Form form);
}
private final FormState state ;
public Form(FormState state) {
super();
this.state = state;
}
protected void enabelAll() {
}
protected void enableFName() {
}
}
W końcu podejście takie pozwala na pozbycie się z kodu switchy, bądź wieży if-ów, ponieważ mając jako argument enum wywołujemy odpowiednią metodę.
Podsumowanie
Typy wyliczeniowe w Javie to potężne narzędzia, które odpowiednio użyte bardzo upraszczają kod czyniąc go zarówno czytelniejszym jak i łatwiejszym w testowaniu i dalej w utrzymaniu. Warto jednak pamiętać, że zbytnie nadużywanie typów wyliczeniowych może wpędzić nas w nie lada kłopoty kiedy to powstaną klasy typu złoty młot czy gdy jakiś kawałek systemy będzie nierozszerzalny bez na przykład dodania kilkunastu wartości do enuma.