Immutable objects (neměnné objekty)

Návrhové vzory Immutable objects (neměnné objekty)

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 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 v třída StringBuilder, která využívá právě 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 (C# 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ěci:

  • 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()/hashCo­de() 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

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 mě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.


 

  Aktivity (3)

Článek pro vás napsal patrik.valkovic
Avatar
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu.

Jak se ti líbí článek?
Celkem (6 hlasů) :
4.166674.166674.166674.16667 4.16667


 


Miniatura
Předchozí článek
Factory (tovární metoda)
Miniatura
Všechny články v sekci
Návrhové vzory
Miniatura
Následující článek
Decorator (dekorátor)

 

 

Komentáře

Avatar
Jakub Šárník:

V C# rozhodně jde vytvářet hodnotové typy a pokud vím tak StringBuilder neobsahuje žádný spojový seznam ale pole pokud vím ;-)

 
Odpovědět  +1 24.11.2015 16:56
Avatar
Jan Vargovský
Redaktor
Avatar
Odpovídá na Jakub Šárník
Jan Vargovský:

To je tak, když někdo píše do odborného článku jeho dojmy a né jak to ve skutečnosti je... Tady by ti lidi řekli, že i List je spoják...

 
Odpovědět 24.11.2015 17:07
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 2 zpráv z 2.