98
OBJEKTOVÉ PROGRAMOVÁNÍ – NAUČTE SE PRAVIDLA OBJEKTOVÉHO MYŠLENÍ
6.2.2 Strukturální hledisko Z pohledu struktury je – jaksi naopak – zcela zřejmé to, že v minulém bodě jsme to vzali za zcela špatný konec. Reálná čísla jsou přece speciálním případem komplexních; kdekoli, kde může stát komplexní číslo, může vždy stát také reálné – neboť reálné číslo ve skutečnosti není nic jiného, než komplexní číslo s nulovou imaginární složkou. Naopak to ovšem neplatí: počítáme-li pouze s reálnými složkami a ignorujeme-li (nenulové) imaginární, dostaneme nesmysl. Podle substitučního principu tedy musí být „ComplexNumber“ nadtřídou a „RealNumber“ její podtřídou – podobně by u geometrických objektů byla třída „Circle“ podtřídou „Ellipse“, protože každá kružnice je speciálním případem elipsy, kdežto naopak to neplatí. Nejjednodušší implementace čísel by při využití tohoto přístupu mohla vypadat asi nějak takto: class ComplexNumber { private double rvalue,cvalue; double re() { return rvalue; } void setRe(double val) { rvalue=val; } double im() { return cvalue; } void setIm(double val) { cvalue=val; } void multiplyBy(ComplexNumber cn) { double re=re()*cn.re()-im()*cn.im(); setIm(re()*cn.im()+im()*cn.re()); setRe(re); } } class RealNumber extends ComplexNumber { double im() { return 0; } } Ušetřili jsme pár řádků zdrojového kódu, a to je rozhodně dobře: kratší kód jde se správným objektovým návrhem obvykle ruku v ruce. Navíc se nám podařilo zjednodušit i rozhraní – nyní nám stačí jediné násobení, jež funguje stejně dobře se všemi čísly. Dokonce bude platit i to, že je-li nějaký algoritmus napsán pouze pro reálná čísla – tedy ignoruje-li imaginární složku, což, jak víme, by potenciálně vedlo k chybám –, překladač nám nedovolí v něm komplexních čísel vůbec použít (protože tam, kde použijeme deklaraci „RealNumber“, instance nadřízené třídy „ComplexNumber“ stát nemůže). Máme tedy správné řešení?
6. Problémy objektového návrhu
Ukázka elektronické knihy, UID: KOS181112
Bohužel pořád nikoli. Zkušení programátoři hned na první pohled namítnou, že určitě není vůbec žádoucí, aby všechna reálná čísla zbytečně obsahovala nepoužitou imaginární složku (podobně by v geometrii kružnice, implementované jako dědici elips, měly vždy oba středy stejné, resp. obě osy stejně dlouhé): to by bylo poněkud plýtvání. Další problém spočívá v tom, že jakékoli případné další rozšíření – třeba pro podporu n-rozměrných vektorů – by znamenalo kompletně předělat celou hierarchii tříd. Takový vektor by se pak totiž musel stát novou kořenovou třídou, a třída „ComplexNumber“ by byla jeho dědicem, majícím namísto obecného počtu položek vždy právě dvě (a výše zmíněné plýtvání by se – v rozhodně nejčastějším případě práce s obyčejnými reálnými čísly – ještě zhoršilo). Analogií tohoto případu v geometrii je obecná „kuželosečka“; ta by musela být novou nadtřídou „elipsy“.
99 Problémy objektového návrhu
OBJEKTOVÉ PROGRAMOVÁNÍ – NAUČTE SE PRAVIDLA OBJEKTOVÉHO MYŠLENÍ
A nakonec – ani z čistě matematického hlediska nám to nebude fungovat úplně dobře; představme si naprosto triviální funkci, která překlápí komplexní rovinu kolem osy „x=y“, tedy vzájemně prohodí reálnou a imaginární složku čísla: void swap(ComplexNumber cn) { double re=cn.re(); cn.setRe(cn.im()); cn.setIm(re); } Je zřejmé, že dvojí zavolání této funkce by mělo ponechat číslo beze změny; je neméně zřejmé, že pro reálná čísla to platit nebude... Podobně u geometrických objektů – elipsa může mít kupříkladu metodu void stretch(double x,double y); jež ji „roztáhne“ o faktor „x“ v ose X, o faktor „y“ v ose Y. U elipsy samotné to žádný problém není (za předpokladu, že oba středy elipsy jsou vedle sebe nebo nad sebou, což v řadě grafických systémů bývá implicitně splněno); neřešitelný problém ovšem je kterak interpretovat zděděnou metodu v podtřídě „Circle“. Ta buď nesplní to, co splnit měla – neboť kružnice nelze roztáhnout v každé z os jinak, tím jsme ale právě porušili substituční princip, na jehož základě jsme toto řešení sestavili! – nebo kružnici skutečně roztáhne, jenže... ta pak již nebude kružnicí. Tudy, zdá se, také cesta nevede.
6.2.3 Řešení Jednoduché a obecné řešení, vhodné pro všechny podobné případy, bohužel neexistuje; lze ovšem najít řadu částečných řešení, z nichž každé se hodí v trochu jiném kontextu a pro trochu jiné úlohy: • Hodnotová logika s neměnnými objekty: v případě čísel je asi nejpraktičtější učinit instance všech čísel neměnnými. Zrušíme tedy přístupové metody „setRe“ a „setIm“, a také zrušíme všechny operace, jež jakkoli mění obsah příjemce (tj. naše metody „multiply“, „square“, „swap“). Nahradíme je metodami, jež vždy vytvoří a vrátí novou instanci, odlišnou od původního příjemce. Pak samozřejmě není problém, aby např. obyčejné reálné číslo – instance třídy „RealNumber“ – reagovalo korektně na přijetí zprávy „swap“ vrácením nové instance třídy „ComplexNumber“; naopak komplexní číslo, jehož reálná složka je nulová, může po přijetí zprávy „swap“ vytvořit a vrátit instanci třídy „RealNumber“. Zcela ideální pak je tento přístup v kombinaci s mechanismem sdružených
6.2 Je kružnice elipsou, nebo elipsa...
Ukázka elektronické knihy, UID: KOS181112
100
OBJEKTOVÉ PROGRAMOVÁNÍ – NAUČTE SE PRAVIDLA OBJEKTOVÉHO MYŠLENÍ
tříd a skrytých podtříd, kdy navíc zjednoduší programátorské rozhraní: z hlediska programátora žádné „instance tříd RealNumber nebo ComplexNumber“ vůbec nebude třeba brát v úvahu, všechna čísla budou zcela konsistentně instancemi (libovolné skryté podtřídy) jediné sdružené třídy „Number“. U jednoduchých objektů, representujících nepříliš složité hodnoty – jichž jsou právě čísla excelentním příkladem –, bývá toto řešení skutečně optimální; ve složitějších případech (kružnice a elipsa) se však obvykle moc nehodí. Sice by naši obtíž řešilo – neměnnou kružnici nelze roztáhnout v jedné z os, takže výše popsaný problém nehrozí – neustálé vytváření a rušení kopií netriviálních geometrických objektů by ale nebylo příliš praktické. • Ohlášení chyby: tato varianta přinese nejméně práce při sestavení knihoven; zato však nemá už takřka žádnou jinou výhodu, neboť programování nijak neusnadní, a bezpečnost aplikací významným způsobem také nezvýší (problém se totiž projeví až tehdy, když na něj v praxi narazíme). V našem případě by znamenala implementaci „strukturální“ varianty, v níž je „RealNumber“ podtřídou; navíc bychom v ní ale implementovali i metodu „setIm“, jež by ovšem vyvolala výjimku (podobně by u geometrických objektů chybu vyvolala metoda „stretch“ pro instance podtřídy „Circle“ – stejně jako obecně i cokoli dalšího, co pro kružnici nemá dobrý smysl). • Změna třídy: v některých objektových systémech lze po technické stránce implementovat také to, že se instance třídy „RealNumber“ po přijetí zprávy „setIm“ s nenulovou hodnotou automaticky na místě změní na instanci třídy „ComplexNumber“, nebo že se instance geometrické třídy „Circle“ po přijetí zprávy „stretch“ na místě změní v instanci třídy „Ellipse“ (my se s trochu podobnou technikou v Objective C seznámíme v sedmé kapitole, až si budeme ukazovat příklady záchytných objektů). I pokud však je toto řešení realizovatelné – a v mnoha jazycích tomu tak není –, je většinou poměrně nevhodné pro značné risiko chyb, jež může vnášet do celkového kódu: možnost, že do proměnné uložíme instanci nějaké třídy, a ona se nám jaksi sama od sebe „pod rukama“ změní v instanci třídy jiné je obecně riskantní. • Zrušení dědičnosti: můžeme implementovat obě třídy jako vzájemně nezávislé, tak, že ani jedna z nich nebude nadtřídou druhé. Překvapivě často jde o jednoduché a ideální řešení, zvláště je-li doplněno jednoduchou možností vytvořit instanci třídy „složitější“ na základě instance té „jednodušší“ (tedy v našem případě bychom přidali konstruktor, který vytvoří instanci „ComplexNumber“ na základě instance „RealNumber“). U geometrických objektů by nebylo zapotřebí ani to – zde však bývá možná ještě praktičtější řešení následující. • Zrušení „jednodušší“ třídy: v našem příkladu by asi nebylo příliš pohodlné pracovat důsledně vždy jen s komplexními čísly. Naproti tomu grafický systém, který podporuje pouze elipsy a neumožňuje vůbec práci s kružnicemi – jinak, než ve formě elips s oběma osami stejně dlouhými – je velmi rozumnou a prakticky použitelnou variantou. • Kombinace služeb obou tříd do jedné: v podstatě jde takřka o totéž, jako v minulém bodě; navíc ale bude mít programátor k dispozici služby odpovídající té „jednodušší“ z tříd, ale nabízet je budou přímo instance té „složitější“. Pro čísla opět tato varianta nedává příliš dobrý smysl, avšak hodí se velmi dobře pro geometrické objekty: zpráva „circle“ tak může vytvořit elipsu se stejně dlouhými osami, zpráva „radius“ může vyvolat chybu nejsou-li osy stejně dlouhé, apod.
6. Problémy objektového návrhu
Ukázka elektronické knihy, UID: KOS181112