JE_15: Dědění navrhněte a zdokumentujte, nebo je zakažte

Rada 14 vás upozornila na nebezpečí vytváření podtřídy nějaké "cizí" třídy, která nebyla pro dědičnost vytvořena ani ji nemá zdokumentovanou. Co vlastně znamená navrhnout a zdokumentovat třídu pro dědění?

Za prvé, třída musí přesně dokumentovat vliv překrytí všech metod. Jinými slovy, třída musí dokumentovat své vlastní používání překrytelných metod: u každé veřejné nebo chráněné metody či konstruktoru musí její dokumentace říkat, které překrytelné metody volá, v jakém pořadí a jak výsledky každého volání ovliv�ují další zpracování (překrytelnou myslím neefinální a buď veřejnou nebo chráněnou). Obecněji, třída musí dokumentovat všechny podmínky, za nichž může zavolat nějakou překrytelnou metodu. Volání mohou například pocházet od vláken na pozadí nebo statických inicializátorů.

Metoda, která volá překrytelné metody, obsahuje podle konvence popis těchto volání na konci svého dokumentačního komentáře. Tento popis začíná frází "Tato implementace". Uvedenou frázi nelze chápat jako náznak, že se toto chování může v dalších verzích změnit. Jen znamená, že popis se zabývá vnitřním fungováním dané metody. Zde máme příklad zkopírovaný ze specifikace java.util.AbstractCollection:

Uvedená dokumentace nás nenechává na pochybách v tom, že překrytí metody iterator bude mít vliv na chování metody remove. navíc přesně popisuje, jak chování třídy Iterator vrácené metodou iterator ovlivní chování metody remove. Srovnejte si to se situací v radě 14, kde programátor vytvářející podtřídu třídy HashSet prostě nevěděl, zda překrytí metody add ovlivní chování metody addAll.

Neporušuje se však tím předpis, že dobrá dokumentace API má popisovat, co daná metoda dělá, nikoli však jak to dělá? Ano, porušuje! To je nešťastný důsledek skutečnosti, že dědičnost narušuje zapouzdření. Chcete-li zdokumentovat třídu tak, aby bylo možné bezpečně vytvářet její podtřídy, musíte popsat implementační detaily, které by jinak měly zůstat nespecifikované.

Návrh dědičnosti zahrnuje víc než jen deokumentování vzorů používání sama sebe. Má-li mít programátor možnost psát efektivní podtřídy bez zybtečných problémů, pak musí daná třída nabízet náznaky svého interního fungování ve formě soudně vybraných chráněných metod, nebo ve výjimečných případech také chráněných atributů.

Zvažme například metodu removeRange z java.util.AbstractList:

Tato metoda vůbec nezajímá koncové uživatele implementace List. Je poskytnuta výhradně proto, aby mohly podtřídy jednoduše nabídnout nějakou rychlou metodu clear podseznamům. Pokud by metoda removeRange neexistovala, podtřídy by se musely spokojit s kvadaratickou výkonnsotí volání metody clear na poddsezanmech, nebo by musely od základu přepsat celý mechanismus subList - nic jednoduchého!

Jak tedy určíte, které chráněné metody nebo atributy vystavovat, když navrhujete třídu pro dědění? Bohužel neexistuje žádné magické zaklínadlo. Můžete jen usilovně přemýšlet, co nejlépe vše odhadnout a pak si výsledek vyzkoušet vytvoření nějakých podtříd. Měli byste poskytnou t co nejmenší množství chráněných metod a atributů, protože každý z nich představuje závazek k implementačnímu detailu. Na druhou stranu jich nesmíte poskytnout příliš málo, jelikož chybějící chráněná metoda může učinit třídu prakticky nevyužitelnou z hlediska dědičnosti.

Navrhujete-li pro dědní třídu, která se bude pravděpodobně široce používat, pak si uvědomte, že se navždy zavazujete ohledně vzorů používání sebe sama, které dokumentujete, a ohledně implementačních rozhodnutí implicitních atributů v jejích chráněných metodách. Tyto závazky mohou omezit nebo znemožnit vylepšování výkonnosti nebo funkčnosti takové třídy v dalších verzích.

Také si uvědomte, že speciální dokumentace potřebná kvůli dědičnosti komplikuje normální dokumentaci, jež je určená pro programátory, kteří vytvářejí instance vaší třídy a volají s nimi metody. V době psaní této knihy existuje jen velmi málo nástrojů či komentovacích konvencí, které by oddělovaly obvyklou dokumentaci API od informací zajímavých pouze pro programátory implementující podtřídy.

Existuje ještě několik omezení, kterým musí třída vyhovovat, aby umož�ovala dědičnost. Konstruktory nesmějí volat překrytelné metody, ať už přímo nebo nepřímo. Je-li toto pravidlo porušené, pak bude důsledkem pravděpodobně selhání programu. Konstruktor nadtřídy se vykonává před konstruktorem podtřídy, takže překrývající metoda v podtřídě se zavolá, ještě než proběhne konstruktor podtřídy. Pokud taková překrývací metoda závisí na nějaké inicializaci vykonávané konstruktorem podtřídy, pak se tato metoda nebude chovat podle očekávání. Vše si ukážeme na konkrétním příkladu malé třídy, jež porušuje uvedené pravidlo:

public class Super {
	// Chyba - konstruktor volá překrytelnou metodu
	public Super() {
		m();
	}

	public void m() {
	}
}

Zde máme podtřídu, která překrývá metodu m, jež je chybně volána jediným konstruktorem třídy Super:

final class Sub extends Super {
	private final Date date; // Nastaví se v konstruktoru

	Sub() {
		date = new Date();
	}

	// Překrývá Super.m, volána konstruktorem Super()
	public void m() {
		System.out.println(date);
	}

	public static voidmain(String[] args) {
		Sub s = new Sub();
		s.m();
	}
}

Mohli byste očekávat, že tento program vytiskne dvakrát datum, ve skutečnosti se však poprvé vytiskne null, protože metoda m je volána kosntruktorem Super() ještě předtím, než může konstruktor Sub() inicializovat atribut date. Všimněte si, že tento program azchycuje určitý finální atribut ve dvou různých stavech.

Rozhraní Cloneable a Serialzable představují při návrhu pro dědění zvláštní potíže. Obvykle není vhodné, aby třída určená pro dědění implementovala některé z těchto rozhraní, protože ta kladou na programátora, jenž takovou třídu rozšiřuje, značnou zátěž. Existují však speciální akce, které vám umožní implementovat v podtřídách tato rozhraní, aniž k tomu budete nuceni. Tyto akce jsou vysvětleny v radě 10 a radě 54.

Rozhodnete-li se implementovat Cloneable nebo Serializable ve třídě určené pro dědění, pak si musíte uvědomit, že jelikož se metody clone a readObject chovají do zančné míry jako konstruktory, platí pro ně i podobné omezení. Ani clone ani readObject nesmějí volat nějakou překrytelnou metodu, přímo ani nepřímo. V případě metody readObject se překrývající metoda spustí ještě před deserializací stavu podtřídy. V případě metody clone se překrývající metoda spustí dříve, než dokáží metody clone podtříd zafixovat stav klonu. V obou případech dojde pravděpodobně k selhání programu. V případě metody clone může takové selhání poškodit klonovaný objekt i klon samotný.

Když se rozhodnete implementovat Serializable ve třídě určené pro dědění a tato třída má metodu readResolve nebo writeReplace, pak musíte učinit metodu readResolve nebo writeReplace chráněnou a nikoli soukromou. Jsou-li tyto metody soukromé, podtřídy je budou tiše ignorovat. to je další případ, kdy se impleemntační detail stává součástí API třídy, aby bylo možné dědění.

Nyní je již zřejmé, že vytváření třídy pro dědění klade na danou třídu zančná omezení. Není to rozhodnutí, které byste měli učinit rychle a snadno. Existují situace, kdy je to evidentně naprosto v pořádku, jako jsou například abstraktní třídy, včetně skeletálních implementací rozhraní (rada 16). Existují další situace, kdy je to jasně nesprávné, například u neměnitelných tříd (rada 13.

A co obvyklé konkrétní třídy? Obvykle nejsou ani finální ani nejsou vytvořeny či zdokumentovány pro vytváření podtříd, ale tento dtav je nebezpečný. Kdykoli dojde v takové třídě ke změně, pak existuje možnost, že se zhroutí klientské třídy rozšiřující tuto třídu. To není jen teoretický problém. Není obvyklé, že dostáváte hlášení o chybách souvisejících s podtřídami po změně vnitřních součástí nefinální konkrétní třídy, která nebyla ani vytvořena ani dokumentována pro dědění.

Nejlepším řešením tohoto problému je zakázat vytváření podtříd ve třídách, které nejsou vytvořeny ani zdokumentovány tak, aby umož�ovaly bezpečné vytváření podtříd. Existují dvě možnosti, jak zakázat vytváření podtříd. Jednodušší z nich je deklarovat takovou třídu jako finální. Alternativou je učinit všechny konstruktory soukromými nebo přítelskými (výchozími) a přidat veřejné statické tovární metody namísto konstruktorů. Tato alterantiva, která umož�uje pružně používat podtřídy interně, je popsána v radě 13. Oba přístupy jsou přijatelné.

Tato rada je poněkud kontroverzní, protože mnoho programátorů si již zvyklo vytářet podtřídy obvyklých konkrétních tříd, potřebují-li přidat určité možnosti, jakými jsou instrumentace, upozor�ování a synchronizace, nebo chtějí-li omezit funkčnost. Pokud třída implementuje nějaké rozhraní, které zachycuje její esenci, jako je například Set, List nebo Map, pak se vůbec nezdráhejte vytváření podtříd zakázat. Vzor s obalovou třídou popsaný v radě radě 14 posyktuje alternativu nadřazenou dědičnosti, pokud jde o změnu funkčnosti.

Jestliže konkrétní třída neimplementuje standardní rozhraní, pak můžete zákazem dědění způsobit některým programátorům problémy. Máte-li pocit, že musíte povolit dědění z takové třídy, pak je rozumné zajistit, aby tato třída nikdy nevolala žádnou ze svých překrytelných metod a tuto skutečnost zdokumentovat. jinými slovy, zcela odstra�te využívání překrytelných metod třídou samotnou. Když tak učiníte, vytvoříte třídu, z níž bude rozumně bezpečné vytvářet podtřídy. Překrytí nějaké metody nikdy neovlivní chování žádné jiné metody.

Využívání překrytelných metod třídou samotnou můžete zabránit mechanicky, beze změny jejího chování. Přesu�te tělo každé překrytelné metody do nějaké soukromé "pomocné metody" a nechte každou překrytelnou metodu volat svou soukromou pomocnou metodu. Pak nahraďte každé používání překrytelné metody ve třídě samotné přímým voláním soukromé pomocné metody dané překrytelné metody.