外觀模式 Facade Pattern 外觀模式,或者稱作門面模式,一種把複雜邏輯給包裝起來的一種模式,舉個例子來說,今天已經不單只是計算大頭菜了,而是你有個背包(Bag)要先放入鈴錢(Bells),然後拿出鈴錢從曹賣(DaisyMae)手中購買大頭菜,並且把大頭菜賣給 Nook 商店(Store),牽涉到許多的細節,透過外觀模式來將複雜的操作集中成幾個簡單的動作。
UML
實作 首先我們先慣例定義一下主角大頭菜(Turnips),在這邊只需要給予價格、數量即可。
Turnips.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 class Turnips { protected $price ; protected $count ; public function __construct (int $price , int $count ) { $this ->price = $price ; $this ->count = $count ; } public function setPrice (int $price ) { $this ->price = $price ; } public function setCount (int $count ) { $this ->count = $count ; } public function getPrice ( ): int { return $this ->price; } public function getCount ( ): int { return $this ->count; } }
接下來我們要定義背包(Bag),會需要可以放入鈴錢、大頭菜,並且可以取用,在取出的同時必須做到扣除背包裡鈴錢或大頭菜的數量。
BagInterface.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 interface BagInterface { public function setBells (int $bells ): int ; public function getBells (int $bells ): int ; public function setTurnips (Turnips $turnips ): Turnips ; public function getTurnips (int $count ): Turnips ; }
Bag.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 class Bag implements BagInterface { protected $bells = 0 ; protected $turnips ; public function setBells (int $bells ): int { $this ->bells += $bells ; return $this ->bells; } public function getBells (int $bells ): int { if ($this ->bells >= $bells ) { $this ->bells -= $bells ; return $this ->bells; } throw new \InvalidArgumentException ('背包裡頭沒有那麼多的鈴錢。' ); } public function setTurnips (Turnips $turnips ): Turnips { $this ->turnips = $turnips ; return $this ->turnips; } public function getTurnips (int $count ): Turnips { if ($this ->turnips->getCount () >= $count ) { $newCount = $this ->turnips->getCount () - $count ; $this ->turnips->setCount ($newCount ); return new Turnips ($this ->turnips->getPrice (), $count ); } throw new \InvalidArgumentException ('背包裡頭沒有那麼多的大頭菜。' ); } }
再來我們要定義曹賣(DaisyMae),曹賣的功能很簡單,就只需要購買大頭菜即可,當玩家購買大頭菜時,曹賣必須給玩家大頭菜。
DaisyMaeInterface.php
1 2 3 4 5 6 7 8 9 10 11 12 13 interface DaisyMaeInterface { public function buy (int $price , int $count ): Turnips ; }
DaisyMae.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class DaisyMae implements DaisyMaeInterface { public function buy (int $price , int $count ): Turnips { return new Turnips ($price , $count ); } }
再來我們需要建立 Nook 商店(Store),商店的功能也很簡單,就是把大頭菜賣給商店就可以了。
StoreInterface.php
1 2 3 4 5 6 7 8 9 10 11 12 13 interface StoreInterface { public function sell (Turnips $turnips , int $price ): int ; }
Store.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Store implements StoreInterface { public function sell (Turnips $turnips , int $price ): int { return $turnips ->getCount () * $price ; } }
最後我們需要來建立外觀(Facade),其功能是負責處理複雜邏輯,將其化為簡單的動作,這裡我們需要幾個簡單的動作,分別是:
在背包裡放入鈴錢。
向曹賣購買大頭菜。
向 Nook 商店販賣大頭菜。
從背包裡取出鈴錢。
Facade.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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 class Facade { protected $bag ; protected $store ; protected $daisyMae ; public function __construct (BagInterface $bag , StoreInterface $store , DaisyMaeInterface $daisyMae ) { $this ->bag = $bag ; $this ->store = $store ; $this ->daisyMae = $daisyMae ; } public function takeupBells (int $bells ): int { return $this ->bag->setBells ($bells ); } public function takeoutBells (int $bells ): int { return $this ->bag->getBells ($bells ); } public function buyTurnips (int $price , int $count ): int { $this ->bag->getBells ($price * $count ); $turnips = $this ->daisyMae->buy ($price , $count ); $this ->bag->setTurnips ($turnips ); return $this ->bag->setBells (0 ); } public function sellTurnips (int $price , int $count ): int { $turnips = $this ->bag->getTurnips ($count ); $bells = $this ->store->sell ($turnips , $price ); return $this ->bag->setBells ($bells ); } }
測試 撰寫完外觀模式後,我們需要測試程式邏輯是否正確,因此接下來也會有幾個大項目需要測試:
在背包裡塞入 10,000 鈴錢。
購買 40 顆單價 100 鈴錢的大頭菜。
以 400 鈴錢賣出 20 顆大頭菜。
從背包拿出 10,000 鈴錢。
再以 600 鈴錢賣出 20 顆大頭菜。
FacadePatternTest.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 class FacadePatternTest extends TestCase { public function test_buy_and_sell_turnips ( ) { $bag = new Bag (); $store = new Store (); $daisyMae = new DaisyMae (); $facade = new Facade ($bag , $store , $daisyMae ); $this ->assertEquals (10000 , $facade ->takeupBells (10000 )); $this ->assertEquals (6000 ,$facade ->buyTurnips (100 , 40 )); $this ->assertEquals (14000 ,$facade ->sellTurnips (400 , 20 )); $this ->assertEquals (4000 ,$facade ->takeoutBells (10000 )); $this ->assertEquals (16000 ,$facade ->sellTurnips (600 , 20 )); } }
最後測試的執行結果會獲得如下:
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 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. ==> AbstractFactoryTest ✔ ✔ ✔ ✔ ==> BuilderPatternTest ✔ ✔ ✔ ✔ ==> FactoryMethodTest ✔ ✔ ✔ ✔ ==> PoolPatternTest ✔ ✔ ==> PrototypePatternTest ✔ ✔ ==> SimpleFactoryTest ✔ ✔ ✔ ✔ ==> SingletonPatternTest ✔ ==> StaticFactoryTest ✔ ✔ ✔ ✔ ✔ ==> AdapterPatternTest ✔ ✔ ==> BridgePatternTest ✔ ✔ ✔ ==> CompositePatternTest ✔ ✔ ✔ ==> DataMapperTest ✔ ✔ ==> DecoratorPatternTest ✔ ✔ ==> DependencyInjectionTest ✔ ✔ ✔ ==> FacadePatternTest ✔ Time: 00:00.028, Memory: 6.00 MB OK (42 tests, 93 assertions)
完整程式碼 設計模式不難,找回快樂而已,以大頭菜為例。
參考文獻