1. Programování proti rozhraní Cíl látky Cílem tohoto bloku je seznámení se s jednou z nejdůležitější programátorskou technikou v objektově orientovaném programování. Tou technikou je využívaní rozhraní jako datového typu při deklarování proměnných.
1.1. Úvod Pro správné pochopení zásady, co je to programování proti rozhraní, je nutné nejdříve definovat co je to rozhraní. Definic je celá řada. Jedna z nich rozhraním rozumí souhrn informací, které o sobě daná entita (atribut, metoda, třída) zveřejňuje. Rozhraní tedy obsahuje informace, které mohou kooperující programy využívat, resp. které musí respektovat. Informace deklarované v rozhraní lze rozdělit do dvou kategorií: 1. ty, které, tvoří signaturu 2. to co obsahuje kontrakt
1.1.1. Signatura Signaturu tvoří souhrn informací ve zdrojových kódech, které zpracovává překladač. Nerespektování pravidel signatury se projeví jako syntaktická chyba, kterou je nutné opravit. Podle dané struktury objektově orientovaného programovacího jazyka může být několik úrovní signatury. Typicky jsou známy tyto úrovně: Signatura dat
Do signatury dat se zahrnují především deklarace konstant a proměnných, kdy musí být definováno:
• název proměnné nebo konstanty • a jejich typ Signatura metod
Do signatury metod patří:
• název metody • návratový typ • typy jednotlivých parametrů Signatura tříd a datových typů
Do signatury tříd patří:
• název typu • případně název předka • seznam implementovaných rozhraní • signatury všech členů tj. • atributů • metod • signatury zanořených tříd 1
Takto přísně definovaná signatura dat platí pro jazyky se statickou typovou kontrolou jako jsou například C++, JAVA, C# tj. pro jazyky, které provádějí typovou kontrolu v době překladu. Některé programovací jazyky, jako jsou Lisp, PHP, Python, Ruby nebo Smalltalk, používají tzv. dynamickou typovou kontrolou, která se provádí za běhu programu. U signatury metod stojí za pozornost si uvědomit, že součástí signatury nejsou názvy parametrů. Sice součástí signatury je i návratový typ, ale nelze v jedné třídě deklarovat dvě metody se stejnou signaturou tj. stejným názvem, počtem, typem parametrů a odlišit je pouze návratovým typem. U signatury tříd a datových typů je nutné si všimnout, že do signatury patří všechny členy třídy, včetně signatur vnořených a vnitřních tříd. Zase zde platí, že vše musí být syntaktický správně.
1.1.2. Kontrakt Kontraktem se specifikují informace o rozhraní, které překladač nedokáže zkontrolovat, ale jsou důležité pro pochopení užití daného rozhraní. Do kontraktu patří například: • omezení na rozsah vstupních parametrů metod • některé dodatečné povinné vlastnosti výsledků metod • informace o tom, že při implementaci jedné metody je využitá jiná metoda • upozornění na speciální algoritmus, který by mohl využít případný potomek Protože kontrakt nelze kontrolovat překladačem, měl by každý kdo chce dané rozhraní použít se s kontraktem předem seznámit, aby se vyhnul při jeho porušení problémům. Sice kontrakt nelze kontrolovat překladačem to však neznamená, že by nešel vůbec zkontrolovat. Kontroluje se však až za běhu programu vložením vhodných kontrolních příkazů, které ověřují platnost vstupních a výstupních podmínek. Java pro tento účel zavedla ve verzi 1.4 klíčové slovo assert. Výpis 1.1. Příklad na použití příkazu assert class TestAssert { static double prevracena_hodnota(double x) { assert x != 0.0 : "Argument nesmi byt nulovy"; return 1.0 / x; } public static void main(String args[]) { System.out.println(prevracena_hodnota(1)); System.out.println(prevracena_hodnota(0)); } } Příklad použití příkazu assert jako kontroly nepřípustné hodnoty v parametru. V případě, že se objeví nulová hodnota v parametru x, vystaví 2
se výjimka AssertionError. Text za dvojtečkou se vypíše současně s výjimkou. Exception in thread "main" java.lang.AssertionError: Argument nesmi byt nulovy at javaapplication2.TestAssert.prevracena_hodnota(TestAssert.java:6) at javaapplication2.TestAssert.main(TestAssert.java:11) Vyvolání metody s nepovolenou hodnotou argumentu. Výhoda používaní příkazu assert je v tom, že jeho vykonávání se dá vypínat. To umožňuje, po odladění aplikace, vykonávání kontrol vypnout a tím zrychlit její běh. Příklad jak spustit nebo vynech kontrolu pomocí příkazů assert javac -source 1.4 TestAssert.java java TestAssert java -ea TestAssert Příkaz přeložení programu TestAssert ve verzi Javy 1.4 Spuštění přeloženého programu bez zapnutých kontrol Spuštění programu se zapnutými kontrolami příkazů assert. Kontrole vstupních a výstupních podmínek přímo v kódu se někdy říká, že se programuje defenzivně. Defenzivní programování, vychází z předpokladu, že co je mimo vlastní kód, může tento kód ohrozit. To ohrožení může být neúmyslné, ale bohužel někdy s cílem způsobit škodu nebo získat nějakou neoprávněnou výhodu. Defenzívní programování na Wikipedii [http://cs.wikipedia.org/wiki/Defenzivn %C3%AD_programov%C3%A1n%C3%AD]
1.1.3. Jak dodržovat zásadu Základní myšlenkou zásady je omezit deklarovat proměnné pomocí instančních, konrétních tříd. Lepší je nahradit tyto deklarace pomocí nějakého datového typů, která není vázana na nějakou konkrétní implementaci. Takovým typem je právě interface nebo abstraktní třída.
1.1.4. Návrh vlastního rozhraní Při návrhu rozhraní se má myslet především na budoucího uživatele a ne na to, jak bude toto rozhraní implementováno. Mezi těmeto dšma přístupy je podstatný rozdíl. Rozhraní se má navrhovat tak, aby se budoucímu uživateli s ním dobře pracovalo a tedy nesmí se při jeho návrhu myslet na to, aby se dobře implementovalo programátorovi. Zde je dobré si uvědomit, že se zde se nejedná o uživatelské rozhraní celkové aplikace, ale o aplikační programátorské rozhraní, které je všeobecně známo pod zkratkou API (Application Programming Interface). API používají programátoři, když začleňují cízí části programu do svého programu. Rozhraní specifikované pomocí inteface umožňuje mnohem lépe skrýt implementaci vyvíjeného programu a tím umožnit jeho snadnější vylepšování a upravování. 3
1.1.5. Ukázka rozdílu Na následujícíh dvou diagramech 1.1 a 1.2 je ukázán rozdíl mezi programování proměnných proti instačním konkrétním třídám a proti rozhraní.
Obrázek 1.1. Programování proměnných proti instanci
Obrázek 1.2. Programování proměnných proti rozhraní
Ukázka programování proti instančním třídám Výpis 1.2. Instanční třída Ucet public class Ucet { private int cislo; private long zustatek; public Ucet(int cislo) { this.cislo = cislo; } public int dejCislo() { 4
}
return cislo; } public long dejZustatek() { return zustatek; } public void vlozCastku(long castka) { assert 0 < castka; zustatek += castka; } Výpis 1.3. Třída Banka s instančními proměnnými
public class Banka { private Ucet[] ucty; private int pocet;
}
public Banka() { ucty = new Ucet[100]; pocet = 0; } public long dejZustatek(int cislo) { Ucet u; u = najdiUcet(cislo); if (u != null) { return u.dejZustatek(); } return 0; } public void pridejUcet(Ucet ucet) { if (pocet < ucty.length) { ucty[pocet++] = ucet; } } public void vlozNaUcet(long castka, int cislo) { Ucet u; u = najdiUcet(cislo); if (u != null) { u.vlozCastku(castka); } } private Ucet najdiUcet(int cislo) { Ucet u = null; for (int i = 0; i < pocet; i++) { if (ucty[i].dejCislo() == cislo) { u = ucty[i]; } } return u; }
5
Výpis 1.4. Rozhraní IUcet public interface IUcet { public int dejCislo(); public long dejUrok(); public long dejZustatek(); public void vlozCastku(long castka); } Pro tvorbu jména rozhraní je dobré si zavést konvenci. V tomto případě je před významové jméno „Ucet“ vložen prefix „I“. To výrazně zvyšuje čitelnost zdrojových kódů, kdy je jednoznačně řečeno, že proměnná je datového typy, že není vázaná na žádnou konrétní třídu. Výpis 1.5. Instanční třída BeznyUcet public class BeznyUcet implements IUcet { private int cislo; private long zustatek;
}
public BeznyUcet(int cislo) { this.cislo = cislo; } @Override public int dejCislo() { return cislo; } @Override public long dejUrok() { return 0; } @Override public long dejZustatek() { return zustatek; } @Override public void vlozCastku(long castka) { assert 0 < castka; zustatek += castka; } Jestliže se chce zajistit, aby konkrétní třída měla předepsané rozhraní, musí se to překladači říct uvedením jména rozhraní za klíčové slovo implements. Překladač potom zajistí, zda všechny signatury metod jsou v dané konkrétní třídě implementovány. Metody, které vnutí třídě rozhraní a které nemají v dané třídě smysl, se musí též implementovat. Většinou se voli nějaká neutrální reakce nebo se vyvolá výjimka. Výpis 1.6. Instanční třída SporiciUcet
public class SporiciUcet implements IUcet { 6
private int cislo; private float urok; private long zustatek;
}
public SporiciUcet(int cislo) { this.cislo = cislo; } @Override public int dejCislo() { return cislo; } @Override public long dejUrok() { return (long) (zustatek * urok); } @Override public long dejZustatek() { return zustatek; } @Override public void vlozCastku(long castka) { if (0 < castka) { zustatek += castka; } } Metoda dejUrok() u spořícího učtu již smysl má, takže vrací výpočet úroku ze zůstatku. Tyto metody jsou implementovány stejné jako metody se stejnou signaturou ve třídě BeznyUcet. To znamená, máme duplicitní kód. Takže tyto dvě třídy jsou kandidáti na zevšeobecnění. Výpis 1.7. Využití rozhraní jako datového typu ve třídě Banka,
public class Banka { private IUcet[] ucty; private int pocet; public Banka() { ucty = new IUcet[100]; pocet = 0; } public long dejZustatek(int cislo) { IUcet u; u = najdiUcet(cislo); if (u != null) { return u.dejZustatek(); } return 0; } public void pridejBeznyUcet(IUcet ucet) { 7
if (pocet < ucty.length) { ucty[pocet++] = ucet; }
}
} public void vlozNaUcet(long castka, int cislo) { IUcet u; u = najdiUcet(cislo); if (u != null) { u.vlozCastku(castka); } } private IUcet najdiUcet(int cislo) { IUcet u = null; for (int i = 0; i < pocet; i++) { if (ucty[i].dejCislo() == cislo) { u = ucty[i]; } } return u; } Prvky pole účtů je typu IUcet. To je možné v Java, protože pole je polem referencí. Při vytvoření pole se musí též použít datový typ IUcet, protože to pale ja tak deklarováno. Že to je možné, je zase dáno tím, že pole je polem referencí a ne polem objektů. Datový typ rozhraní IUcet lze zase použít i jako typ parametru metody. To samé, jako v přechozím bodě, platí i pro typ navratové hodnoty při volaní metody.
Důležitá pasáž Třída Banka, z přechozího příkladu, je zcela nezávislá na tom, z jakou konkrétní instanční třídou bude pracovat. Tedy jinak řečeno, tato třída je zcela nezávislá na budoucích implementacích.
1.1.6. Jak na změnu rozhraní Jakmile se jednou zveřejní rozhraní, už by se nemělo měnit. Důvod je jednoduchý, protože se do potíží mohou dostat všichni ostatní programátoři, kteří toto rozhraní vyžívají ve svých kódech. Totiž by musel by přeprogramovat všechny své odladěné a otestované zdrojové kódy. Pokud je nezbytné rozhraní doplnit, je lépe definovat nové rozhraní nebo ho rozšířit pomocí dědičnosti. Po rozšíření nebo změně rozhraní se programátoři můžou rozhodnout, zda tyto rozšíření zapracují do svého kódu nebo ne.
8