Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Serializace dat a objektů - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Serializace dat a objektů

Následující článek je úvodem do další série o generickém programování (šablony), nyní se zaměříme na problém perzistentního ukládání dat nebo jejich transport, obecně o serializaci a deserializaci dat


Co to je serializace a různá řešení

Serializací se většinou označuje vytvoření proud symbolů, které reprezentují vnitřní strukturu nějakých informací (například stavy všech objektů v aplikaci) tak, aby přečtením tohoto proudu bylo možné vnitřní strukturu zpětne zrekonstruovat. Všimněte si, že píši o proudu symbolů, není zde nic o bajtech, bitech, formátech i způsobu nakládání s proudem.

Důvodů, proč potřebujeme serializovat je mnoho. Asi bezpochyby nejznámější užití serializace je ukládání dat do perzistentního úložiště, čili většinou na pevný disk, a deserializací se pak rozumí čtení a rekonstrukce struktury v paměti aplikace. Serializovat je nutné už proto, že data uložená v paměti prostě není možné uložit přímo, pokud aplikace nechce ukládat celý obraz paměť, což není zrovna moc výhodné a generuje to obrovské soubory informačně naprosto prázdné. A to neuvažujeme o variantě selektivního uložení dat a případného transportu takto velkých souborů. Aplikace samozřejmě může interně data organizovat už s ohledem na jejich ukládání, ale ani to není výhodné, protože omezuje aplikaci a může nás stát spoustu výkonu.

Už jsem zmínil další problém, k jehož řešení použijeme serializaci a to transport dat například po síti (ale i transport mezi aplikacemi rourou). V zásadě všechny RPC (Remote Procedure Call) systémy obsahují nějaký systém serializace a deserializace, která bývá ale skryta za rozhraním. Ukážeme si, jak obecnou serializací můžeme navrhnout vlastní RPC protokol, aniž by nás to stálo mnoho času. Naopak instalování některého populárního RPC protokolu nás bude stát paradokce daleko více času a další čas vyplýtváme v okamžiku, kdy se rozhodneme změnit tento protokol (nebo implementovat alternativní)

Lze najít samozřejmě mnoho další způsobů využití serializace, například pri ladění aplikací, pokud potřebujeme vygenerovat přehled o stavu dat v lidsky čitelné formě, pak stačí data pouze serializovat a vhodně popsat. V poslední době je velmi populární formát XML, a můžeme si ukázat, že serializací lze XML dokumenty nejen vytvářet, ale číst a to bez nutnosti integrovat SAX či DOM

Podpora serializace v programovacím jazyce

Serializace je tak silný a zároveň populární nástroj, že jej spoustu programovacích jazyků implementují přímo do syntaxe.

Nikoho však nepřekvapí, že jazyka C++ se to netýká (bohužel nebo bohudík?). Je to zřejmé, jedná se totiž o nástroj vyskytující se na vyšších úrovních programování, a cílem C++ je spíš co největší obecnost a tím i co nejnižší úrověň (low-level). Ukážeme si dále, že tahle nevýhoda má naštěstí spoustu sympatických výhod.

Zůstaneme chvíli ještě v C++, kde je serializace k dispozici pomocí knihoven Boost. Nicměně tento článek nebude o Boostu, ukážeme si, že používání serializace není ani tak o knihovnách, ale o pracovním postupu a myšlení.

V jazyce C# je podpora přímo v jazyce pomocí atributu třídy:

[Serializable] class SerializovatelnaTrida;

C# zařídí, aby se všechny členské proměné třídy uložily. Pomocí rozhraní lze pak vlastní proces serializace upravit
Podobně lze serializovat ve VB

<Serializable()> Class SerializovatelnaTrida;

V jazyce Java lze označit třídu k serializaci vhodnou pomocí rozhraní java.io.Serializable, a vlastní proces serializace zařídí knihovny v JRE.

V PHP najdeme přímo funkce serialize() a unserialize() a podporu řízení serializace pomocí funkcí sleep() a wakeup().

Zájemce o toto téma bych odkázal na Wikipedii téma Serialization. V další části se už vrhneme na to C++.

Označování serializovatelných dat

Nejprve si popišme takový ideál, jak by mohl vypadat zápis serializovatelné třídy ve smyšleném programovacím jazyce (pro přehlednost česky).


serializovatelná třída Zaměstnanec {
řetězec jméno;
řetězec příjmení;
celé_číslo rok_narození;
výkaz vykaz_prace;
celé_číslo stavZpracování;
};

Inspirovali jsme se například jazykem C# a zavedli serializovatelnou třídu. Třída představuje kartu zaměstnance obsahující jméno, příjmení, rok narození, výkaz práce a jakýsi vnitřní stav zpracování karty.

Zatímco jméno, příjmení a rok narození je složeno z jakýchsi dále nedělitelných elementů (řetězec a číslo), výkaz práce bude pravděpodobně nějaká tabulka obsahující řádky a sloupce. Předpokládejme, že výkaz je též serializovatelná třída a v tom případě by serializace měla být bez problémů, protože se v tomto místě třída opět rozpadne na základní elementy.

Do třídy jsem schválně zahrnul proměnnou stavZpracování, která má představovat jakýsi stav, který je důležitý zejména po dobu, kdy je karta zpracovávána, ale má už nulový význam pro uložení do proudu. Přesto zde nemáme možnost proměnnou vyjmout ze serializace. Jediným řešením by bylo ji vyčlenit mimo třídu a uložit ji někam jinam, ale tak, abychom pomocí karty zaměstnance mohli tuto proměnnou vyhledat.

Máme ještě jedno řešení a to obohatit náš jazyk o možnost vybrat, které proměnné serializovat a které ne.

serializovatelná třída Zaměstnanec {
serializovatelný řetězec jméno;
serializovatelný řetězec příjmení;
serializovatelný celé_číslo rok_narození;
serializovatelný výkaz vykaz_prace;
celé_číslo stavZpracování;
};

Záměrně jsem označil proměnné, které se budou serializovat, než abych označoval proměnné, které se nemají serializovat. Jednak není vždy úplně pravdou, že těch serializovatelných bude většina a pak je dobré vědět na první pohled, co je opravdu perzistentní. Pokud proměnná neobsahuje klíčové slovo serializovatelný nebude prostě serializována. Pokud bychom použili opačný přísup, první pohled by nestačil.

Označování v C++

Máme hezká klíčová slova, ale do syntaxe C++ je těžko dostaneme. Je potřeba jít na to jinak. Jako první se nabízí použít javovský způsob a každou serializovatelnou třídu udělat potomkem nějakého rozhraní. Např:

class Zamestnanec: public ISerializable {...}

Jenže Java další postup serializace řeší pomocí knihoven JRE, které mají mimojiné přístup k definici třídy v run-time, takže zvládne zařídit serializaci bez další práce navíc. V C++ nic takového neexistuje, budeme si muset nějak pomoci.

Když už tu máme interface, můžeme do něho přidat funkci. Řekněme, že to bude funkce obsahující předpis, jak se třída Zamestnanec serializuje. V předpisu označíme proměnné, které se mají serializovat. Předpis se pak provede jako funkce, což je v tuto chvíli pouze implementační detail.

class ISerializable {
public:
virtual void Serialize() = 0;
};

Tomu už Céčkař začíná rozumět. Jak by mohla vypadat implementace této funkce?

class Zamestnanec: public ISerializable {
std::string jmeno;
std::string prijmeni;
int rok_narození;
Vykaz vykaz_prace;
int stavZpracování;
public:
virtual void serialize() {
doSerialize(jmeno);
doSerialize(prijmeni);
doSerialize(rok_narozeni);
doSerialize(vykaz_prace);
}
};

Používání globálních funkcí nevypadá moc objektově a pokud si představíme, že funkce se nakonec provádí, tedy nejde o předpis, je potřeba si teď rychle představit, jak taková serializace vlastně bude probíhat. Určitě tam bude nějaký stream, nějaký kontext, který bude uložen v nějakém objektu... No říkejme mu tedy Archive. A abychom nebyli vázani na jednu konkrétní implementaci, nejspíš bude nutné použít rozhraní: IArchive. Funkce serialize by tedy měla spíš vypadat takto:

  virtual void serialize(IArchive &a) {
a.doSerialize(jmeno);
a.doSerialize(prijmeni);
a.doSerialize(rok_narozeni);
a.doSerialize(vykaz_prace);
}

Proč bude vadit použití virtual?

Jak jste si mohli všimnout, vše je to postavené na rozhraní ISerializable, které přes klíčové slovo virtual umožňuje implementovat předpis. Pokud zavoláme serialize na jakoukoliv třídu, od které víme jen to, že "umí" toto rozhraní, budeme umět instanci této třídy serializova. Ukažme si, že to má ale své, celkem velmi omezující, nevýhody.

  • Je potřeba mít společné rozhraní, které musíme dávat všude tam, kde chceme umožnit serializaci, tedy i do všech knihoven a můžeme se zamotat do pekla závislostí (dependency hell)
  • Použití pozdní vazby nám může znesnadnit práci při rozšiřování sady základních elementů v programu. To co umožní IArchive, to je jednou dané a basta.
  • Pozdní vazba take znesnadňuje optimalizaci serializace. Pokud bychom použili statickou vzabu, překladač by mohl volání inlinovat.
  • Nutnost dědit ISerializable komplikuje implementaci serializace u typů, které v základu rozhraní nedědí a dědit nemohou. Musíme je zdědit a používat zděděné třídy, ovšem běda, pokud máme závislé knihovny, které takto zděděné třidy neumí používat.

Z toho důvodu je lepší jít na to jinak, ideálně pomocí šablon.

Řešení pomocí šablon

Pokud použijeme šablony, zbavíme se ISerializable. Šablony totiž nepotřebují rozhraní, stačí jen, když třída použitá v šabloně umí to co šablona vyžaduje. To co naopak budeme definovat je cosi, čemu se říká koncept (Concept). V nové revizi C++ (označovaný jako C++0x) už najdeme concept jako součást jazyka. V současné verzi C++ však musí stačit, že dodržování konceptu vynutíme například v dokumentaci, nebo pravidly v programátorském týmu. Rozhodně žádný koncept není třeba psát do programu, ani jej sdílet mezi knihovnami.

Tím konceptem budou následující pravidla

  • Serializovatelná třída musí definovat šablonu funkce
template<typename Archive> void serialize(Archive &);
  • Serializovatelná členská proměnná se serializuje pomocí operátoru závorek ()

Příklad:

class Zamestnanec {
std::string jmeno;
std::string prijmeni;
int rok_narození;
Vykaz vykaz_prace;
int stavZpracování;
public:
template<typename Archive> void serialize(Archive &a) {
a(jmeno);
a(prijmeni);
a(rok_narozeni);
a(vykaz_prace);
}
};

Použití šablony umožňuje použít serializovatelnou třídu i tam, kde nemáme k dispozici serializační knihovny. Výše uvedený příklad totiž půjde přeložit vždy, protože překladač nebude instanciovat šablonu, doku mu neřekneme, jaký "archiv" bude serializaci provádet.

Použití operátoru() má dva důvody. První je výhoda nevynucovat si další pravidlo v konceptu (bude jich ještě dost, uvidíme) a předem definovat názvy, které se mají používat. Samotné uvedení proměnné do závorek dost dobře označuje, že tato proměnná se bude serializovat. Druhým důvodem je unifikace, když si uvědomíme, že jsme nevytvořili nic jiného než funkci "forEachMemberVariable". Jinými slovy, můžeme použít jakýkoliv funktor, abychom získali seznam členských proměnných (pochopitelně jen ty, co jsou serializovatelné).

Rozšíření: identifikace proměnné

To že umíme procházet seznam všech členských proměnných ještě nic neznamená. Funktor, který bude takto proměnné číst nebude mít žádné vodítko, jak proměnné identifikovat. Pokud budeme serializovat do nějaké strukturované databáze, bylo by dobré umět každý záznam zpětně dohledat, například při deserializaci. V současné době umíme proměnnou identifikovat pouze pořadím v proudu (což je někdy dostačující, někdy však nikoliv).

Identifikaci zapíšeme jako druhý parametr operátoru závorek

a(<proměnná>,<identifikace>)

Můžeme na identifikaci použít libovolný typ. Bylo by dobré se ale na nějakém typu domluvit, protože není nic komplikovanějšího, než když každá třída pro účely identifikace proměnné používá ve funkci serialize jiný typ. Případny serializátor pak musíme naučit všechny tyto typy. Po mnoha testech se mi osvěčilo použití obyčejného řetězce (const char *). V situacích, kdy je třeba pracovat v UNICODE pak je možné použít wchar_t nebo zapisovat identifikaci v UTF-8. Druhé řešení je sympatické v tom, že je kompatibilní tam, kde wchar_t nepodporují.

Příklad našeho zaměstnance:

template<typename Archive> void serialize(Archive &a) {
a(jmeno, "jmeno");
a(prijmeni, "prijmeni");
a(rok_narozeni, "rok_narozeni");
a(vykaz_prace, "vykaz_prace");
}

Názvy píšeme pokud možno podle skutečnosti. Pokud se v názvu vyskytuje znak, který se pak ve výsledném formátě nesmí vyskytovat, musí toto vyřešit serializační třída. Ve funkci serialize totiž nemáme představu o tom, kdo bude následně serializaci provádět.

Všimněte si skrytého vytváření struktury. Při serializaci výkazu práce identifikujeme proměnnou, která se však dále rozloží na nějaké elementy, které také budou identifikované. Lze tak dobře prezentovat serializovaný objekt ve stromové reprezentaci, a nebo... představte si zápis do XML!

<zamestnanec>
<jmeno>Ondřej</jmeno>
<prijmeni>Novák</prijmeni>
<rok_narozeni>1976</rok_narozeni>
<vykaz_prace>
<...> </...>
</vykaz_prace>
</zamestnanec>

nebo:

<zamestnanec jmeno="Ondřej" prijmeni="Novák" rok_narozeni="1976">
<vykaz_prace>
<...> </...>
</vykaz_prace>
</zamestnanec>
Poznámka:
Tento koncept vyžaduje, aby operátor() pro toto rozšíření vracel bool, kde true znamená, že operace se zdařila, a false, že proměnnou nelze identifikovat (neexistuje, nebo ji nelze serializovat, třeba proto, že je zakázána)

Rozšíření: volitelné proměnné a výchozí hodnota

Ve výčtu rozšíření se zmíním ještě o volitelné proměnné. Tuto vlastnost nabízí C# - umožňuje určit, které proměnné jsou volitelné. Abychom mohly zavést volitelnou proměnnou, musíme nějakým způsobem učit, jakou hodnotu proměnná získa, pokud serializace selže (uplatní se u deserializace, ale může se uplatnit i u serializace, pokud třeba vyžadujeme, aby proměnné, které mají výchozí hodnoty se přeskočily).

Zápis:

a(<proměnná>,<identifikace>,<výchozí hodnota>)

Je zřejmé, že výchozí hodnota má smysl pouze tehdy, když je známa identifikace. Bez identifikace totiž nepoznáme, zda v proudu nějaká proměnná chybí.

Trošku nelogický ale názorný příklad u zaměstnance, pokud budeme tvořit karty zaměstnanců, kteří se narodili kolem roku 1976.

a(rok_narozeni, "rok_narozeni", 1976);
Poznámka:
Tento koncept vyžaduje, aby operátor() pro toto rozšíření vracel bool, kde true znamená, že operace se zdařila a proměnná se serializovala, a false, pokud se uplatní výchozí hodnota bez serializování.

Rozšíření: serializace tříd které neimplementují serialize()

Stále jsme nevyřešili případ tříd, které neimplementují serialize() a přesto je třeba serializovat.

Díky šablonám můžeme serializaci zajistit například specializací nějaké šablony. ''Tento koncept tedy předepisuje, že serializace takové třídy se bude dělat specializací šablony Serializable.

Obecná šabloná má tvar:

template<typename T>
class Serializable;

O obsahu obecné šablony si povíme něco příště (prozradím, že bude obsahovat volání metody serialize()).

Třída zastupuje původní třídu tak, že implementuje metodu serialize, tentokrát však ve statické verzi a se dvěma parametry. Uvedeme si příklad serializace pro std::pair

template<typename T1, typename T2>
class Serializable< std::pair < T1, T2 > >
{
public:

template<typename Archive>
static void serialize(std::pair<T1,T2> &o, Archive &a)
{
a(o.first,"first");
a(o.second,"second");
}
};

Podmínkou, aby toto bylo možné je, že příslušná třída umožňuje přístup ke svému vnitřnímu stavu, protože v tomhle řešení není možné serializovat proměnné typu protected: a private: (chráněné proměnné - protected: - lze případně serializovat pomocí podědění a přetypování, ale soukromé - private: - nelze nijak!)

Použití šablonové serializace v týmové práci

Představte si, že vám senior programátor zadal úkol napsat nějakou třídu, která něco zajišťuje a jejiž vnitřní stav lze uložit do streamu. Bez dalších znalostí by každý programátor toto implementoval tak, že by zavedl funkce load a save (případně její ekvivalenty) a uložení provedl do iostreamu.

To je příliš mnoho zbytečné práce a navíc neefektivní.

Mnohem jednodušší je, pokud tým má už nějaké zkušenosti se serializacemi a má k dispozici minimálně jeden serializátor napsaný. Pak stačí, aby tento programátor pouze do třídy vložil metodu serialize() a určil, které proměnné se budou serializovat.

A tím je úkol vyřešen, rychle a efektivně!

Co je vlastně výsledkem serializace

Jak už bylo mnohokrát zmíněno, o serializaci se stará serializátor, který si rozebereme v některé příští kapitole. V tuto chvíli, pokud předpokládáme, že serializátor se chová jako funktor je výsledkem serializace série volání operátoru() pokaždé s jiným parametrem, tak jak serializáce probíhá všemy členskými proměnnými. To vyžaduje aby operátor byl šablona, ale to není žádný problém. Nicméně díky tomu, že je to šablona, získáváme kompletní informaci o serializované proměnné, zejména:

  • její adresu (referenci)
  • její typ (T v šabloně)
  • její jméno (pokud je uvedeno)
  • výchozí hodnotu (pokud je uvedena).

Všimněte si, že mluvím o serializaci, ale úplně stejný předpis lze použít i na deserializaci. Konečně, nikdo nespecifikoval, jestli se proměnná předaná do funktoru má číst, nebo se do ni bude něco zapisovat.

Samozřejmě, že si časem ukážeme, že tenhle obecný přístup má občas nějaké výjimky, ale i ty se naučíme postihovat.

Závěr

V další části si představíme blokové schéma obecného serializátoru a koncept, co by měl každý serializátor umět. Také možná rozšíříme koncept serializace o další rozšíření, které by bylo dobré, aby serializátor ve vztahu k serializovaným objektům podporoval.

Serializace dat a objektů II - Návrh serializátoru

vytvořeno: 22.5.2008 16:13:49, změněno: 15.9.2008 15:36:26
Jsou informace v článku pro Vás užitečné?
  • (15)
  • (1)
  • (2)
  • (0)
  • (0)
Nick nebo OpenID
Vzkaz
 
25.7.2016 21:15:02

6ozUTFlaImir

Action requires knodlewge, and now I can act!
5.9.2015 11:59:54

converse

converse
15.8.2015 07:47:34

converse tw

converse
6.8.2015 16:23:45

new balance
17.7.2015 17:48:52

new balance

new balance
26.6.2015 21:32:37

toms shoes sale

http://www.seofilter.org/
toms shoes sale http://tomssg.mennosource.org/

Podobné články

Serializace dat a objektů II - Návrh serializátoru

Po teoretickém úvodu o serializaci se podíváme (ještě stále teoreticky) na vlastní serializátor. Zatím si uděláme takovou jednoduchou analýzu

Serializace dat a objektů IV - Transport a struktura

Dnešní díl o serializaci se pozastaví nad nejvhodnější implementací transportního modulu a pak se vrhneme na strukturu dat.

Serializace dat a objektů III - Volba formátu

V našem seriálu o serializaci si tentokrát ukážeme, jakým způsobem budeme volit formát výsledného streamu. Nepůjde o konkrétní formáty, ale o způsoby, jak různé formáty implementovat

Úvod do obecného OOP 2. díl

V tomto díle prozkoumáme život objektů

Automatické klonování objektů v C++

C++ nenabízí standardní prostředky jako klonovat objekty. Pouze kopírovací kontruktor, který nám však v případě polymorfních objektů moc nepomůže. Naše "lenost" nám však pomůže nalézt řešení.
Reklama: