IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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 4 - 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 a zbytečnému vytváření objektů, jejichž konstrukce je příliš složitá nebo trvá dlouhou dobu.

U neměnných objektů nemůžeme změnit jejich vnitřní stav. V C# .NET/Javě je to například třída String, která po každé operaci vrací nový objekt typu String. Podíváme se na způsob jak správně takové objekty implementovat a vysvětlíme si k čemu nám jsou dobré.

Motivace

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 přímo 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ř. při použití vláken je mnohem jednodušší používat immutable objekty, jelikož jsou thread-safe).

Proč je String neměnný?

Problematiku si vysvětleme na třídě String. Pro ty, co programují v C++ je odpověď zřejmá. Pro ostatní zde přináším menší objasnění.

Textový řetězec je v počítači reprezentován jako souvislý blok paměti. Z programátorského hlediska bychom mohli říci, že se jedná o pole znaků. Na rozdíl od polí ve vyšších programovacích jazycích (např. PHP) mají pole v C/C++ určité omezující vlastnosti, například se automaticky nezvětšují. Když tedy v C vytvoříme pole, jedná se o prostý blok paměti. Pokud chceme pole zvětšit, musíme požádat o další (větší) místo někde jinde v paměti, původní hodnoty do něj zkopírovat a poté původní (menší) pole uvolnit. Pole nelze jednoduše zvětšit z toho důvodu, že za ním v paměti zkrátka nemusí být volno a nedalo by se prodloužit.

Ačkoliv můžeme v programu (i v C) použít seznamy, které se automaticky rozšiřují (tzv. spojové seznamy), operační systém si s nimi neporadí. Proto např. před vypsáním textu do konzole operačního systému (které chceme pravděpodobně provádět poměrně často) musíme vytvořit klasické pole a zkopírovat do něj náš text. Aby byl jazyk efektivní, měla by třída String ukládat řetězce tak, jak je potřebuje operační systém.

Je tedy patrné, že u řetězců (které String reprezentuje) je nutné zakázat jejich modifikaci, protože by po změně mohl být text jinak dlouhý a to implementace neumožňuje. Je tedy nutné vytvoření nového pole a překopírováním pole původního, ve výsledku máme v paměti dva řetězce, kdy se jeden poté uvolní.

Pozn.: 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 nedělá nic jiného, než že převede seznam na pole znaků, který vrátí.

Hodnotové datové typy

Proč zmiňuji hodnotové datové typy, tedy základní datový typy jako je int, double a další? Protože když se na chvilku 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ů (objektů) nevíme, protože každá z metod může změnit vnitřní stav objektu.

Ve vyšších jazycích (PHP nebo Java) můžeme však obvykle vytvářet jen objektové typy. To je další z věcí, kde se liší C/C++ od vyšších programovacích jazyků. V nižších programovacích jazycích si můžeme říci, jakým způsobem chceme proměnou (nebo objekt) předat. Jestli funkce dostane referenci nebo kopii samotného objektu si specifikuje sám autor metody. Ve vyšších programovacích jazycích jsou objekty vždy předávány jako reference.

Naším úkolem nyní bude definovat třídu, která bude předávána referencí tak, aby nebylo možné její vnitřní hodnotu změnit a svým způsobem se chovala jako hodnotový datový typ.

Implementace

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

  • Všechny atributy, na kterých je třída závislá, definujeme jako konstantní, popřípadě read-only nebo jiným způsobem (záleží na jazyku), aby nám kompilátor kontroloval, jestli se hodnota mění.
  • 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 naleznete ve zdejších kurzech základů OOP u konkrétního jazyka, pod pojmem "klonování".
  • Většinou musíme přepsat další, výchozí metody objektů. Např. v C# .NET jsou to metody GetHashCode() a Equals() a v Javě hashCode() a equals(). Výchozí implementace metody GetHashCode()/hashCode() se rozhoduje na základě umístění v paměti. Tedy jestliže porovnáváme dva objekty (nebo chceme získat jejich hash) vrátí se stejná hodnota pro objekty, které jsou ve stejné paměti (prakticky to znamená, že se jedná o identické objekty – porovnáváme objekt sám se sebou). Naše implementace se musí rozhodovat na základě hodnot, které neměnný objekt má, a nikoliv na základě umístění v paměti.
  • Všechny funkce, které nějakým způsobem operují s vnitřním stavem objektu, nesmí měnit hodnoty objektu přímo, ale musí vracet novou instanci se změněnými hodnotami. Například pro celé číslo by operace +,-,\,*,/,% 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

  • Zvláštní pozor si musíme dát na kopírovací konstruktor a metodu Clone() (či jakoukoliv metodu vytvářející kopii). Kopie musí být vždy hluboká. Na obdobném principu funguje kopírovací konstruktor, které si musí vytvořit kopii původního objektu a teprve poté může hodnoty přiřadit k vlastním atributům.
  • Operátor = je poněkud kontroverzní. V některých jazycích si jej můžeme sami nadefinovat, v některých to není možné (vyšší jazyky). Asi 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.

Nyní se již můžeme podívat na implementaci takového neměnného objektu v C# .NET. Budeme předpokládat, že neexistuje typ pro komplexní čísla. My si takový typ vytvoříme:

class KomplexniCislo : ICloneable
{
    private readonly int RealnaCast;
    private readonly int ImaginarniCast;

    public KomplexniCislo(int RealnaCast, int ImaginarniCast)
    {
        this.RealnaCast = RealnaCast;
        this.ImaginarniCast = ImaginarniCast;
    }

    public KomplexniCislo()
    {
        this.RealnaCast = 0;
        this.ImaginarniCast = 0;
    }

    public KomplexniCislo(KomplexniCislo Copy)
    {
        this.RealnaCast = Copy.RealnaCast;
        this.ImaginarniCast = Copy.ImaginarniCast;
    }

    public object 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() == this.GetHashCode();
    }

    public override int GetHashCode()
    {
        return RealnaCast.GetHashCode() * 73 + ImaginarniCast.GetHashCode();
    }

    public static bool operator ==(KomplexniCislo prvni, KomplexniCislo druhe)
    {
        return prvni.Equals(druhe);
    }

    public static bool operator !=(KomplexniCislo prvni, KomplexniCislo druhe)
    {
        return !prvni.Equals(druhe);
    }

    public static KomplexniCislo operator +(KomplexniCislo prvni, KomplexniCislo druhe)
    {
        return new KomplexniCislo(prvni.RealnaCast+druhe.RealnaCast,prvni.ImaginarniCast+druhe.ImaginarniCast);
    }

}

Neměnné objekty – diagram - Ostatní návrhové vzory Pozn.: Šipka nahoru má být přerušovaná a namísto Extends zase implements.

Výhody

Neměnné objekty nám poskytují několik výhod, kterých můžeme využít.

  • Hodnoty se nám nemění pod rukama. Máme jistotu, že co jsme do proměnné uložili tam bude na libovolném 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é z vláken nemůže 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.

Změna vnitřního stavu

Č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říklad může být právě třída String a StringBuilder. Ačkoliv je v globálním měřítku práce se StringBuilderem pomalejší než práce se samotným Stringem, za časté změny neplatíme žádnou režii. To ale neznamená, že by mohl StringBuilder nahradit String. StringBuilder (jak již název napovídá) 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. A jak již bylo řečeno na začátku, 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í:
13 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity