在之前的文章《深入淺析PHP中的建造者模式》中我們介紹了PHP中的建造者模式,下面本篇文章帶大家了解一下PHP設(shè)計模式中的備忘錄模式。
備忘錄,這個名字其實就已經(jīng)很形象的解釋了它的作用。典型的例子就是我們原來玩硬盤游戲時的存檔功能。當你對即將面對的大BOSS有所顧慮時,一般都會先保存一次進度存檔。如果挑戰(zhàn)失敗了,直接讀取存檔就可以恢復到挑戰(zhàn)BOSS前的狀態(tài),然后你就開開心心的再去練一會級回來解決這個大BOSS就好了。不過,為了以防萬一,在挑戰(zhàn)BOSS之前存?zhèn)€檔總是好的。另外一個例子就是我們碼農(nóng)們天天要用到的代碼管理工具Git或者Svn了。每次的提交都像是一次存檔備份,當新代碼出現(xiàn)問題的時候,直接回滾恢復就行了。這些,都是備忘錄模式的典型應用,下面就一起來看看這個模式吧。
Gof類圖及解釋
GoF定義:在不破壞封裝性的前提下,捕獲一個對象的內(nèi)部狀態(tài),并在該對象之外保存這個狀態(tài)。這樣以后就可將該對象恢復到原先保存的狀態(tài)
GoF類圖:
代碼實現(xiàn):
class Originator { private $state; public function SetMeneto(Memento $m) { $this->state = $m->GetState(); } public function CreateMemento() { $m = new Memento(); $m->SetState($this->state); return $m; } public function SetState($state) { $this->state = $state; } public function ShowState() { echo $this->state, PHP_EOL; } }
原發(fā)器,也可以叫做發(fā)起人。它有一個內(nèi)部狀態(tài)(state),這個狀態(tài)可以在不同的情況下進行改變。當某一個事件發(fā)生時,需要將這個狀態(tài)恢復到原先的狀態(tài)。在這里,我們有一個CreateMemento()用于創(chuàng)建一個備忘錄(存檔),有一個SetMeneto()用于還原狀態(tài)(讀檔)。
class Memento { private $state; public function SetState($state) { $this->state = $state; } public function GetState() { return $this->state; } }
備忘錄,非常簡單,就是用于記錄狀態(tài)。將這個狀態(tài)以對象的形式保存,就可以讓原發(fā)器非常方便地創(chuàng)建很多存檔用于記錄各種不同的狀態(tài)。
class Caretaker { private $memento; public function SetMemento($memento) { $this->memento = $memento; } public function GetMemento() { return $this->memento; } }
負責人,也叫做管理者類,保存?zhèn)渫?,當需要的時候從這里取出備忘錄。它只負責保存,不能修改備忘錄。在復雜的應用中,可以將這里做成列表,就像游戲中可以選擇性的展現(xiàn)多條存檔記錄供玩家選擇。
$o = new Originator(); $o->SetState('狀態(tài)1'); $o->ShowState(); // 保存狀態(tài) $c = new Caretaker(); $c->SetMemento($o->CreateMemento()); $o->SetState('狀態(tài)2'); $o->ShowState(); // 還原狀態(tài) $o->SetMeneto($c->GetMemento()); $o->ShowState();
客戶端的調(diào)用中,我們的原發(fā)器初始化狀態(tài)后進行了保存,然后人為的更改了狀態(tài)。這時只需要通過負責人將狀態(tài)還原回來就可以了。
- 備忘錄模式說白了就是讓一個外部類B來保存A的內(nèi)部狀態(tài),然后在適當?shù)臅r候可以方便的還原這個狀態(tài)。
- 備忘錄模式的應用場景其實非常多,瀏覽器的回退、數(shù)據(jù)庫的備份還原、操作系統(tǒng)的備份還原、文檔的撤銷重做、棋牌游戲的悔棋等等
- 這個模式能夠保持對原發(fā)器的封裝,也就是這些狀態(tài)需要對外部的對象隱藏,所以只能交給一個備忘錄對象來記錄
- 狀態(tài)在原發(fā)器和備忘錄之間的拷貝可能帶來性能問題,特別是大型對象的復雜繁多的內(nèi)部狀態(tài),而且也會帶來一些編碼方面的漏洞,比如漏掉某些狀態(tài)
Mac的時光機功能大家有了解過吧,可以將電腦恢復到某一時間點的狀態(tài)下。其實windows的ghost也是類似的功能。我們的手機操作系統(tǒng)上也決定開發(fā)這樣的一個功能。當我們點擊時光機備份時,將手機上所有的資料、數(shù)據(jù)、狀態(tài)信息都壓縮保存起來,如果用戶允許的話,我們將這個壓縮包上傳到我們的云服務器上避免占用用戶的手機內(nèi)存,否則就只能保存到用戶的手機內(nèi)存中了。當用戶的手機需要恢復到某個時間點,我們將所有的時光機備份列出,用戶只需要用手指輕輕一按就可以把手機系統(tǒng)狀態(tài)恢復到當時的樣子了,是不是非常方便??!
完整代碼:https://github.com/zhangyue0503/designpatterns-php/blob/master/17.memento/source/memento.php
實例
這次又回到短信發(fā)送的例子上來。通常我們做短信或者郵件發(fā)送這些功能時,會有一個隊列從數(shù)據(jù)庫或者緩存中讀取要發(fā)送的內(nèi)容進行發(fā)送,如果成功了就不管了,如果失敗了會將短信的狀態(tài)改成失敗或者重發(fā)。在這里,我們直接將它改回到之前未發(fā)送的狀態(tài)然后等待下次發(fā)送的隊列再次執(zhí)行發(fā)送。
短信發(fā)送類圖
完整源碼:https://github.com/zhangyue0503/designpatterns-php/blob/master/17.memento/source/memento-message.php
<?php class Message { private $content; private $to; private $state; private $time; public function __construct($to, $content) { $this->to = $to; $this->content = $content; $this->state = '未發(fā)送'; $this->time = time(); } public function Show() { echo $this->to, '---', $this->content, '---', $this->time, '---', $this->state, PHP_EOL; } public function CreateSaveSate() { $ss = new SaveState(); $ss->SetState($this->state); return $ss; } public function SetSaveState($ss) { if ($this->state != $ss->GetState()) { $this->time = time(); } $this->state = $ss->GetState(); } public function SetState($state) { $this->state = $state; } public function GetState() { return $this->state; } } class SaveState { private $state; public function SetState($state) { $this->state = $state; } public function GetState() { return $this->state; } } class StateContainer { private $ss; public function SetSaveState($ss) { $this->ss = $ss; } public function GetSaveState() { return $this->ss; } } // 模擬短信發(fā)送 $mList = []; $scList = []; for ($i = 0; $i < 10; $i++) { $m = new Message('手機號' . $i, '內(nèi)容' . $i); echo '初始狀態(tài):'; $m->Show(); // 保存初始信息 $sc = new StateContainer(); $sc->SetSaveState($m->CreateSaveSate()); $scList[] = $sc; // 模擬短信發(fā)送,2發(fā)送成功,3發(fā)送失敗 $pushState = mt_rand(2, 3); $m->SetState($pushState == 2 ? '發(fā)送成功' : '發(fā)送失敗'); echo '發(fā)布后狀態(tài):'; $m->Show(); $mList[] = $m; } // 模擬另一個線程查找發(fā)送失敗的并把它們還原到未發(fā)送狀態(tài) sleep(2); foreach ($mList as $k => $m) { if ($m->GetState() == '發(fā)送失敗') { // 如果是發(fā)送失敗的,還原狀態(tài) $m->SetSaveState($scList[$k]->GetSaveState()); } echo '查詢發(fā)布失敗后狀態(tài):'; $m->Show(); }
說明
- 短信類做為我們的原發(fā)器,在發(fā)送前就保存了當前的發(fā)送狀態(tài)
- 隨機模擬短信發(fā)送,只有兩個狀態(tài),發(fā)送成功或者失敗,并改變原發(fā)器的狀態(tài)為成功或者失敗
- 模擬另一個線程或者腳本對短信的發(fā)送狀態(tài)進行檢查,如果發(fā)現(xiàn)有失敗的,就將它重新改回未發(fā)送的狀態(tài)
- 這里我們只是保存了發(fā)送狀態(tài)這一個字段,其他原發(fā)器的內(nèi)部屬性并沒有保存
- 真實的場景下我們應該會有一個重試次數(shù)的限制,當超過這個次數(shù)后,狀態(tài)改為徹底的發(fā)送失敗,不再進行重試了
原文地址:https://juejin.cn/post/6844903983555805192
作者:硬核項目經(jīng)理
推薦學習:《PHP視頻教程》