前言
我們學習的設計模式分為三類:建立者模式、結構型模式、行為型模式;建立型模式與物件的建立有關;結構型模式處理類或物件的組合;而行為型模式是對類或物件怎樣互動和怎樣分配職責進行描述;
內容:上一篇介紹PHP 設計模式的建立型一篇,這一篇是結構型。包括:介面卡模式(Adapter),橋樑模式(Bridge),組合模式(Composite),裝飾模式(Decorator),門面模式(Facade),享元模式(Flyweight),代理模式(Proxy),資料對映模式(Data Mapper),依賴注入模式(Dependency Injection),流介面模式(Fluent Interface),註冊模式(Registry)
(一)介面卡模式(Adapter)
- 定義
將一個類的介面轉換成可應用的相容介面。介面卡使原本由於介面不相容而不能一起工作的那些類可以一起工作。
- 解釋例: 在朋友聚會上碰到了一個美女Sarah,從香港來的,可我不會說粵語,她不會說普通話,只好求助於我的朋友kent了,他作為我和Sarah之間的Adapter,讓我和Sarah可以相互交談了 例項:客戶端資料庫介面卡
- 例項: 客戶端資料庫介面卡
- 例項程式碼實現
interface IDatabase { function connect($host, $user, $passwd, $dbname); function query($sql); function close(); } class MySQL implements IDatabase { protected $conn; function connect($host, $user, $passwd, $dbname) { $conn = mysqli_connect($host, $user, $passwd, $dbname); $this->conn = $conn; } function query($sql) { return mysqli_query($this->conn, $sql); } function close() { mysqli_close($this->conn); } } class PDO1 implements IDatabase { protected $conn; function connect($host, $user, $passwd, $dbname) { $conn = new \PDO("mysql:host=$host;dbname=$dbname", $user, $passwd); $this->conn = $conn; } function query($sql) { return $this->conn->query($sql); } function close() { unset($this->conn); } } $db = new MySQL(); $db->connect('127.0.0.1','root','root','item'); $db->query('select * from p1'); $db->close(); $db = new PDO1(); $db->connect('127.0.0.1','root','root','item'); $db->query('select * from p1'); $db->close();
(二)橋樑模式(Bridge)
- 定義
將抽象化與實現化脫耦,使得二者可以獨立的變化,也就是說將他們之間的強關聯變成弱關聯,也就是指在一個軟體系統的抽象化和實現化之間使用組合/聚合關係而不是繼承關係,從而使兩者可以獨立的變化。
- 解釋例: BRIDGE-早上碰到MM,要說早上好,晚上碰到MM,要說晚上好;碰到MM穿了件新衣服,要說你的衣服好漂亮哦,碰到MM新做的髮型,要說你的頭髮好漂亮哦。不要問我"早上碰到MM新做了個髮型怎麼說"這種問題,自己用BRIDGE組合一下不就行了.
- 例項: 服務需要需要輸出字串格式時,調字串格式化類,需要輸出HTML格式時,調HTML格式化類
- 例項程式碼實現
interface FormatterInterface { public function format(string $text); } class PlainTextFormatter implements FormatterInterface { /** * 返回字串格式。 */ public function format(string $text) { return $text; } } class HtmlFormatter implements FormatterInterface { /** * 返回 HTML 格式。 */ public function format(string $text) { return sprintf('<p>%s</p>', $text); } } abstract class Service { /** * @var FormatterInterface * 定義實現屬性。 */ protected $implementation; /** * @param FormatterInterface $printer * 傳入 FormatterInterface 實現類物件。 */ public function __construct(FormatterInterface $printer) { $this->implementation = $printer; } /** * @param FormatterInterface $printer * 和構造方法的作用相同。 */ public function setImplementation(FormatterInterface $printer) { $this->implementation = $printer; } /** * 建立抽象方法 get() 。 */ abstract public function get(); } class HelloWorldService extends Service { /** * 定義抽象方法 get() 。 * 根據傳入的格式類定義來格式化輸出 'Hello World' 。 */ public function get() { return $this->implementation->format('Hello World'); } } $service = new HelloWorldService(new PlainTextFormatter()); $service->get(); //Hello World $service->setImplementation(new HtmlFormatter()); $service->get(); //Hello World
(三)組合模式(Composite)
- 定義
合成模式將物件組織到樹結構中,可以用來描述整體與部分的關係。合成模式就是一個處理物件的樹結構的模式。合成模式把部分與整體的關係用樹結構表示出來。合成模式使得客戶端把一個個單獨的成分物件和由他們複合而成的合成物件同等看待。
- 解釋例: Mary今天過生日。"我過生日,你要送我一件禮物。""嗯,好吧,去商店,你自己挑。""這件T恤挺漂亮,買,這條裙子好看,買,這個包也不錯,買。""喂,買了三件了呀,我只答應送一件禮物的哦。""什麼呀,T恤加裙子加包包,正好配成一套呀,小姐,麻煩你包起來。""……",MM都會用Composite模式了,你會了沒有?
- 例項: 想要生成一個form表單框.需要一個text生成類,一個input生成類.一個合成類
- 例項程式碼實現
interface RenderableInterface { public function render(): string; } class Form implements RenderableInterface { /** * @var RenderableInterface[] */ private $elements; /** * 遍歷所有元素,並對他們呼叫 render() 方法,然後返回表單的完整 * 的解析表達。 * * 從外部上看,我們不會看到遍歷過程,該表單的操作過程與單一對 * 象例項一樣 * * @return string */ public function render(): string { $formCode = '<form>'; foreach ($this->elements as $element) { $formCode .= $element->render(); } $formCode .= '</form>'; return $formCode; } /** * @param RenderableInterface $element */ public function addElement(RenderableInterface $element) { $this->elements[] = $element; } } class InputElement implements RenderableInterface { public function render(): string { return '<input type="text" />'; } } class TextElement implements RenderableInterface { /** * @var string */ private $text; public function __construct(string $text) { $this->text = $text; } public function render(): string { return $this->text; } } $form = new Form(); $form->addElement(new TextElement('Email:')); $form->addElement(new InputElement()); $embed->addElement(new TextElement('Password:')); $embed->addElement(new InputElement()); $form->addElement($embed); echo $form->render();
(四)裝飾模式(Decorator)
- 定義
裝飾模式以對客戶端透明的方式擴充套件物件的功能,是繼承關係的一個替代方案,提供比繼承更多的靈活性。動態給一個物件增加功能,這些功能可以再動態的撤消。增加由一些基本功能的排列組合而產生的非常大量的功能。
- 解釋例: Mary過完輪到Sarly過生日,還是不要叫她自己挑了,不然這個月伙食費肯定玩完,拿出我去年在華山頂上照的照片,在背面寫上"最好的的禮物,就是愛你的Fita",再到街上禮品店買了個像框(賣禮品的MM也很漂亮哦)再找隔壁搞美術設計的Mike設計了一個漂亮的盒子裝起來……,我們都是Decorator,最終都在修飾我這個人呀,怎麼樣,看懂了嗎?
- 例項: 返回json資料和xml資料
-
例項程式碼實現
abstract class RendererDecorator { /** * 定義渲染介面變數。 */ protected $wrapped; /** * 傳入渲染介面類物件 */ public function __construct($data) { $this->wrapped = $data; } abstract function renderData(): string; } class XmlRenderer extends RendererDecorator { /** * 對傳入的渲染介面物件進行處理,生成 DOM 資料檔案。 */ public function renderData(): string { $doc = new \DOMDocument(); $doc->appendChild($doc->createElement('content', $this->wrapped)); return $doc->saveXML(); } } class JsonRenderer extends RendererDecorator { /** * 對傳入的渲染介面物件進行處理,生成 JSON 資料。 */ public function renderData(): string { return json_encode($this->wrapped); } } $data = 'hello world'; $json = new JsonRenderer($data); $json->renderData(); $xml = new XmlRenderer($data); $xml->renderData();
(五)門面模式(Facade)
- 定義
要求一個子系統的外部與其內部的通訊必須通過一個統一的門面(Facade)物件進行。門面模式提供一個高層次的介面,使得子系統更易於使用。就如同醫院的接待員一樣,門面模式的門面類將客戶端與子系統的內部複雜性分隔開,使得客戶端只需要與門面物件打交道,而不需要與子系統內部的很多物件打交道。
- 解釋例: 我有一個專業的Nikon相機,我就喜歡自己手動調光圈、快門,這樣照出來的照片才專業,但MM可不懂這些,教了半天也不會。幸好相機有Facade設計模式,把相機調整到自動檔,只要對準目標按快門就行了,一切由相機自動調整,這樣MM也可以用這個相機給我拍張照片了。
- 例項: 一個保安系統由錄影機、電燈組成。保安系統的操作人員需要經常將這些儀器上班啟動和下班關閉。
- 例項程式碼實現
abstract class SecuritySystem { abstract function TurnOn(); abstract function TurnOff(); } class Camera extends SecuritySystem { public function TurnOn() { echo "Turning on the camera".'<br/>'; } public function TurnOff() { echo "Turning off the camera".'<br/>'; } } class Light extends SecuritySystem { public function TurnOn() { echo "Turning on the light".'<br/>'; } public function TurnOff() { echo "Turning off the light".'<br/>'; } } class SecurityFacade { private $securitySystem; public function __construct(SecuritySystem ...$securitySystem) { $this->securitySystem = $securitySystem; } //開啟系統 public function TurnOnSystem() { foreach ($this->securitySystem as $key => $value) { $value->TurnOn(); } } //關閉系統 public function TurnOffSystem() { foreach ($this->securitySystem as $key => $value) { $value->TurnOff(); } } } $system = new SecurityFacade(new Camera,new Light); $system->TurnOnSystem(); $system->TurnOffSystem(); /** * Turning on the camera * Turning on the light * Turning off the camera * Turning off the light */
(六)享元模式(Flyweight)
- 定義
為了節約記憶體的使用,享元模式會盡量使類似的物件共享記憶體。在大量類似物件被使用的情況中這是十分必要的。常用做法是在外部資料結構中儲存類似物件的狀態,並在需要時將他們傳遞給享元物件。
- 解釋例: 跟MM在網上聊天,一開頭總是"hi,你好","你從哪兒來呀?""你多大了?""身高多少呀?"這些話,真煩人,寫個程式做為我的Proxy吧,凡是接收到這些話都設定好了自動的回答,接收到其他的話時再通知我回答.
- 例項: 由抽象享元-具體享元-享元工廠生成享後設資料。
-
例項程式碼實現
interface FlyweightInterface { /** * 建立傳遞函式。 * 返回字串格式資料。 */ public function render(string $extrinsicState): string; } class CharacterFlyweight implements FlyweightInterface { /** * 任何具體的享元物件儲存的狀態必須獨立於其執行環境。 * 享元物件呈現的特點,往往就是對應的編碼的特點。 * * @var string */ private $name; /** * 輸入一個字串物件 $name。 */ public function __construct(string $name) { $this->name = $name; } /** * 實現 FlyweightInterface 中的傳遞方法 render() 。 */ public function render(string $font): string { // 享元物件需要客戶端提供環境依賴資訊來自我定製。 // 外在狀態經常包含享元物件呈現的特點,例如字元。 return sprintf('Character %s with font %s', $this->name, $font); } } class FlyweightFactory implements \Countable { /** * @var CharacterFlyweight[] * 定義享元特徵陣列。 * 用於儲存不同的享元特徵。 */ private $pool = []; /** * 輸入字串格式資料 $name。 * 返回 CharacterFlyweight 物件。 */ public function get(string $name): CharacterFlyweight { if (!isset($this->pool[$name])) { $this->pool[$name] = new CharacterFlyweight($name); } return $this->pool[$name]; } /** * 返回享元特徵個數。 */ public function count(): int { return count($this->pool); } } $characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; $fonts = ['Arial', 'Times New Roman', 'Verdana', 'Helvetica']; $factory = new FlyweightFactory(); foreach ($characters as $char) { foreach ($fonts as $font) { $flyweight = $factory->get($char); $rendered = $flyweight->render($font); echo $rendered.'<br/>'; } }
(七)代理模式(Proxy)
- 定義
代理模式(Proxy)為其他物件提供一種代理以控制對這個物件的訪問。使用代理模式建立代理物件,讓代理物件控制目標物件的訪問(目標物件可以是遠端的物件、建立開銷大的物件或需要安全控制的物件),並且可以在不改變目標物件的情況下新增一些額外的功能.
- 解釋例: 在某些情況下,一個客戶不想或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用,並且可以通過代理物件去掉客戶不能看到的內容和服務或者新增客戶需要的額外服務。
- 例項: 代理主題角色.
-
例項程式碼實現
abstract class Subject { // 抽象主題角色 abstract public function action(); } class RealSubject extends Subject { // 真實主題角色 public function __construct() {} public function action() {} } class ProxySubject extends Subject { // 代理主題角色 private $_real_subject = NULL; public function __construct() {} public function action() { $this->_beforeAction(); if (is_null($this->_real_subject)) { $this->_real_subject = new RealSubject(); } $this->_real_subject->action(); $this->_afterAction(); } private function _beforeAction() { echo '在action前,我想幹點啥....'; } private function _afterAction() { echo '在action後,我還想幹點啥....'; } } // client $subject = new ProxySubject(); $subject->action();//輸出:在action前,我想幹點啥....在action後,我還想幹點啥....
(八)資料對映模式(Data Mapper)
- 定義
描述如何建立提供透明訪問任何資料來源的物件。資料對映模式,也叫資料訪問物件模式,或資料物件對映模式。
- 例項: 持久化資料儲存層、駐於記憶體的資料表現層、以及資料對映本身三者相互獨立、互不依賴。這個資料訪問層由一個或多個對映器(或者資料訪問物件)組成,用於實現資料傳輸。通用的資料訪問層可以處理不同的實體型別,而專用的則處理一個或幾個.
-
例項程式碼實現
class User { /** * @var string */ private $username; /** * @var string */ private $email; public static function fromState(array $state): User { // 在你訪問的時候驗證狀態 return new self( $state['username'], $state['email'] ); } public function __construct(string $username, string $email) { // 先驗證引數在設定他們 $this->username = $username; $this->email = $email; } /** * @return string */ public function getUsername() { return $this->username; } /** * @return string */ public function getEmail() { return $this->email; } } class UserMapper { /** * @var StorageAdapter */ private $adapter; /** * @param StorageAdapter $storage */ public function __construct(StorageAdapter $storage) { $this->adapter = $storage; } /** * 根據 id 從儲存器中找到使用者,並返回一個使用者物件 * 在記憶體中,通常這種邏輯將使用 Repository 模式來實現 * 然而,重要的部分是在下面的 mapRowToUser() 中,它將從中建立一個業務物件 * 從儲存中獲取的資料 * @param int $id * @return User */ public function findById(int $id): User { $result = $this->adapter->find($id); if ($result === null) { throw new \InvalidArgumentException("User #$id not found"); } return $this->mapRowToUser($result); } private function mapRowToUser(array $row): User { return User::fromState($row); } } class StorageAdapter { /** * @var array */ private $data = []; public function __construct(array $data) { $this->data = $data; } /** * @param int $id * * @return array|null */ public function find(int $id) { if (isset($this->data[$id])) { return $this->data[$id]; } return null; } } $storage = new StorageAdapter([1 => ['username' => 'domnikl', 'email' => 'liebler.dominik@gmail.com']]); $mapper = new UserMapper($storage); $user = $mapper->findById(1); //object(User)#3 (2) { ["username":"User":private]=> string(7) "domnikl" ["email":"User":private]=> string(25) "liebler.dominik@gmail.com" }
(九)依賴注入模式(Dependency Injection)
- 定義
用鬆散耦合的方式來更好的實現可測試、可維護和可擴充套件的程式碼。
- 例項: DatabaseConfiguration 被注入 DatabaseConnection 並獲取所需的 $config 如果沒有依賴注入模式,配置將直接建立 DatabaseConnection.
-
例項程式碼實現
class DatabaseConfiguration { /** * @var string */ private $host; /** * @var int */ private $port; /** * @var string */ private $username; /** * @var string */ private $password; public function __construct(string $host, int $port, string $username, string $password) { $this->host = $host; $this->port = $port; $this->username = $username; $this->password = $password; } public function getHost(): string { return $this->host; } public function getPort(): int { return $this->port; } public function getUsername(): string { return $this->username; } public function getPassword(): string { return $this->password; } } class DatabaseConnection { /** * @var DatabaseConfiguration */ private $configuration; /** * @param DatabaseConfiguration $config */ public function __construct(DatabaseConfiguration $config) { $this->configuration = $config; } public function getDsn(): string { // 這僅僅是演示,而不是一個真正的 DSN // 注意,這裡只使用了注入的配置。 所以, // 這裡是關鍵的分離關注點。 return sprintf( '%s:%s@%s:%d', $this->configuration->getUsername(), $this->configuration->getPassword(), $this->configuration->getHost(), $this->configuration->getPort() ); } } $config = new DatabaseConfiguration('localhost', 3306, 'domnikl', '1234'); $connection = new DatabaseConnection($config); $connection->getDsn();
(十)流介面模式(Fluent Interface)
- 定義
用來編寫易於閱讀的程式碼,就像自然語言一樣
- 例項: 資料查詢操作.
-
例項程式碼實現
class Sql { /** * @var array */ private $fields = []; /** * @var array */ private $from = []; /** * @var array */ private $where = []; public function select(array $fields): Sql { $this->fields = $fields; return $this; } public function from(string $table, string $alias): Sql { $this->from[] = $table.' AS '.$alias; return $this; } public function where(string $condition): Sql { $this->where[] = $condition; return $this; } public function __toString(): string { return sprintf( 'SELECT %s FROM %s WHERE %s', join(', ', $this->fields), join(', ', $this->from), join(' AND ', $this->where) ); } } $query = (new Sql())->select(['foo', 'bar']) ->from('foobar', 'f') ->where('f.bar = ?'); echo $query; //SELECT foo, bar FROM foobar AS f WHERE f.bar = ?
(十一)註冊模式(Registry)
- 定義
目的是能夠儲存在應用程式中經常使用的物件例項,通常會使用只有靜態方法的抽象類來實現(或使用單例模式)。需要注意的是這裡可能會引入全域性的狀態,我們需要使用依賴注入來避免它。
- 例項: 物件儲存.
-
例項程式碼實現
class Register { protected static $objects; static public function set($alias, $object) { self::$objects[$alias] = $object; } static public function get($key) { if (!isset(self::$objects[$key])) { return false; } return self::$objects[$key]; } public function _unset($alias) { unset(self::$objects[$alias]); } } Register::set('LOGGER',new \stdClass); var_dump(Register::get('LOGGER'));