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