JE_20: Nahrazujte sjednocení hierarchiemi tříd

Konstrukce union jazyka C se nějčastěji používá k definování struktur schopných obsahovat více než jeden typ dat. Taková struktura obsahuje obvykle přinejmenším dva atributy: sjednocení a značku. Značka je obyčejný atribut používaný k indikaci, který z možných typů obsahuje sjednocení. Značka je obecně nějakého typu enum. Struktura, obsahující sjednocení a značku, se někdy obsahuje za rozlišované (diskriminované) sjednocení.

V dále uvedeném příkladu jazyka C je typ shape_t rozlišovaným sjednocením, které lze použít k reprezentaci buď obdélníku nebo kruhu. Funkce area přebírá ukazatel na strukturu shape_t a vrací její plochu nebo -1.0, je-li daná konstrukce neplatná:

/* Rozlišované sjednocení */
#include "math.h"
typedef enum {RECTANGLE, CIRCLE} shapeType_t;

typedef struct {
	double length;
	double width;
} rectangleDimensions_t;

typdef struct {
	double radius;
} circleDimensions_t;

typedef struct {
	shapeType_t tag;
	union {
		rectangleDimensions_t rectangle;
		circleDimensions_t circle;
	} dimensions;
} shape_t;

double area(shape_t *shape) {
	switch (shape->tag) {
		case RECTANGLE: {
			double length = shape->dimensions.rectangle.length;
			double width = shape->dimensions.rectangle.width;
			return length * width;
		}
		case CIRCLE {
			double r = shape->dimensions.circle.radius;
			return M_PI * (r * r);
		}
		default: return -1.0; /* Neplatná značka */
	}
}

Návrháři programovacího jazyka Java se rozhodli vynechat konstrukci union, protože tu existuje mnohem lepší mechanismus definování jediného datového typu schopného reprezentovat objekty různých typů: vytváření podtypů. Rozlišované sjednocení je vlastně jen chabou imitací hierarchie tříd.

Chcete-li převést rozlišované sjednocení na hierarchii tříd, definujte nějakou abstraktní třídu obsahující určitou abstraktní metodu pro každou operaci, jejíž chování závisí na hodnotě značky. V předchozím příkůadě existuje jen jedna taková operace, area. Tato abstraktní třída je kořenem hierarchie tříd. Vyskytují-li se nějaké operace, jejichž chování nezávisí na hodnotě značky, převeďte tyto operace na konkrétní metody v kořenové třídě. Podobně existují-li v rozlišovaném sjednocení nějaké datové atributy kromě značky a sjednocení, tyto atributy představují data společná všem typům a měli byste je přidat do kořenové třídy. V našem příkladu žádné takové operace ani data závislá na typu nejsou.

Dále definujte konkrétní podtřídu kořenové třídy pro každý typ, který může být reprezentován daným rozlišovaným sjednocením. V předchozím příkladu jsou těmito typy kruh (circle) a obdélník (rectangle). Do každého podtypu zahr�te datové atributy příslušné jeho typu. Zde radius (poloměr) přísluší kruhu a length (délka) a width (šířka) přísluší obdélníku. Do každé podtřídy zahr�te také potřebnou implementaci každé abstraktní metody v kořenové třídě. Zde máme hierarchii tříd odpovídající našemu příkladu rozlišovaného sjednocení:

abstract class Shape {
	abstract double area();
}

class Circle extends Shape {
	final double radius;

	Circle(double radius) {
		this.radius = radius;
	}

	double area() {
		return Math.PI * radius * radius;
	}
}
class Rectangle extends Shape {
	final double length;
	final double width;

	Rectangle(double length, double width) {
		this.length = length;
		this.width = width;
	}

	double area() {
			return length * width;
	}
}

Hierarchie má oproti rozlišovanému sjednocení mnoho výhod. Z nich nejdůležitější je skutečnost, že hierarchie tříd zajišťuje zabezpečení typů. V příkladu je každá instance Shape buď platný Circle nebo platný Rectangle. Je velmi jednoduché vygenerovat strukturu shape_t, která je naprostým nesmyslem, protože jazyk nevynucuje asociaci mezi značkou a sjednocením. Jestliže značla indikuje, že shape_t představuje obdélník, ale sjednocení bylo nastaveno na kruh, pak je vše ztraceno. I po řádné inicializaci rozlišovaného sjednocení je možné předat je funkci, která hodnotě dané značky nepřísluší.

Druhou výhodou hierarchie tříd je jednoduchost a jasnost kódu. Rozlišované sjednocení je zahlcené zbytečnostmi: deklarováním typu enum, deklarováním atributu značky, změnou atributu značky, zpracováním neočekávaných hodnot značky apod. Čitelnost kódu rozlišovaného sjednocení dále snižuje fakt, že operace pro různé typy jsou smíchány dohromady a nikoli pdděleny podle typu.

Třetí výhodou hierarchie tříd je její snadná rozšiřitelnost, třeba i více nezávisle pracujícími týmy. Chcete-li rozšířit hierarchii tříd, prostě přidáte novou podtřídu. Zapomenete-li překrýt některou z abstraktních metod v nadtřídě, kompilítor vám to naprosto jednoznačně oznámí. Abyste mohli rozšířit rozlišované sjednocení, musíte mít přístup ke zdrojovému kódu. Muíste přidat novou hodnotu k typu enum a také nový případ do příkazu switch v každé operaci s rozlišovaným sjednocením. Nakonec musíte vše znovu zkompilovat. Zapomenete-li poskytnout nový případ v nějaké metodě, zjisíte to až za běhu a to ještě jen za předpokladu, že jste velmi opatrní a kontrolujete neznámé hodnoty značky a generujete příslušná chybová hlášení.

Čtvrtá výhoda hierarchie tříd spočívá v tom, že může odrážet přirozené hierarchické vztahy mezi typy, což zaručuje větší pružnost a lepší kontrolu typů během kompilování. Představme si, že rozlišované sjednocení z našeho původního příkladu podporuje také čtverce. Lze vytvořit takovou hierarchii tříd, která zachytí skutečnost, že čtverec je speciálním typem obdélníku (za předpokladu že jsou oba neměnitelné):

class Square extends Rectangle {
	Square(double side) {
		super(side, side);
	}

	double side() {
		return length; // Nebo stejným způsobem width
	}
}

Hierarchie tříd v tomto příkladu není jedinou, kterou by bylo možné vytvořit jako náhradu rozlišovaného sjednocení. Tato hierarchie ztěles�uje několik rozhodnutí návrhu, která stojí za přiblížení. Ke třídám v hierarchii, s výjimkou třídy Square, se přistupuje jejich atributy a nikoli přístupovými metodami. To bylo učiněno kvůli stručnosti a bylo by nepřípustné, kdyby byly tyto třídy veřejné (rada 19). Třídy jsou neměnitelné, což není vždy vhodné, ale obecně je to lepší (rada 13).

Protože programovací jazyk Java neposkytuje konstrukci union, mohli byste si myslet, že vůbec není nebezpečené implementovat rozlišované sjednocení. Přesto však lze napsat kód s mnoha stejnými nevýhodami. Kdykoli máte dojem, že musíte vytvořit třídu s explicitním atributem značky, zamyslete se nad tím, zda by nebylo možné značku odstranit a nahradit hierarchií tříd.

Další použití konstrukce union jazyka C, které vůbec nesouvisí s rpzlišovanými sjednoceními, spočívá v prohlížení interní reprezentace nějakých dat, což je vlastně úmyslné narušování typového systému. Toto použití ilustruje následující ukázka kódu jazyka C, která tiskne hexadecimální reprezentaci typu float specifickou pro daný počítač:

union {
	float f;
	int bits;
} sleaze;

sleaze.f = 6.699e-41; /* Vložit data do jednoho atributu... */
printf("%x\n", sleaze.bits); /* ...a přečíst je z druhého. */

Třebaže to může být užitečné, zejména při programování systému, tot nepřenositelné použití nemá v programovacím jazyku Java žádný protějšek. Došlo by k porušení etiky ducha jazyka, který zaručuje zabezpečení typů a výrazně se snaží izolovat programátory od interních reprezentací specifických pro jednotlivé počítače.

Balíček java.lang sice obsahuje metody překládající čísla s pohyblivou desetinnou čárkou na bitové reprezentace, ale tyto metody jsou definované pouze s ohledem na přesně specifikovanou bitovou reprezentaci zajišťující přenositelnost. Následující ukázka kódu, která se trochu podobá výše uvedenému kódu jazyka C, vytiskne vždy stejný výsledek bez ohledu na to, kde běží:

System.out.println(
	Integer.toHexString(Float.floatToIntBits(6.699e-41f)));