觀察者模式 Observer Pattern 觀察者模式,一種現在全中國都知道你來了的模式,就有點像是收音機,打開收音機就開始自動接收廣播,關掉收音機就停止接收,就有點像是動森的連線模式,你跟朋友在同一座島遊玩時,如果有其他朋友來玩,那你們通通都會收到這個通知,然後開始看渡渡鳥航空飛起來的動畫。
UML
實作 這次我們要實作有一座島嶼(Island)讓玩家(Player)加入,當有玩家加入島嶼時,島嶼上其他的玩家會收到系統通知,所以會需要讓島嶼(Island)去繼承 SplSubject
這個類別,讓島嶼可以把玩家加入島嶼當中、讓玩家離開島嶼,實作這些時也順便通知其他玩家事件的產生,最後提供一個 sendMessages
的方法來通知當前所有加入觀察者名單的玩家。
Island.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 class Island implements SplSubject { protected SplObjectStorage $observers ; public function __construct ( ) { $this ->observers = new SplObjectStorage (); } public function attach (SplObserver $observer ) { $this ->sendMessages ("有玩家加入了!" ); $this ->observers->attach ($observer ); } public function detach (SplObserver $observer ) { $this ->observers->detach ($observer ); $this ->sendMessages ("有玩家離開了!" ); } public function notify ( ) { foreach ($this ->observers as $observer ) { $observer ->update (); } } public function sendMessages (string $message ) { foreach ($this ->observers as $observer ) { $observer ->sendMessage ($message ); } } }
實作完島嶼以後,接下來要把玩家(Player)建立出來,讓玩家去繼承 SplObserver
這個類別,這些類別是 php
內建提供觀察者模式的類別,詳細資訊列在 #額外補充 當中,另外需要額外提供一個 sendMessage
方法,代表玩家收到島嶼發出來的通知了,所以把訊息輸出出來,並補上玩家名稱的標示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class PlayerObserver implements SplObserver { protected string $user ; protected array $observers = []; public function __construct (string $user ) { $this ->user = $user ; } public function update (SplSubject $subject ) { } public function sendMessage (string $message ) { echo "[$this ->user 收到通知] $message " ; } }
額外補充 SplSubject 繼承 SplSubject
這個類別會需要實作 attach
、detach
及 notify
這三個方法,attach
會需要賦予 SplObserver
觀察者物件,也就是把觀察者加入集合當中,而 detach
則是抽離指定的 SplObserver
物件,也就是把觀察者拔除,最後 notify
則是通知仍然存在於集合中的觀察者們。
1 2 3 4 5 6 SplSubject { abstract public attach ( SplObserver $observer ) : void abstract public detach ( SplObserver $observer ) : void abstract public notify ( void ) : void }
SplObserver 繼承 SplObserver
這個類別會需要實作 update
這個方法。
1 2 3 4 SplObserver { abstract public update ( SplSubject $subject ) : void }
SplObjectStorage SplObjectStorage
這個類別則是提供一系列的方法供使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SplObjectStorage implements Countable , Iterator , Serializable , ArrayAccess { public addAll ( SplObjectStorage $storage ) : void public attach ( object $object [, mixed $data = NULL ] ) : void public contains ( object $object ) : bool public count ( void ) : int public current ( void ) : object public detach ( object $object ) : void public getHash ( object $object ) : string public getInfo ( void ) : mixed public key ( void ) : int public next ( void ) : void public offsetExists ( object $object ) : bool public offsetGet ( object $object ) : mixed public offsetSet ( object $object [, mixed $data = NULL ] ) : void public offsetUnset ( object $object ) : void public removeAll ( SplObjectStorage $storage ) : void public removeAllExcept ( SplObjectStorage $storage ) : void public rewind ( void ) : void public serialize ( void ) : string public setInfo ( mixed $data ) : void public unserialize ( string $serialized ) : void public valid ( void ) : bool }
測試 這次的測試會假設有一座島嶼建立起來,並且陸續有玩家加入、離開,模擬這段過程所會產生的訊息是否正確,所以會預設幾些動作、動作所產生的訊息:
建立島嶼(Island)
建立玩家(Player A),加入前島嶼上還沒有玩家,所以沒有人收到通知。
建立玩家(Player B),加入前島嶼上已經有玩家 A 了,所以會產生以下通知:
[Player A 收到通知] 有玩家加入了!
建立玩家(Player C),加入前島嶼上已經有玩家 A、B 了,所以會產生以下通知:
[Player A 收到通知] 有玩家加入了!
[Player B 收到通知] 有玩家加入了!
玩家(Player B)離開了島嶼,離開後島嶼上剩下 A、C 玩家,所以會產生以下通知:
[Player A 收到通知] 有玩家離開了!
[Player C 收到通知] 有玩家離開了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class ObserverPatternTest extends TestCase { public function test_observer ( ) { $this ->expectOutputString (implode (array ( "[Player A 收到通知] 有玩家加入了!" , "[Player A 收到通知] 有玩家加入了!" , "[Player B 收到通知] 有玩家加入了!" , "[Player A 收到通知] 有玩家離開了!" , "[Player C 收到通知] 有玩家離開了!" , ))); $island = new Island (); $playerA = new PlayerObserver ('Player A' ); $island ->attach ($playerA ); $playerB = new PlayerObserver ('Player B' ); $island ->attach ($playerB ); $playerC = new PlayerObserver ('Player C' ); $island ->attach ($playerC ); $island ->detach ($playerB ); } }
最後測試的執行結果會獲得如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 PHPUnit Pretty Result Printer 0.28.0 by Codedungeon and contributors. ==> Configuration: ~/php-design-pattern/vendor/codedungeon/phpunit-result-printer/src/phpunit-printer.yml PHPUnit 9.2.6 by Sebastian Bergmann and contributors. ==> ...fResponsibilitiesTest ✔ ✔ ✔ ==> CommandPatternTest ✔ ==> IteratorPatternTest ✔ ✔ ✔ ✔ ==> MediatorPatternTest ✔ ✔ ✔ ==> MementoPatternTest ✔ ==> NullObjectPatternTest ✔ ✔ ✔ ✔ ==> ObserverPatternTest ✔ ==> AbstractFactoryTest ✔ ✔ ✔ ✔ ==> BuilderPatternTest ✔ ✔ ✔ ✔ ==> FactoryMethodTest ✔ ✔ ✔ ✔ ==> PoolPatternTest ✔ ✔ ==> PrototypePatternTest ✔ ✔ ==> SimpleFactoryTest ✔ ✔ ✔ ✔ ==> SingletonPatternTest ✔ ==> StaticFactoryTest ✔ ✔ ✔ ✔ ✔ ==> AdapterPatternTest ✔ ✔ ==> BridgePatternTest ✔ ✔ ✔ ==> CompositePatternTest ✔ ✔ ✔ ==> DataMapperTest ✔ ✔ ==> DecoratorPatternTest ✔ ✔ ==> DependencyInjectionTest ✔ ✔ ✔ ==> FacadePatternTest ✔ ==> FluentInterfaceTest ✔ ==> FlyweightPatternTest ✔ ==> ProxyPatternTest ✔ ✔ ==> RegistryPatternTest ✔ ✔ ✔ ✔ ✔ Time: 00:00.097, Memory: 8.00 MB OK (68 tests, 137 assertions)
完整程式碼 設計模式不難,找回快樂而已,以大頭菜為例。
參考文獻