Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
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 16 - Memento

V minulé lekci, State, jsme si ukázali návrhový vzor State, který umožňuje objektu razantně změnit své chování, které je závislé na stavu tohoto objektu. Nyní víme, že nahrazuje složité větvení uvnitř objektu.

V dnešním tutoriálu Návrhové vzory GoF si představíme návrhový vzor Memento (památka), který řeší uložení vnitřního stavu objektu, aniž poruší princip zapouzdření.

Motivace použití vzoru Memento

V našich aplikacích můžeme na některých místech vyžadovat možnost vrátit se k předchozímu stavu. Například získat data z formuláře, který uživatel vyplnil a po jehož odeslání došlo k výpadku internetového připojení. Můžeme také potřebovat zavést funkce zpět/vpřed, které se hodí ať programujeme kalkulačku nebo například nějaký editor. Jelikož vnitřní stav je uvnitř objektu zapouzdřen, musí uložení dat objektu provést objekt sám. K vnitřnímu stavu se lze poté vrátit.

Vzor Memento odděluje svou funkcionalitu do samostatného objektu. To je princip, o který se většina vzorů snaží. Původní třída uchovávající stav, tak zůstane nezanesená touto logikou a bude lépe udržovatelná.

V základní verzi tohoto vzoru se nejedná o princip historie (zpět a vpřed), ale opravdu o jeden stav, který se uloží a objekt se do něj poté dokáže vrátit. Princip historie lze nicméně doimplementovat, a to i inkrementálně. Například, aby se ukládaly pouze změny oproti původním datům. Vzor nijak neřeší implementaci ukládání stavu, pro niž můžeme použít například serializaci.

Definice vzoru Memento

Vzor obsahuje následující třídy:

  • Originator - Třída, jejíž stav ukládáme. Umožňuje svůj stav načíst z mementa nebo jej uložit do nového mementa a vrátit.
  • Memento - Reprezentace vnitřního stavu třídy Originator. Pouze objekt držící stav bez další logiky.
  • Caretaker - Třída ukládá/načítá mementa z/do originatoru. Jedná se o manažer stavů.

Podívejme se na UML diagram:

Návrhový vzor Memento z GOF - Návrhové vzory GoF

Příklad implementace vzoru Memento

Představme si, že chceme uchovávat historii výpočtů kalkulačky. Pro zjednodušení ukládejme pouze celé zadané příklady jako textové řetězce. Pro reprezentaci historie využijeme datovou strukturu zásobník (Více o této datové struktuře v lekci Fronta a zásobník v C# .NET). Memento můžeme naprogramovat i generický, abychom jej mohli využívat i pro další třídy. A nemuseli psát zbytečně nové třídy.

V praxi bychom mohli ukládat objekty s libovolným počtem libovolně složitých vlastností.

Implementace tříd

Pusťme se tedy na implementaci jednotlivých tříd 😀

Třída Memento

Začneme třídou Memento:

  • public class Memento<T>
    {
        private T data;
    
        public Memento(T data)
        {
            this.data = data;
        }
    
        public T GetData()
        {
            return data;
        }
    }
  • public class Memento<T> {
        private T data;
    
        public Memento(T data) {
            this.data = data;
        }
    
        public T getData() {
            return data;
        }
    }
  • class Memento {
        private $data;
    
        public function __construct($data) {
            $this->data = $data;
        }
    
        public function getData() {
            return $this->data;
        }
    }
  • class Memento {
        constructor(data) {
            this.data = data;
        }
    
        getData() {
            return this.data;
        }
    }
  • class Memento:
        def __init__(self, data):
            self.data = data
    
        def get_data(self):
            return self.data

Třída Originator

Třídu Originator z UML diagramu u nás zastupuje třída Kalkulacka:

  • public class Kalkulacka<T>
    {
        private T priklad;
    
        public Memento<T> Uloz()
        {
            return new Memento<T>(priklad);
        }
    
        public void Nacti(Memento<T> memento)
        {
            priklad = memento.GetData();
        }
    
        public void SetPriklad(T priklad)
        {
            this.priklad = priklad;
        }
    
        public T GetPriklad()
        {
            return priklad;
        }
    
        // Další metody kalkulačky …
    }
  • public class Kalkulacka<T> {
        private T priklad;
    
        public Memento<T> uloz() {
            return new Memento(priklad);
        }
    
        public void nacti(Memento<T> memento) {
            priklad = memento.getData();
        }
    
        public void setPriklad(T priklad) {
            this.priklad = priklad;
        }
    
        public T getPriklad() {
            return priklad;
        }
    
        // Další metody kalkulačky …
    }
  • class Kalkulacka
    {
        private $priklad;
    
        public function uloz()
        {
            return new Memento($this->priklad);
        }
    
        public function nacti(Memento $memento)
        {
            $this->priklad = $memento->getData();
        }
    
        public function setPriklad($priklad)
        {
            $this->priklad = $priklad;
        }
    
        public function getPriklad()
        {
            return $this->priklad;
        }
    
        // Další metody kalkulačky …
    }
  • class Kalkulacka {
        constructor() {
            this.priklad = null;
        }
    
        uloz() {
            return new Memento(this.priklad);
        }
    
        nacti(memento) {
            this.priklad = memento.getData();
        }
    
        setPriklad(priklad) {
            this.priklad = priklad;
        }
    
        getPriklad() {
            return this.priklad;
        }
    
        // Další metody kalkulačky …
    }
  • class Kalkulacka:
        def __init__(self):
            self.priklad = None
    
        def uloz(self):
            return Memento(self.priklad)
    
        def nacti(self, memento):
            self.priklad = memento.get_data()
    
        def set_priklad(self, priklad):
            self.priklad = priklad
    
        def get_priklad(self):
            return self.priklad
    
        # Další metody kalkulačky …

Třída Caretaker

A nakonec si napišme třídu Caretaker:

  • public class Caretaker<T>
    {
        private Kalkulacka<T> kalkulacka;
        private Stack<Memento<T>> historie = new Stack<Memento<T>>();
    
        public Caretaker(Kalkulacka<T> kalkulacka)
        {
            this.kalkulacka = kalkulacka;
        }
    
        public void Uloz()
        {
            historie.Push(kalkulacka.Uloz());
        }
    
        public void Zpet()
        {
            kalkulacka.Nacti(historie.Pop());
        }
    }
  • public class Caretaker<T> {
        private Kalkulacka<T> kalkulacka;
        private Stack<Memento<T>> historie = new Stack<Memento<T>>();
    
        public Caretaker(Kalkulacka kalkulacka) {
            this.kalkulacka = kalkulacka;
        }
    
        public void uloz() {
            historie.push(kalkulacka.uloz());
        }
    
        public void zpet() {
            kalkulacka.nacti(historie.pop());
        }
    }
  • class Caretaker {
        private $kalkulacka;
        private $historie = [];
    
        public function __construct($kalkulacka) {
            $this->kalkulacka = $kalkulacka;
        }
    
        public function uloz() {
            array_push($this->historie, $this->kalkulacka->uloz());
        }
    
        public function zpet() {
            $this->kalkulacka->nacti(array_pop($this->historie));
        }
    }
  • class Caretaker {
        constructor(kalkulacka) {
            this.kalkulacka = kalkulacka;
            this.historie = [];
        }
    
        uloz() {
            this.historie.push(this.kalkulacka.uloz());
        }
    
        zpet() {
            this.kalkulacka.nacti(this.historie.pop());
        }
    }
  • class Caretaker:
        def __init__(self, kalkulacka):
            self.kalkulacka = kalkulacka
            self.historie = []
    
        def uloz(self):
            self.historie.append(self.kalkulacka.uloz())
    
        def zpet(self):
            self.kalkulacka.nacti(self.historie.pop())

Použití tříd

Použití naimplementovaných tříd by bylo následující:

  • Kalkulacka<string> kalkulacka = new Kalkulacka<string>();
    Caretaker<string> historie = new Caretaker<string>(kalkulacka);
    kalkulacka.SetPriklad("1 + 1");
    Console.WriteLine(kalkulacka.GetPriklad());
    historie.Uloz();
    kalkulacka.SetPriklad("2 * 3");
    Console.WriteLine(kalkulacka.GetPriklad());
    historie.Zpet();
    Console.WriteLine(kalkulacka.GetPriklad());
  • Kalkulacka kalkulacka = new Kalkulacka();
    Caretaker<String> historie = new Caretaker<>(kalkulacka);
    kalkulacka.setPriklad("1 + 1");
    System.out.println(kalkulacka.getPriklad());
    historie.uloz();
    kalkulacka.setPriklad("2 * 3");
    System.out.println(kalkulacka.getPriklad());
    historie.zpet();
    System.out.println(kalkulacka.getPriklad());
  • $kalkulacka = new Kalkulacka();
    $historie = new Caretaker($kalkulacka);
    $kalkulacka->setPriklad("1 + 1");
    echo $kalkulacka->getPriklad() . PHP_EOL;
    $historie->uloz();
    $kalkulacka->setPriklad("2 * 3");
    echo $kalkulacka->getPriklad() . PHP_EOL;
    $historie->zpet();
    echo $kalkulacka->getPriklad() . PHP_EOL;
  • let kalkulacka = new Kalkulacka();
    let historie = new Caretaker(kalkulacka);
    kalkulacka.setPriklad("1 + 1");
    console.log(kalkulacka.getPriklad());
    historie.uloz();
    kalkulacka.setPriklad("2 * 3");
    console.log(kalkulacka.getPriklad());
    historie.zpet();
    console.log(kalkulacka.getPriklad());
  • kalkulacka = Kalkulacka()
    historie = Caretaker(kalkulacka)
    kalkulacka.set_priklad("1 + 1")
    print(kalkulacka.get_priklad())
    historie.uloz()
    kalkulacka.set_priklad("2 * 3")
    print(kalkulacka.get_priklad())
    historie.zpet()
    print(kalkulacka.get_priklad())

Testování aplikace

Aplikaci spustíme s tímto výsledkem:

  • Konzolová aplikace
    1 + 1
    2 * 3
    1 + 1
  • Konzolová aplikace
    1 + 1
    2 * 3
    1 + 1
  • Konzolová aplikace
    1 + 1
    2 * 3
    1 + 1
  • Konzolová aplikace
    1 + 1
    2 * 3
    1 + 1
  • Konzolová aplikace
    1 + 1
    2 * 3
    1 + 1

V následujícím kvízu, Kvíz - Strategy, Template method, State,Memento ve Vzory GOF, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


 

Předchozí článek
State
Všechny články v sekci
Návrhové vzory GoF
Přeskočit článek
(nedoporučujeme)
Kvíz - Strategy, Template method, State,Memento ve Vzory GOF
Článek pro vás napsal David Hartinger
Avatar
Uživatelské hodnocení:
15 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity