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 {
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> {
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í.
Související články
Související epizody CZ Podcastu
- #3 · Java 6 SE, Vývoj webových aplikací
Dagi a Filemon o novinkách v Javě 6 a webových frameworcích.
- #1 · Vývojová prostředí v Javě
První díl CZ Podcastu se věnuje vývojovým prostředím pro Javu.