Lekce 9 - Companion objects v Kotlin
V předešlém cvičení, Řešené úlohy k 5.-8. lekci OOP v Kotlin, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
Dnes se v tutoriálu budeme věnovat pojmu statika. Až doposud jsme byli zvyklí, že data (stav) nese instance. Atributy, které jsme definovali, tedy patřily instanci a byly pro každou instanci jedinečné. OOP však umožňuje definovat atributy a metody na samotné třídě. Těmto prvkům říkáme statické (někdy třídní) a jsou nezávislé na instanci.
POZOR! Dnešní lekce vám ukáže statiku, tedy
postupy, které v podstatě narušují objektový model. OOP je obsahuje jen pro
speciální případy a obecně platí, že vše jde napsat bez
statiky. Vždy musíme pečlivě zvážit, zda statiku opravdu
nutně potřebujeme. Obecně bych doporučoval statiku vůbec
nepoužívat, pokud si nejste naprosto jisti, co děláte. Podobně,
jako globální proměnné je statika v objektovém programování něco, co
umožňuje psát špatný kód a porušovat dobré praktiky. Dnes si ji tedy
spíše vysvětlíme, abyste pochopili určité metody a třídy v Kotlinu,
které ji používají. Znalosti použijte s rozvahou, na světe bude méně
zla.
Statické (třídní) atributy
Jako statické můžeme označit různé prvky. Začněme u atributů. Jak jsem se již v úvodu zmínil, statické prvky patří třídě, nikoli instanci. Data v nich uložená tedy můžeme číst bez ohledu na to, zda nějaká instance existuje. V podstatě můžeme říci, že statické atributy jsou společné pro všechny instance třídy, ale není to přesné, protože s instancemi doopravdy vůbec nesouvisí.
Založme si nový projekt (název např. Statika
) a udělejme si
jednoduchou třídu Uzivatel
:
class Uzivatel(private val jmeno: String, private val heslo: String) { private var prihlaseny = false fun prihlasSe(zadaneHeslo: String) { if (zadaneHeslo == heslo) prihlaseny = true else prihlaseny = false } }
Třída je poměrně jednoduchá, reprezentuje uživatele nějakého
systému. Každá instance uživatele má své jméno, heslo a také se o ni
ví, zda je přihlášená či nikoli. Aby se uživatel přihlásil, zavolá se
na něm metoda prihlasSe()
a v jejím parametru se předá heslo,
které člověk za klávesnicí zadal. Metoda ověří, zda se jedná opravdu o
tohoto uživatele a pokusí se ho přihlásit. Vrátí
true
/false
podle toho, zda přihlášení proběhlo
úspěšně. V reálu by se heslo ještě tzv. hashovalo, ale to zde
opomineme.
Když se uživatel registruje, systém mu napíše, jakou minimální délku
musí jeho heslo mít. Toto číslo bychom měli mít někde uložené.
Ve chvíli, kdy uživatele registrujeme, tak ještě nemáme k dispozici
jeho instanci. Objekt není vytvořený a vytvoří se až po
vyplnění formuláře. Nemůžeme tedy v třídě Uzivatel
k
tomuto účelu použít veřejný atribut minimalniDelkaHesla
.
Samozřejmě by bylo velmi přínosné, kdybychom měli údaj o minimální
délce hesla uložený ve třídě Uzivatel
, protože k němu
logicky patří. Údaj uložíme do statického atributu
pomocí bloku companion object:
class Uzivatel(private val jmeno: String, private val heslo: String) { private var prihlaseny: Boolean init { prihlaseny = false } // Zbytek implementace... companion object { val minimalniDelkaHesla = 6 } }
Nyní se přesuňme do Main.kt
a zkusme si atribut vypsat. K
atributu nyní přistoupíme přímo přes třídu:
{KOTLIN_OOP} fun main(args: Array<String>) { println(Uzivatel.minimalniDelkaHesla) } {/KOTLIN_OOP}
{KOTLIN_OOP} class Uzivatel(private val jmeno: String, private val heslo: String) { private var prihlaseny = false fun prihlasSe(zadaneHeslo: String) { if (zadaneHeslo == heslo) prihlaseny = true else prihlaseny = false } companion object { val minimalniDelkaHesla = 6 } } {/KOTLIN_OOP}
Jak již bylo řečeno, Java může používat kotlinovské
třídy. Pokud bychom chtěli z Javy používat statické proměnné
deklarované v Kotlin, museli bychom v Kotlin nejprve každou z nich označit
anotací @JvmStatic
, tedy napsat nad ní tento text. Protože ale
píšeme kód pouze pro Kotlin, tento fakt zde opomineme.
Vidíme, že atribut opravdu náleží třídě. Můžeme se na něj ptát v různých místech programu bez toho, aniž bychom měli uživatele vytvořeného. Naopak na instanci uživatele tento atribut nenalezneme:
// tento kód vyvolá chybu val u = Uzivatel("Tomáš Marný", "heslojeveslo") println(u.minimalniDelkaHesla)
Intellij zahlásí chybu a kód se nezkompiluje.
Číslování instancí
Jako další praktické využití statických atributů se nabízí
číslování uživatelů. Budeme chtít, aby měl každý uživatel
přidělené unikátní identifikační číslo. Bez znalosti statiky bychom si
museli hlídat zvenčí každé vytvoření uživatele a počítat je. My si
však můžeme vytvořit přímo na třídě Uzivatel
privátní
statický atribut dalsiId
, kde bude vždy připraveno číslo pro
dalšího uživatele. První uživatel bude mít id
1, druhý 2 a
tak dále. Uživateli tedy přibude nový atribut id
, který se v
konstruktoru nastaví podle hodnoty dalsiId
. Pojďme si to
vyzkoušet:
class Uzivatel(private val jmeno: String, private val heslo: String) { private var prihlaseny: Boolean val id: Int init { prihlaseny = false id = dalsiId dalsiId++ } // Zbytek implementace... companion object { private var dalsiId = 1 val minimalniDelkaHesla = 6 } }
Třída si sama ukládá, jaké bude id další její instance. Toto id přiřadíme nové instanci v konstruktoru a zvýšíme ho o 1, aby bylo připraveno pro další instanci. Statické však nemusí být jen atributy, možnosti jsou mnohem větší.
Statické metody
Statické metody se volají na třídě. Jedná se zejména o
pomocné metody, které potřebujeme často používat a
nevyplatí se nám tvořit instanci. Kotlin sice umožňuje vytvořit i jen tzv.
funkci, kterou nevoláme ani na instanci, ani na třídě (takto používáme
např. funkci println()
), ale je mnohem výhodnější seskupovat
funkce na třídy, se kterými souvisejí. Tak nám je bude IntelliJ i mnohem
lépe napovídat a budeme si moci pomocí Ctrl + Space
vyvolat seznam toho, co vše můžeme na dané třídě volat.
Ukažme si opět reálný příklad. Při registraci uživatele potřebujeme
znát minimální délku hesla ještě před jeho vytvořením. Bylo by také
dobré, kdybychom mohli před jeho vytvořením i heslo zkontrolovat, zda má
správnou délku, neobsahuje diakritiku, je v něm alespoň jedno číslo a
podobně. Za tímto účelem si vytvoříme pomocnou statickou
metodu zvalidujHeslo()
:
companion object { private var dalsiId = 1 val minimalniDelkaHesla = 6 fun zvalidujHeslo(heslo: String): Boolean { if (heslo.length >= minimalniDelkaHesla) return true else return false } }
Opět si zkusíme, že metodu můžeme na třídě Uzivatel
zavolat:
{KOTLIN_MAIN_BLOCK} println(Uzivatel.zvalidujHeslo("heslojeveslo")) {/KOTLIN_MAIN_BLOCK}
{KOTLIN_OOP} class Uzivatel(private val jmeno: String, private val heslo: String) { private var prihlaseny: Boolean val id: Int init { prihlaseny = false id = dalsiId dalsiId++ } fun prihlasSe(zadaneHeslo: String) { if (zadaneHeslo == heslo) prihlaseny = true else prihlaseny = false } companion object { private var dalsiId = 1 val minimalniDelkaHesla = 6 fun zvalidujHeslo(heslo: String): Boolean { if (heslo.length >= minimalniDelkaHesla) return true else return false } } } {/KOTLIN_OOP}
Pozor! Díky tomu, že metoda
zvalidujHeslo()
náleží třídě, nemůžeme v ní přistupovat k
žádným instančním atributům. Tyto atributy totiž neexistují v
kontextu třídy, ale instance. Ptát se na jmeno
by v naší
metodě nemělo smysl! Můžete si zkusit, že to opravdu nejde.
Stejné funkčnosti při validaci hesla samozřejmě můžeme dosáhnout i
bez znalosti statiky. Vytvořili bychom si nějakou třídu, např.
ValidatorUzivatelu
a do ní napsali tyto metody. Museli bychom
poté vytvořit její instanci, abychom metody mohli volat. Bylo by to trochu
matoucí, protože logika uživatele by byla zbytečně rozdělena do dvou
tříd, když může být za pomoci statiky pohromadě.
U příkladu se statickým atributem minimalniDelkaHesla
jsme
neporušili zapouzdření, protože jsme nastavili atribut jako
val
.
A vyzkoušíme si ještě nakonec naše statické vlastnosti. Metoda
main()
bude vypadat takto:
{KOTLIN_MAIN_BLOCK} val u = Uzivatel("Tomáš Marný", "heslojeveslo") println("ID prvního uživatele: ${u.id}") val v = Uzivatel("Josef Nový", "csfd1fg") println("ID druhého uživatele: ${v.id}") println("Minimální délka hesla uživatele je: ${Uzivatel.minimalniDelkaHesla}") println("""Validnost hesla "heslo" je: ${Uzivatel.zvalidujHeslo("heslo")}""") {/KOTLIN_MAIN_BLOCK}
{KOTLIN_OOP} class Uzivatel(private val jmeno: String, private val heslo: String) { private var prihlaseny: Boolean val id: Int init { prihlaseny = false id = dalsiId dalsiId++ } fun prihlasSe(zadaneHeslo: String) { if (zadaneHeslo == heslo) prihlaseny = true else prihlaseny = false } companion object { private var dalsiId = 1 val minimalniDelkaHesla = 6 fun zvalidujHeslo(heslo: String): Boolean { if (heslo.length >= minimalniDelkaHesla) return true else return false } } } {/KOTLIN_OOP}
A výstup bude:
ID prvního uživatele: 1 ID druhého uživatele: 2 Minimální délka hesla uživatele je: 6 Validnost hesla "heslo" je: false
Privátní konstruktor
Pokud se nám vyskytne třída, která obsahuje jen pomocné
metody nebo nemá smysl od ni tvořit instance (např.
nikdy nebudeme mít 2 konzole), hovoříme o ni někdy jako o statické
třídě/objektu. Kotlin umožňuje přímo označit třídu jako statickou
klíčovým slovíčkem object
. Tento objekt poté nelze
instanciovat (nelze tedy vytvořit instanci takové třídy).
Pojďme si vytvořit testovací objekt A
, který nepůjde
instanciovat. Klíčovým slovem object
nahradíme slovo
class
:
object A { // Zde bychom mohli psát naše metody a atributy }
Pojďme si zkusit objekt A
inicializovat:
val a = A()
Dostaneme vyhubováno, jelikož má instanciaci zakázanou. Objekt má všechny prvky statické a tedy nedává smysl od něj tvořit instanci, ta by nic neobsahovala.
Kotlin podporuje i statický konstruktor, který se zavolá ve chvíli, kdy je třída zaregistrována. Vytváří se stejně jako konstruktor ve třídě, jenom bude v objektu.
object A { init { // Zde bychom měli náš statický konstruktor } // Zde bychom mohli psát naše metody a atributy }
Pokud bychom takový konstruktor chtěli mít i ve standardní třídě a připravit si v něm statické proměnné, je to taktéž možné:
class Trida { companion object { init { // Zde bychom měli náš statický konstruktor } // Zde bychom mohli psát naše metody a atributy } }
Statický registr
Pojďme si takovou jednoduchou statickou třídu vytvořit. Mohlo by se jednat o třídu, která obsahuje jen pomocné metody a atributy. Já jsem se však rozhodl vytvořit tzv. statický registr. Ukážeme si, jak je možné předávat důležitá data mezi třídami, aniž bychom museli mít instanci.
Mějme aplikaci, řekněme nějakou větší a rozsáhlejší, např. diář. Aplikace bude obsahovat přepínání jazyka jejího rozhraní, zvolení používaných záložek, složky k ukládání souborů, barevného schématu a ještě třeba zda ji chceme spouštět při spuštění operačního systému. Bude mít tedy nějaká nastavení, ke kterým se bude přistupovat z různých míst programu. Bez znalosti statiky bychom museli všem objektům (kalendáři, úlohám, poznámkám...) předat v konstruktoru v jakém jazyce pracují, případně jim dodat tímto způsobem další nastavení, jako první den v týdnu (neděle/pondělí) a podobně.
Jednou z možností, jak toto řešit, je použít k uložení těchto nastavení objekt. Bude tedy přístupná ve všech místech programu a to i bez vytvoření instance. Obsahovat bude všechna potřebná nastavení, která si z ní budou objekty libovolně brát. Mohla by vypadat např. nějak takto:
object Nastaveni { val jazyk = "CZ" val barevneSchema = "cervene" val spustitPoStartu = true }
Zkusme si třídu nyní použít, i když program diář nemáme. Vytvoříme
si jen na ukázku třídu Kalendar
a zkusíme si, že v ní máme
opravdu bez problému přístup k nastavení. Vložíme do ni metodu, která
vrátí všechna nastavení:
class Kalendar { fun vratNastaveni(): String { return "Jazyk: ${Nastaveni.jazyk}\n" + "Barevné schéma: ${Nastaveni.barevneSchema}\n" + "Spustit po startu: ${Nastaveni.spustitPoStartu}" } }
Následně vše vypíšeme do konzole:
{KOTLIN_MAIN_BLOCK} val kalendar = Kalendar() println(kalendar.vratNastaveni()) {/KOTLIN_MAIN_BLOCK}
{KOTLIN_OOP} object Nastaveni { val jazyk = "CZ" val barevneSchema = "cervene" val spustitPoStartu = true } {/KOTLIN_OOP}
{KOTLIN_OOP} class Kalendar { fun vratNastaveni(): String { return "Jazyk: ${Nastaveni.jazyk}\n" + "Barevné schéma: ${Nastaveni.barevneSchema}\n" + "Spustit po startu: ${Nastaveni.spustitPoStartu}" } } {/KOTLIN_OOP}
Výstup:
Jazyk: CZ Barevné schéma: cervene Spustit po startu: true
Vidíme, že instance kalendáře má opravdu bez problému přístup ke všem nastavením programu.
Opět pozor, tento kód lze nesprávně použít pro předávání nezapouzdřených dat a používá se jen ve specifických situacích. Většina předávání dat do instance probíhá pomocí parametru v konstruktoru, nikoli přes statiku.
Statika se velmi často vyskytuje v návrhových vzorech, o kterých jsme se zde
již bavili. Jsou to postupy, které dovádí objektově orientované
programování k dokonalosti a o kterých se tu jistě ještě zmíníme. Pro
dnešek je toho však již dost
V následujícím cvičení, Řešené úlohy k 9. lekci OOP v Kotlin, si procvičíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 20x (54.73 kB)
Aplikace je včetně zdrojových kódů v jazyce Kotlin