JE_16: Dávejte přednost rozhraním před abstraktními třídami

Programovací jazyk Java nabízí dva mechanismy definování typu, který umož�uje více implementací: rozhraní a abstraktní třídy. Nejzřejmější rozdíl spočívá v tom, že abstraktní třídy mohou obsahovat implementace určitých metod, zatímco rozhraní nemohou. Důležitějším rozdílem je, ža aby bylo možné implementovat typ definovaný abstraktní třídou, musí být nějaká třída podtřídou dané abstraktní třídy. Rozhraní může implementovat každá třída, která definuje všechny vyžadované metody a vyhovuje obecnému konstraktu, bez ohledu na to, kde v hierarchii třídy se daná třída nachází. Protože Java umož�uje jen jednoduchou ddičnost, toto omezení týkající se abstraktních tříd výrazně snižuje možnost jejich použití jako definic typů.

Existující třídy lze snadno přizpůsobit tak, aby implementovaly nějaké nové rozhraní. Stačí jen přidat vyžadované metody, pokud ještě neexistují, a přidat do deklarace třídy klauzuli implements. Mnoho existujících tříd bylo například přizpůsobeno k implementaci rozhraní Comparable, když bylo na platformu zavedeno. Existující třídy nelze obecně upravit tak, aby rozšiřovaly nějakou novou abstraktní třídu. Chcete-li, aby dvě třídy rozšiřovaly jednu abstraktní třídu, musíte umístit abstraktní třídu vysoko do hierarchie typů, kde je nějaký předek obou vašich tříd podtřídou této abstraktní třídy. To však současně výrazně narušuje hierarchii typů, protože všichni potomci společného předka musejí rozšiřovat novou abstraktní třídu, ať už je to pro ně vhodné nebo ne.

Rozhraní jsou ideální pro definování smíšených tapů. Smíšeny typ je typ, který může třída implementovat jako doplněk svého "primárního typu", čímž dekaruje určité své volitelné chování. Například Comparable je smíšené rozhraní, které umož�uje nějaké třídě deklarovat, že její instance se uspořádavají s ohledem na další vzájemně porovnatelné objekty. Takové rozhraní se označuje za smíšené, protože nabízí "zamíchání" své volitelné funkčnosti do primární funkčnosti typu. Abstraktní třídy nelze použít k definování smíšených typů ze stejného důvodu, z jakého je nelze upravit pro již existující třídy: třída nemůže mít více než jednoho rodiče a v hierarchii tříd není žádné rozumné místo, kam by bylo možné uložit smíšený typ.

Rozhraní umož�ují konstrukci nehierarchických rámců typů. Typové hierarchie jsou vynikající pro organizování určitých věcí, jiné věci však do žádné pevné hierarchie nespadají. Předpokládejme například, že máme rozhraní představující zpěváka a jiné rozhraní představující skladatele:

public interface Singer {
	AudioClip Sing(Song s);
}

public interface Songwriter {
	Song compose(boolean hit);
}

V reálném světě jsou někteří zpěváci také skladateli. Protože jsme k definování těchto typů použili rozhraní a nikoli abstraktní třídy, může jedna třída velmi dobře implementovat jak Singer, tak i Songwriter. Dokonce můžeme definovat řetí rozhraní, které rozšiřuje třídy Singer i Songwriter a přidává nové metody, které jsou pro tuto kombinaci užitečné:

public interface SingerSongwriter extends Singer, Songwriter {
	AudioClip strum();
	void actSensitive();
}

Takovou úrove� flexibility nepotřebujete vždy, pokud však ano, pak vás mohou rozhraní spasit. Alterantivou je rozsáhlá hierarchie tříd obsahujíc samostatnou třídu pro každou podporovanou kombinaci atributů. Existuje-li v systému typů n atributů, pak máte 2n možných kombinací, které může být zapotřebí podporovat. taková situace se označuje za kombinační explozi. Rozsáhlé hierarchie tříd mohou vést k nafouknutým třídám obsahujících mnoho metod, jež se liší jen typy svých argumentů, protože v hierarchii tříd neexistují žádné typy zachycující obecná chování.

Rozhraní umož�ují bezpečná a mocná vylepšení funkčnosti prostřednictvím idiomu obalové třídy, ktrý byl popsán v radě 14. Používáte-li k definování typů abstraktní třídy, pak nutíte programátora, který chce přidat novou funkčnost, k používání dědičnosti. Výsledné třídy jsou méně silné a křehčí než obalové třídy.

Třebaže rozhraní nemohou obsahovat implementace metod, používání rozhraní k definování typů vám nezabra�uje v napomáhání s implementaci programátorům. Výhody rozhraní a abstraktních tříd můžete zkombinovat poskytnutím abstraktní třídy skeletální implementace, která bude u každého netriviálního rozhraní, jež exportujete. Rozhraní stále definuje daný typ, ale skeletální implementace přebírá veškerou práci související s jeho implementováním.

Skeletální implementace se standardně nazývají AbstractInterface, kde Interface je název rozhraní, které implementují. Například rámec kolekcí poskytuje skeletální implementaci doprovázející rozhraní každé hlavní kolekce: AbstractCollection, AbstractSet, AbstractList a Abstractmap.

Jsou-li dobře navržené, pak skeletální implementace umož�ují programátorům velmi snadno posyktnout své vlastní implementace vašich rozhraní. Zde máme například statickou tovární metodu obsahující úplnou, plně funkční implementaci List:

	// Adaptér seznamu pro atribut int
	static int intArrayAsList(final int[] a) {
		if (a == null)
			throw new NullPointerException();
		return new AbstractList() {
			public Object get(int i) {
				return new Integer(a[i]);
			}

			public int size() {
				return a.length;
			}

			public Object set(int i, Object o) {
				int oldVal = a[i];
				a[i] = ((Integer)o).intValue();
				return new Integer(oldVal);
			}
		};
	}

Když zvážíte, co všechno pro vás implmentace List udělá, zjistíte, že tento příklad je působivou ukázkou síly skeletálních implementací. Tento příklad vlastně představuje adaptér [Gamma95, str. 139], který umož�uje dívat se na pole int jako na seznam instancí Integer. Díky všem překladům mezi hodnostami int a instancemi Integer není výkonnost zrovna osl�ující. Všimněte si, že je poskytnuta statická tovární metoda a že daná třída je nepřístupnou anonymní třídou (rada 18), skrytou ve statické tovární metodě.

Krása skeletálních implementací spočívá v tom, že poskytují pomoc s implemenetací abstaraktních tříd bez zavádění zásadních omezení, která doprovázejí abstraktní třídy, když slouží jako definice typů. Pro vtšinu implementátorů nějakého rozhraní stojí na prvním místě rozšíření skeletální implementace, které je však přísně volitelné. Pokud již v existujíc třídě nelze zajistit rozšíření skeletální implementace, pak může taková třída vždy implementovat dané rozhraní manuálně. I zde může skeletální implementace úkol implementátora zjednodušovat. Třída implementující dané rozhraní může přesměrovávat volání metod na rozrhaní nějaké obsažené instanci soukromé vnitřní třídy, kerá rozšiřuje skeletální implementaci. tato technika, známá jako simulace vícenásobné dědičnsoti, úzce souvisí s principem obalové třídy popsaným v radě 14. Poskytuje většinu výhod vícenásobné dědičnosti a vyhýbá se zákeřným problémům.

Vytvoření skeletální implementace je relativně jednoduchá, byť trochu unávná záležitost. Nejprve si musíte prostudovat dané rozhraní a určit, které metody jsou primitivy, z jejichž hlediska lze implementovat ty další. Tyto primitivy budou abstraktními metodami ve vaší skeltální implementaci. Dále musíte poskytnout konkrétní implementace všech dalších metoda v rozhraní. Zde máme například skeletální implementaci rozhraní Map.Entry (v odbě psaní tohoto textu není tato třída součástí knihoven platformy Java, ale pravděpodobně by měla být:

// Skeletání implementace
public abstract class AbstractMapEntry implements MJap.Entry {
	// Primitivy
	public abstract Object getKey();
	public abstract getValue();
	
	// Zadání v měnitelých mapách musejí tuto metodu překrývat
	public Object setValue(Object value) {
		throw new UnsupportedOperationException();
	}

	// Implementuje obecný konstrakt Map.Entry.equals
	public boolean equals(Object o) {
		if (o == this) 
			return true;
		if (!(o instanceof Map.Entry))
			return false;
		Map.Entry arg = (Map.Entry)o;
		return eq(getKey(), arg.getKey()) &&
			eq(getValue(), arg.getValue());
	}

	private static boolean eq(Object o1, Object o2) {
		return (o1 == null ? o2 == null : o1.equals(o2));
	}

	// Implementuje obecný konstrakt Map.Entry.hashCode
	public int hashCode() {
		return
			(getKey() == null ? 0 : getKey.hashCode()) ^
			(getValue() == null ? 0 : getValue().hashCode());
	}
}

Protže skeletální implementace jsou určené pro dědění, měli byste se řídit všemi zásadami návrhu a dokumentace v radě 15. Kvůli zestručnění byly z předchozího příkladu vypuštěny komentáře, dobrá dokumetnace je však ve skeletálních implementacích naprosto zásadní.

Používání abstraktních tříd k fdefinování typů, jež umož�ují vícenásobné implementace, má jednu základní výhodu oproti používání rozhraní: Je mnohem jednodušší rozvíjet abstraktní třídu, než rozvíejt rozhraní. Chcete-li v dalších verzích přidat do nějaké abstraktní třídy novou metodu, vždy můžete přidat konkrétní metodu obsahující nějakou rozumnou výchozí implementaci. Tuto metodu pak budou poskytovat všechny existující implementace dané abstraktní třídy. U rozhraní to nefunguje.

Z obecného hlediska není možné přidat do veřejného rozhraní nějakou metodu, aniž by to nenarušilo všechny existující programy používající dané rozhraní. Třídám, které toto rozhraní již dříve implementovaly, bude tato nová metoda chybět a již se nezkompilují. kodu lze trochu omezit tím, že danou novou metodu přidáte do skeletální implementace zárove� s jejím přidáním do rozhraní, ale ani to problém neřeší. Narušeny budou stále všechny implementace, které nedědily z dané skeletální implementace.

Proto je zapotřebí navrhovat veřejná rozhraní opatrně. Jakmile je nějaké rozhraní uvolněno a široce implementováno, je téměř nemožné ho změnit. Skutečně musí být v pořádku hned napoprvé. Obsahuje-li rozhraní nějakou menší chybu, bude vás i jeho uživatele obtěžovat už napořád. Má-li rozhraní zásadní problémy, může API zcela znehodnotit. Při uvol�ování nového rozhraní je nejlepší nechat rozhraní implementovat co největším počtem programátorů v co největším možném počtu způsobů ještě před jeho "zmrazením". To vám pomůže odhalit všechny nedostatky v době, kdy je ještě můžete napravit.

Celkově lze říci, že orzhraní je obecně nejlepším způsobem definování typu, který umož�uje vícenásobné implementace . Výjimkou z tohoto pravidla je případ, kdy je snadnost postupného vývoje důležitější než flexibilita a síla. Za takového stavu byste měli k definování typu použí abstraktní třídu, pouze však pokud chápete a přijímáte všechna z toho vyplývající omezení. Exportujete-li netriviální rozhraní, vážně se zamyslete nad poskytnutím jeho skeletální implementace. Konečně platí, že byste měli navrhovat vššechna svá veřejná rozhraní s maximální pečlivosrí a dokonale je otestovat vytvořením více implementací.