čtvrtek 7. února 2013

Statická typovost s marker interface vs. anotace

Statická typovost Javy sebou nese vyšší množství kódu, který píšeme pro potřeby kompilátoru, ačkoliv bychom se bez něj v mnoha případech obešli. Je to jistím dílem setrvačností, že používáme konstrukty, které jsme byli nuceni používat ještě za časů, kdy byly anotace zbožným přáním a vlastností, po které jsme pošilhávali do .NET. Jedním z těchto konstruktů je takzvaný marker interface a jeho variace, které vídám všude kolem sebe. Zjednodušeně řečeno marker interface nás stojí hodně řádků kódu za málo muziky.

Docela nedávno jsme Lukášem Křečanem řešili půlení a čtvrcení jednoho modulu. Abych to zkrátil, kamenem úrazu se stala právě třída, kterou bych označil za marker interface. Separace této třídy totiž znamenala vytvoření třetího modulu, kde by byla pouze a jenom tato třída.

public interface Persistable<K> {
  void setId(K key);
  K getId();  
}

Celá tahle čimčaráda sloužila k tomu, abych mohl mít podle zažitého idiomu odpovídající data access object.

public abstract class AbstractDao<P extends Persistable<K>, K> {
  P load(K key);
  void save(P persistable);
  ...  
}

Díky použití marker interface jsme si zajistili pocit compile time bezpečnosti. Schválně používám slovo pocit, jehož význam vysvětlím za malý okamžik. Díky rozhraní Persistable a odpovídající definici AbstractDao, nám kompilátor neumožní použít dohromady AbstractDao s třídou, která není Persistable. Jinak řečeno, následující kód nebude kompilovatelný.

AbstractDao dao = ...
dao.save(new Object());

Tím by se daly uzavřít asi všechny výhody marker interface. Teď k nevýhodám. Už jsem nakousl compile time bezpečnosti. Pokud máte všechen kód na jedné hromadě a kompilujete ho vždy, pak to není skutečně pocit, ale jistota. Sranda nastane ve chvíli, kdy třídy implementující marker interface leží v jiném modulu jako kompilační závislost. Ano narážím na binární kompatibilitu v runtime. Může se vám totiž stát, že zkompilujete proti interfacu s metodou P load(K key), ale v runtime budete mít novější verzi modulu, kde bude interface bohatší o další metodu, kterou bude AbstractDao používat. Ve výsledku jste tedy bez pořádných integračních testů nahraní a kompilátor vám nepomůže.

Podívejme se, jak by mohla vypadat alternativa, kterou jsem naznačoval v úvodním odstavci. Pro zjednodušení nebudu uvádět definici vlastních anotací, ale příklad objektu, který je používá namísto marker interface.

@Persistable
public class Person {
 @Id
 private Serializable id;
}

Anotace Persistable je zde uvedená namísto marker interface, ale obešli bychom se i bez ní. Uvedený příklad asi nepotřebuje další vysvětlení, snad kromě poznámky, že AbstractDao si pres reflection přečte instační proměnnou id. Použití anotací přináší několik výhod:

  • Méně kódu
  • Anotaci mohu aplikovat jak na instanční proměnné tak i na metody. Na použité třídě se mohu rozhodnout jestli bude proměnná id součástí veřejného rozhraní (pokud bych použil veřejný setter/getter) nebo nikoliv.
  • Přestože je anotace Persistable a Id vyžadována pro kompilaci, není potřeba za běhu. To je výhodné pokud se rozhodnete třídu použít v jiném kontextu. V našem případě byla třída použitá i na klientu, kde persistentní ukládání nedávalo žádný smysl. Za použití marker interface, bychom riskovali zatažení dalších nechtěných závislostí, kterým bychom se vyhýbali jenom za cenu speciálního modulu s právě s marker interface.
  • O compile time bezpečnost nemusíme přijít úplně, pokud si naimplementujeme vlastní anotační procesor, který bude kontrolovat, že v třídě s anotací Persistable je instanční proměnná nebo getter/setter označený pomocí anotace Id. Na konto anotačních procesorů přidávám odkaz na prezentaci Jaroslava Tulacha o API paradoxes.

V tomto článku jsem chtěl upozorni na jeden z reliktů, který zbytečně přežívá v code base mnoha javových projektů, a alternativu kterou nám nabízí anotace. Anotace rozvolňují striktní statickou typovost a tím pádem i ušetří velké množství kódu.

Update

Připravil jsem příklad rozdílného chování anotací vs. rozhrani v runtime. V případě anotací nemusí být daná anotace k dispozici na classpath pokud se s ní nepracuje. Naopak třída rozhraní tam musí být vždy. Tato výhoda nám umožňuje využívat třídu v různých kontextech bez zatažení nechtěných závislostí.