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řídyOriginator
. 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:
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í.