NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 5 - Immutable objects (neměnné objekty)

V minulé lekci, Object pool (fond objektů), jsme si ukázali návrhový vzor Object pool, který zamezí opakovanému vytváření objektů, jejichž konstrukce je příliš složitá nebo trvá dlouhou dobu.

V dnešním tutoriálu si představíme neměnné objekty (immutable objects), u kterých nemůžeme změnit jejich vnitřní stav. Podíváme se na způsob jak správně takové objekty implementovat a vysvětlíme si k čemu nám jsou dobré.

Neměnné objekty

V programech často pracujeme s jednoduchými třídami jako je Rect (obdélník), Point (bod), Line (přímka), Interval a jim podobné. Společnou charakteristikou těchto tříd je, že mají relativně jednoduché rozhraní. Zpravidla pouze několik atributů a využíváme je pouze lokálně. Málokdy potřebujeme, aby se změnila souřadnice obdélníku někde jinde v programu, a proto je vhodné změnu zakázat pro další zjednodušení kódu. V některých případech nemáme ani na vybranou, jelikož neměnnost vyžaduje hardware, operační systém počítače nebo daná technologie. Například při použití vláken je mnohem jednodušší používat immutable objekty, jelikož jsou thread-safe.

Thread-safe znamená, že můžeme kód spouštět na více vláknech. Nemusíme se starat o to, že nám více vláken bude upravovat stejnou proměnnou.

Proč je textový řetězec neměnný?

Problematiku si vysvětleme na textovém řetězci.

Textový řetězec je v počítači reprezentován jako souvislý blok v paměti. Z programátorského hlediska bychom mohli říci, že se jedná o pole znaků. Pole mají určité omezující vlastnosti. Například se automaticky nezvětšují. Když tedy vytvoříme pole, jedná se o prostý blok paměti. Můžeme založit pole s velikostí, kterou dynamicky zadáme až za běhu programu, ale velikost již existujícího pole modifikovat nemůžeme.

Je tedy patrné, že u řetězců, je nutné zakázat jejich modifikaci, jinak by po změně mohl být text různě dlouhý. Je tedy nutné vytvořit nové pole a překopírovat do nového pole hodnoty původního pole. Ve výsledku máme v paměti dva řetězce, kdy se poté jeden uvolní.

Něco jiného je třída StringBuilder, která využívá právě polí nebo spojových seznamů, aby mohla řetězec volně modifikovat. Co je ale důležité, řetězec dostaneme až ve chvíli, kdy zavoláme metodu ToString(). Ta převede seznam na pole znaků, který vrátí.

Hodnotové datové typy

Proč si zde pod neměnnými objekty zmiňujeme hodnotové datové typy, tedy základní datové typy jako je int, double a další? Protože když se zamyslíme, zjistíme, že všechny hodnotové datové typy jsou neměnné. Uložíme-li celé číslo, víme, že pokud jsme mu nepřiřadili jinou hodnotu, bude jeho hodnota stále stejná. A to bez ohledu na to, kolika funkcemi prošel. To u referenčních typů jako objekt nevíme, protože každá z metod může změnit vnitřní stav objektu.

Výhody neměnných objektů

Neměnné objekty nám poskytují několik výhod:

  • Hodnoty se nám nemění pod rukama. Máme jistotu, že to co jsme do proměnné uložili tam bude, akorát na jiném místě v programu.
  • S předchozím bodem souvisí i bezpečnost z hlediska více vláken, protože v žádném z vláken nemůžeme proměnnou změnit, pouze vytvořit její kopii. Máme zajištěno, že nemůže dojít ke kolizi při zapisování několika vlákny. Ve velkých aplikacích si s těmito typy objektů ušetříme spoustu zamykání, zbytečných chyb a vývojového času.

Nevýhody neměnných objektů

Toto paradigma přináší i své nevýhody:

  • Pokaždé, když chceme upravit objekt, musíme vytvořit nový. Čím větší objekt je, tím více výkonu spotřebováváme na vytvoření nové verze objektu.
  • Při častějším vytváření objektů nám narůstá využití paměti.

Implementace vzoru

Implementace je velmi klíčová. Pokud někde necháme skulinku, může nám program změnit proměnnou. Z hlediska návrhu si budeme muset dát pozor na několik věcí.

Konstantní atributy

Všechny atributy, na kterých je třída závislá definujeme jako konstantní, read-only nebo jiným způsobem dle konkrétního programovacího jazyka.

Hluboká kopie objektu

Dáme si pozor na všechny atributy, které jsou referenčními typy (přiřazujeme jim objekty). Vždy musíme udělat hlubokou kopii objektu, aby k ní žádná jiná část programu nemohla přistupovat. Kdybychom to neprovedli, nezměníme sice přímo hodnotu neměnného typu, ale změníme nepřímo jeho vnitřní reprezentaci.

Více informací o hluboké kopii objektu se dozvíme ve zdejších kurzech objektově orientovaného programování u konkrétního jazyka.

Přepsání zděděných metod

Většinou musíme přepsat výchozí metody objektů, jako GetHashCode(), Equals() a další. Uveďme si například metodu GetHashCode(). Její výchozí implementace se rozhoduje na základě umístění v paměti. Jestliže tedy porovnáváme dva objekty za účelem získání jejich hashe, vrátí se nám stejná hodnota pro objekty, které jsou na stejném místě v paměti. Prakticky to znamená, že se jedná o identické objekty, protože porovnáváme objekt sám se sebou. Implementace vzoru se musí rozhodovat na základě hodnot, které neměnný objekt má. Nikoliv na základě umístění v paměti, protože každý objekt se bude nacházet na jiném místě v paměti.

Modifikace stavu objektu

Všechny metody, které nějakým způsobem operují s vnitřním stavem objektu, nesmí měnit hodnoty přímo na objektu. Musí vracet novou instanci se změněnými hodnotami, abychom zajistili neměnitelnost původního objektu. Například pro celé číslo by operace +,-,\,*,/ a % vracely vždy novou instanci. Původní instance by zůstala nezměněna. Všimněme si, že je to přesně chování, které od celočíselných typů očekáváme.

Časté chyby implementace vzoru

Zaměříme se na konstruktor objektu a operátor =.

Konstruktor objektu

Zvláštní pozor si musíme dát na konstruktor a metodu Clone(), neboli jakoukoliv metodu vytvářející kopii. Kopie musí být vždy hluboká. Nejprve se musí vytvořit hluboká kopie původního objektu a teprve poté se mohou přiřadit hodnoty k vlastním atributům.

Operátor =

Operátor = je poněkud kontroverzní. V některých jazycích si jej můžeme sami nadefinovat, v některých to možné není. Cítíme, že operátor = by měl změnit stav objektu, můžeme jej tedy použít s neměnnými objekty? Odpověď je ano, protože operátor = nemění hodnoty původního objektu, ale jen přepíše referenci, aby se odkazoval na jiný objekt.

Příklad použití vzoru

Nyní se již můžeme podívat na implementaci takového neměnného objektu. Naším úkolem bude definovat třídu, která bude předávána referencí tak, aby nebylo možné změnit její vnitřní hodnotu. Bude se tedy chovat jako hodnotový datový typ.

Budeme předpokládat, že neexistuje typ pro komplexní čísla. Komplexní číslo se skládá ze dvou částí, reálného a imaginárního čísla.

UML diagram

UML diagram našeho příkladu vypadá následovně:

Neměnné objekty – diagram - Ostatní návrhové vzory

Implementace UML diagramu

Pojďme si tedy vytvořit třídu komplexního čísla:

  • class KomplexniCislo : ICloneable
    {
        private readonly int RealnaCast;
        private readonly int ImaginarniCast;
    
        public KomplexniCislo()
        {
            RealnaCast = 0;
            ImaginarniCast = 0;
        }
    
        public KomplexniCislo(int realnaCast, int imaginarniCast)
        {
            RealnaCast = realnaCast;
            ImaginarniCast = imaginarniCast;
        }
    
        public KomplexniCislo(KomplexniCislo komplexniCislo)
        {
            RealnaCast = komplexniCislo.GetRealnaCast();
            ImaginarniCast = komplexniCislo.GetImaginarniCast();
        }
    
        public KomplexniCislo Clone()
        {
            return new KomplexniCislo(RealnaCast, ImaginarniCast);
        }
    
        public override bool Equals(object druheCislo)
        {
            if (!(druheCislo is KomplexniCislo))
                return false;
            KomplexniCislo druheKomplexniCislo = (KomplexniCislo)druheCislo;
            return druheKomplexniCislo.GetHashCode() == GetHashCode();
        }
    
        public override int GetHashCode()
        {
            return RealnaCast.GetHashCode() * 73 + ImaginarniCast.GetHashCode();
        }
    
        public int GetRealnaCast()
        {
            return RealnaCast;
        }
    
        public int GetImaginarniCast()
        {
            return ImaginarniCast;
        }
    
        public static bool operator ==(KomplexniCislo prvniCislo, KomplexniCislo druheCislo)
        {
            return prvniCislo.Equals(druheCislo);
        }
    
        public static bool operator !=(KomplexniCislo prvniCislo, KomplexniCislo druheCislo)
        {
            return !prvniCislo.Equals(druheCislo);
        }
    
        public static KomplexniCislo operator +(KomplexniCislo prvniCislo, KomplexniCislo druheCislo)
        {
            return new KomplexniCislo(prvniCislo.GetRealnaCast() + druheCislo.GetRealnaCast(), prvniCislo.GetImaginarniCast() + druheCislo.GetImaginarniCast());
        }
    }
  • public class KomplexniCislo implements Cloneable {
        private final int realnaCast;
        private final int imaginarniCast;
    
        public KomplexniCislo() {
            this.realnaCast = 0;
            this.imaginarniCast = 0;
        }
    
        public KomplexniCislo(int realnaCast, int imaginarniCast) {
            this.realnaCast = realnaCast;
            this.imaginarniCast = imaginarniCast;
        }
    
        public KomplexniCislo(KomplexniCislo komplexniCislo) {
            this.realnaCast = komplexniCislo.getRealnaCast();
            this.imaginarniCast = komplexniCislo.getImaginarniCast();
        }
    
        @Override
        public KomplexniCislo clone() {
            return new KomplexniCislo(this.realnaCast, this.imaginarniCast);
        }
    
        @Override
        public boolean equals(object druheCislo) {
            if (this == druheCislo) {
                 return true;
            }
    
            if (druheCislo == null || getClass() != druheCislo.getClass()) {
                return false;
            }
    
            KomplexniCislo that = (KomplexniCislo)komplexniCislo;
            return this.hashCode() == that.hashCode();
        }
    
        @Override
        public int getHashCode() {
            return Integer.hashCode(realnaCast) * 73 + Integer.hashCode(imaginarniCast);
        }
    
        public int getRealnaCast() {
            return realnaCast;
        }
    
        public int getImaginarniCast() {
            return imaginarniCast;
        }
    
        public static KomplexniCislo add(KomplexniCislo prvniCislo, KomplexniCislo druheCislo) {
            return new KomplexniCislo(prvniCislo.getRealnaCast() + druheCislo.getRealnaCast(), prvniCislo.getImaginarniCast() + druheCislo.getImaginarniCast());
        }
    }
  • class KomplexniCislo
    {
        private int $realnaCast;
        private int $imaginarniCast;
    
        public function __construct(int $realnaCast = 0, int $imaginarniCast = 0)
        {
            $this->realnaCast = $realnaCast;
            $this->imaginarniCast = $imaginarniCast;
        }
    
        public function __construct(KomplexniCislo $komplexniCislo)
        {
            $this->realnaCast = $komplexniCislo->getRealnaCast();
            $this->imaginarniCast = $komplexniCislo->getImaginarniCast();
        }
    
        public function clone(): KomplexniCislo
        {
            return new KomplexniCislo($this->realnaCast, $this->imaginarniCast);
        }
    
        public function equals(object $druheCislo): bool
        {
            if (!($druheCislo instanceof KomplexniCislo)) {
                return false;
            }
    
            return $this->getHashCode() === $druheCislo->getHashCode();
        }
    
        public function getHashCode(): int
        {
            return $this->realnaCast * 73 + $this->imaginarniCast;
        }
    
        public function getRealnaCast(): int
        {
            return $this->realnaCast;
        }
    
        public function getImaginarniCast(): int
        {
            return $this->imaginarniCast;
        }
    
        public static function add(KomplexniCislo $prvniCislo, KomplexniCislo $druheCislo): KomplexniCislo
        {
            return new KomplexniCislo($prvniCislo->getRealnaCast() + $druheCislo->getRealnaCast(), $prvniCislo->getImaginarniCast() + $druheCislo->getImaginarniCast());
        }
    }
  • class KomplexniCislo {
        constructor(realnaCast = 0, imaginarniCast = 0) {
            this.realnaCast = realnaCast;
            this.imaginarniCast = imaginarniCast;
        }
    
        constructor(komplexniCislo) {
            this.realnaCast = komplexniCislo.getRealnaCast();
            this.imaginarniCast = komplexniCislo.getImaginarniCast();
        }
    
        clone() {
            return new KomplexniCislo(this.realnaCast, this.imaginarniCast);
        }
    
        equals(druheCislo) {
            if (!(druheCislo instanceof KomplexniCislo)) {
                return false;
            }
    
            return this.getHashCode() === druheCislo.getHashCode();
        }
    
        getHashCode() {
            return this.realnaCast * 73 + this.imaginarniCast;
        }
    
        getRealnaCast() {
            return this.realnaCast;
        }
    
        getImaginarniCast() {
            return this.imaginarniCast;
        }
    
        static add(prvniCislo, druheCislo) {
            return new KomplexniCislo(prvniCislo.getRealnaCast() + druheCislo.getRealnaCast(), prvniCislo.getImaginarniCast() + druheCislo.getImaginarniCast());
        }
    }
  • class KomplexniCislo:
        def __init__(self, realna_cast=0, imaginarni_cast=0):
            self.realna_cast = realna_cast
            self.imaginarni_cast = imaginarni_cast
    
        def __init__(self, komplexniCislo):
            self.realna_cast = komplexniCislo.get_realna_cast()
            self.imaginarni_cast = komplexniCislo.get_imaginarni_cast()
    
        def __eq__(self, other):
            if not isinstance(other, KomplexniCislo):
                return False
            return self.get_hash_code() == other.get_hash_code()
    
        def clone(self):
            return KomplexniCislo(self.realna_cast, self.imaginarni_cast)
    
        def get_hash_code(self):
            return self.realna_cast * 73 + self.imaginarni_cast
    
        def get_realna_cast(self):
            return self.realna_cast
    
        def get_imaginarni_cast(self):
            return self.imaginarni_cast
    
        def __add__(self, prvniCislo, druheCislo):
            return KomplexniCislo(prvniCislo.get_realna_cast() + druheCislo.get_realna_cast(), prvniCislo.get_imaginarni_cast() + druheCislo.get_imaginarni_cast())

Můžeme si všimnout, že u implementace pro C# dochází k přetěžování jednotlivých operátorů, protože to C# umožňuje. Můžeme si tak nahradit chování jednotlivých operátorů vlastním chováním.

Násobení imaginárního čísla číslem 73 v metodě GetHashCode() se provádí pro snížení počtu kolizí hashe. Číslo 73 je prvočíslo, a tím ještě snižuje šanci na kolizi.

Závěr

Jak bylo zmíněno výše časté vytváření a mazání objektů z paměti může mít neblahý důsledek na výkon programu. Proto většina neměnných typů přichází s jejich proměnnými variantami. Příkladem může být právě třída String a StringBuilder. Ačkoliv je v globálním měřítku práce se třídou StringBuilder pomalejší za časté změny neplatíme žádnou režii. To ale neznamená, že by mohl StringBuilder nahradit String. StringBuilder slouží k vytváření a modifikaci řetězce. Kdykoliv můžeme zavolat metodu ToString() a získat řetězec, se kterým budeme dále pracovat. Většina knihoven potřebuje řetězec ve formátu jak jej ukládá String. V takových situacích nemůžeme do metody předat StringBuilder.

V další lekci, Method chaining a method cascading, si vysvětlíme význam pomocných proměnných ve zdrojových kódech, návrhový vzor Method chaining a také si ukážeme, jak funguje Method cascading.


 

Předchozí článek
Object pool (fond objektů)
Všechny články v sekci
Ostatní návrhové vzory
Přeskočit článek
(nedoporučujeme)
Method chaining a method cascading
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
24 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity