設計模式總結(模式篇)

oliver-l發表於2020-10-14

設計模式

設計模式是針對軟體開發中經常遇到的一些設計問題,總結出來的一套解決方案或者設計思路。大部分設計模式要解決的都是程式碼的可擴充套件性問題。設計模式相對於設計原則來說,沒有那麼抽象,而且大部分都不難理解,程式碼實現也並不複雜。

它們又可以分為三大類:建立型、結構型、行為型。

建立型設計模式主要解決“物件的建立”問題,結構型設計模式主要解決“類或物件的組合”問題,那行為型設計模式主要解決的就是“類或物件之間的互動”問題

1. 建立型

常用的有:

不常用的有:

2. 結構型

常用的有:

不常用的有:

3. 行為型

常用的有:

不常用的有:

單例模式

單例設計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許建立一個物件(或者例項),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。

要實現一個單例,需要關注的點無外乎下面幾個:

  • 建構函式需要是 private 訪問許可權的,這樣才能避免外部通過 new 建立例項;

  • 考慮物件建立時的執行緒安全問題;

  • 考慮是否支援延遲載入;

  • 考慮 getInstance() 效能是否高(是否加鎖)。

參考單例模式

<?php


final class Singleton
{
    /**
    * @var Singleton
    */
    private static $instance;

    /**
    * 通過懶載入獲得例項(在第一次使用的時候建立)
    */
    public static function getInstance(): Singleton
    {
        if (null === static::$instance) {
            static::$instance = new static();
        }

        return static::$instance;
    }

    /**
    * 不允許從外部呼叫以防止建立多個例項
    * 要使用單例,必須通過 Singleton::getInstance() 方法獲取例項
    */
    private function __construct()
    {
    }

    /**
    * 防止例項被克隆(這會建立例項的副本)
    */
    private function __clone()
    {
    }

    /**
    * 防止反序列化(這將建立它的副本)
    */
    private function __wakeup()
    {
    }
}

儘管單例是一個很常用的設計模式,在實際的開發中,我們也確實經常用到它,但是,有些人認為單例是一種反模式(anti-pattern)

單例存在哪些問題?

1. 單例對 OOP 特性的支援不友好
2. 單例會隱藏類之間的依賴關係
3. 單例對程式碼的擴充套件性不友好
4. 單例對程式碼的可測試性不友好
5. 單例不支援有引數的建構函式

工廠模式

簡單工廠(Simple Factory)

在下面這段程式碼中,我們根據配置檔案的字尾(json、xml、yaml、properties),選擇不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),將儲存在檔案中的配置解析成記憶體物件 RuleConfig。

class RuleConfigSource {

    public function load($path){
        $fileExtension = $this->getFileExtension($path);
        $parser = $this->createParser($fileExtension);
        if(!$parse){
            throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
        }
        //檔案中讀取配置
        $content = $parser->parse($path);
        return $content;
    }

    private function getFileExtension($filePath){
        //...解析檔名獲取副檔名,比如rule.json,返回json
        return  "json";
    }

    //建立相應的檔案解析器類
    private function createParser($extension){
        if($extension == 'json'){
            return new JsonConfigParser();
        }else if($extension == 'xml'){
            return new XmlConfigParser();
        }else if($extension == 'yaml'){
            return new YamlConfigParser();
        }else if($extension == 'properties'){
            return new PropertiesConfigParser();
        }
        return null;
    }
}

//定義配置解析介面類
interface ConfigParse(){
    //定義解析方法
    function parse();
}

//json檔案配置解析類
class JsonConfigParser extends ConfigParse{
    private public parse($path){
        //解析json檔案配置
        return $fileContext;
    }
}

//xml檔案配置解析類
class XmlConfigParser extends ConfigParse{
    private public parse($path){
        //解析xml檔案配置
        return $fileContext;
    }
}
....

$ruleConfigSource = new RuleConfigSource();
$content = $ruleConfigSource->load($path);

儘管簡單工廠模式的程式碼實現中,有多處 if 分支判斷邏輯,違背開閉原則,但權衡擴充套件性和可讀性,這樣的程式碼實現在大多數情況下(比如,不需要頻繁地新增 parser,也沒有太多的 parser)是沒有問題的。

抽象工廠(Abstract Factory)

在不指定具體類的情況下建立一系列相關或依賴物件。 通常建立的類都實現相同的介面。 抽象工廠的客戶並不關心這些物件是如何建立的,它只是知道它們是如何一起執行的。

還是根據以上的例子

//定義配置解析介面類
interface ConfigParse(){
    //定義解析方法
    function parse();
}

//json檔案配置解析類
class JsonConfigParser extends ConfigParse{
    private public parse($path){
        //解析json檔案配置
        return $fileContext;
    }
}

//xml檔案配置解析類
class XmlConfigParser extends ConfigParse{
    private public parse($path){
        //解析xml檔案配置
        return $fileContext;
    }
}

//工廠類
class ConfigParserFactory{
    public function createJsonConfigParser(){
        return new JsonConfigParser();
    }
    public function createXmlConfigParser(){
        return new XmlConfigParser();
    }
}

$parserFactory = new ConfigParserFactory();
$jsonContent = $parserFactory->createJsonConfigParser()->parse($path);

判斷要不要使用工廠模式的最本質的參考標準。

  • 封裝變化:建立邏輯有可能變化,封裝成工廠類之後,建立邏輯的變更對呼叫者透明。

  • 程式碼複用:建立程式碼抽離到獨立的工廠類之後可以複用。

  • 隔離複雜性:封裝複雜的建立邏輯,呼叫者無需瞭解如何建立物件。

  • 控制複雜度:將建立程式碼抽離出來,讓原本的函式或類職責更單一,程式碼更簡潔。

建造者模式

參考建造者模式(Builder)
建造者模式是讓建造者類來負責物件的建立工作。

假設我們定義一個商品類,商品類包含相關的引數,如商品名稱,現價,原價,分類,商品圖片

//定義建造者介面
interface  BuilderInterface  { 
    //建立類物件
    public function createBuilder();
    //設定預設引數
    public function setParameter();
    //獲取類物件
    public function getBuilder();
}
//定義建造者類
class builder{
    public function build(BuilderInterface $builder){
        $this->createBuilder();
        $this->setParameter();
        return $this->getBuilder();
    }
}

//定義商品建造者類
class GoodsBuilder implements BuilderInterface{
    private $goods;
    public function createBuilder(){
        $this->goods = new Goods();
    }

    public function setParameter(){
        $this->goods->setPart('name','test');
        $this->goods->setPart('price','10.00');
        $this->goods->setPart('img','test.jpg');
    }

    public function getBuilder(){
        return $this->goods;
    }
}

//定義商品類並繼承Setting抽象類
class Goods implements Setting{

}

//定義抽象類,用於設定類屬性
abstract class Setting {  
    private  $data  =  [];
    public function setPart($key,  $value){  
        $this->data[$key] = $value;  
    }
}

$goodsBuilder = new GoodsBuilder();
$goods = (new builder())->build($goodsBuilder);

如果存在下面情況中的任意一種,我們就要考慮使用建造者模式了。

  • 我們把類的必填屬性放到建構函式中,強制建立物件的時候就設定。如果必填的屬性有很多,把這些必填屬性都放到建構函式中設定,那建構函式就又會出現引數列表很長的問題。如果我們把必填屬性通過 set() 方法設定,那校驗這些必填屬性是否已經填寫的邏輯就無處安放了。

  • 如果類的屬性之間有一定的依賴關係或者約束條件,我們繼續使用建構函式配合 set() 方法的設計思路,那這些依賴關係或約束條件的校驗邏輯就無處安放了。

  • 如果我們希望建立不可變物件,也就是說,物件在建立好之後,就不能再修改內部的屬性值,要實現這個功能,我們就不能在類中暴露 set() 方法。建構函式配合 set() 方法來設定屬性值的方式就不適用了。

原型模式

如果物件的建立成本比較大,而同一個類的不同物件之間差別不大(大部分欄位都相同),在這種情況下,我們可以利用對已有物件(原型)進行復制(或者叫拷貝)的方式來建立新物件,以達到節省建立時間的目的。這種基於原型來建立物件的方式就叫作原型設計模式(Prototype Design Pattern),簡稱原型模式。

//定義介面原型類
interface Prototype {
    public function copy();
}

//定義商品原型類
class GoodsPrototype implements Prototype{
    private $name;
    __constrcut($name = null){
        $this->name = $name;
    }

    public function setName($name){
        $this->name = $name;
    }

    public function getName(){
        return $this->name;
    }

    public function copy(){
        //深拷貝
        //$serialize_obj = serialize($this);
        //return unserialize($serialize_obj);
        //淺拷貝
        return clone $this;
    }
}
$goods = new GoodsPrototype('test');
$cloneGoods = $goods->copy();

建立型設計模式總結

  • 單例模式用來建立全域性唯一的物件。
  • 工廠模式用來建立不同但是相關型別的物件(繼承同一父類或者介面的一組子類),由給定的引數來決定建立哪種型別的物件。
  • 建造者模式是用來建立複雜物件,可以通過設定不同的可選引數,“定製化”地建立不同的物件。
  • 原型模式針對建立成本比較大的物件,利用對已有物件進行復制的方式進行建立,以達到節省建立時間的目的。

代理模式

代理模式在不改變原始類(或叫被代理類)程式碼的情況下,通過引入代理類來給原始類附加功能。

代理模式最常用的一個應用場景就是,在業務系統中開發一些非功能性需求,比如:監控、統計、鑑權、限流、事務、冪等、日誌。我們將這些附加功能與業務功能解耦,放到代理類中統一處理,讓程式設計師只需要關注業務方面的開發。

以下我定義了一個商品類,create方法表示建立商品的業務邏輯,這時需要在建立商品的時候寫下相關的日誌或者擴充套件其他功能時,在不修改原建立商品業務邏輯的情況下。可以使用代理模式

//定義商品類
class Goods{

    public function create(){
        //新增建立商品業務邏輯
        return $goodsId;
    }
}

//定義商品代理類
class GoodsProxy extends Goods(){

    public function create(){
        parent::create();
        //新增日誌
        Log::info();
    }
}

橋接模式

在 GoF 的《設計模式》一書中,橋接模式被定義為:“將抽象和實現解耦,讓它們可以獨立變化。”在其他資料和書籍中,還有另外一種更加簡單的理解方式:“一個類存在兩個(或多個)獨立變化的維度,我們通過組合的方式,讓這兩個(或多個)維度可以獨立進行擴充套件。”

第一種 GoF 的理解方式,弄懂定義中“抽象”和“實現”兩個概念,是理解它的關鍵。定義中的“抽象”,指的並非“抽象類”或“介面”,而是被抽象出來的一套“類庫”,它只包含骨架程式碼,真正的業務邏輯需要委派給定義中的“實現”來完成。而定義中的“實現”,也並非“介面的實現類”,而是一套獨立的“類庫”。“抽象”和“實現”獨立開發,通過物件之間的組合關係,組裝在一起。

對於第二種理解方式,它非常類似我們之前講過的“組合優於繼承”設計原則,通過組合關係來替代繼承關係,避免繼承層次的指數級爆炸。

在使用laravel進行開發,在開發過程中會應用到佇列,laravel的佇列使用起來非常方便,我們需要選擇佇列的驅動方式,可以選擇database,redis,beanstalkd等相關驅動,但是使用的job類還是一樣的,這裡就應用到了橋接模式

//定義佇列介面
interface JobInterface{
    public function run();
}

//定義資料庫佇列類
class DataBaseJob implements JobInterface{
    public function run(){
        //資料庫驅動執行函式
    }
}

//定義redis佇列類
class RedisJob implements JobInterface{
    public function run(){
        //redis驅動執行函式
    }
}

//定義使用佇列服務抽象類
abstract class JobServer{
    protected $jobRunning;
    public function __constrcut(JobInterface $jobInterface){
        $this->jobRunning = $jobInterface;
    }
    public function setJobRunning(JobInterface $jobInterface){
        $this->jobRunning = $jobInterface;
    }
    public function running();
}

//定義佇列類,通過繼承抽象類獲取具體驅動run()函式
class Job implements JobServer{
    public function running(){
        $this->jobRunning->run();
    }
}

$job = new Job(new DataBaseJob());
$job->running();

裝飾器模式

裝飾器模式主要解決繼承關係過於複雜的問題,通過組合來替代繼承。它主要的作用是給原始類新增增強功能。這也是判斷是否該用裝飾器模式的一個重要的依據。除此之外,裝飾器模式還有一個特點,那就是可以對原始類巢狀使用多個裝飾器。為了滿足這個應用場景,在設計的時候,裝飾器類需要跟原始類繼承相同的抽象類或者介面。

假設在Web應用要在返回響應給使用者之前執行一系列操作,例如驗證使用者及記錄請求等。也許還需要將請求中的原始輸入處理成某種資料結構。最後,還必須執行核心操作。

class RequestHelper{}

//定義抽象基類
abstract class ProcessRequest(){
    abstract function process(RequestHelper $req);
}

//定義一個具體的元件
class MainProcess extends ProcessRequest{
    function process(RequestHelper $req){
        print __class__.":doing something useful with request \n";
    }
}

//定義抽象裝飾類
abstract class DecorateProcess extends ProcessRequest(){
    protected $processRequest;
    function __construct(ProcessRequest $pr){
        $this->processRequest = $pr;
    }
}

//定義具體的日誌裝飾類
class LogRequest extends DecorateProcess(){
    function process(RequestHelper $req){
        print __class__.":logging request \n";
        $this->processRequest->process();
    }
}

//定義具體的驗證裝飾類
class AuthenticateRequest extends DecorateProcess(){
    function process(RequestHelper $req){
        print __class__.":authenticating request \n";
        $this->processRequest->process();
    }
}

$mainProcess = $new MainProcess();
$process = new AuthenticateRequest(new LogRequest($mainProcess));

介面卡模式

介面卡模式是用來做適配,它將不相容的介面轉換為可相容的介面,讓原本由於介面不相容而不能一起工作的類可以一起工作。介面卡模式有兩種實現方式:類介面卡和物件介面卡。其中,類介面卡使用繼承關係來實現,物件介面卡使用組合關係來實現。對於這個模式,有一個經常被拿來解釋它的例子,就是 USB 轉接頭充當介面卡,把兩種不相容的介面,通過轉接變得可以一起工作。

以USB轉接頭為例子

//定義USB介面類
interface USBInterface{
    //USB插頭
    public function plug();
    //輸入電壓
    public function voltage();
}

//定義安卓USB類
class Android implements USBInterface{
    public function plug(){
        return true;
    }
    public function voltage(){
        return '220(V)';
    }
}

//定義IOS介面
interface IOSUsbInterface{
    public function IosPlug();
    public function IosVoltage();
}

//定義IOS類
class IOS implements IOSUsbInterface(){
    public function IosPlug(){
        return true;
    }
    public function IosVoltage(){
        return '200(V)';
    }
}

//定義USB介面卡類,適配USB介面類
class USBAdapter implements USBInterface(){
    private $iosInterface;
    __constrct(IOSUsbInterface $iosUsbInterface){
        $this->iosInterface = $iosUsbInterface;
    }

    public function plug(){
        $this->iosInterface->IosPlug();
        return true;
    }

    public function voltage(){
        $this->iosInterface->iosVoltage();
        return '220(V)';
    }
}

$ios = new IOS();
$adapter = new USBAdapter($ios);
$adapter->plug();

那在實際的開發中,什麼情況下才會出現介面不相容呢?

  • 封裝有缺陷的介面設計

  • 統一多個類的介面設計

  • 替換依賴的外部系統

  • 相容老版本介面

  • 適配不同格式的資料

代理、橋接、裝飾器、介面卡 4 種設計模式的區別

代理、橋接、裝飾器、介面卡,這 4 種模式是比較常用的結構型設計模式。它們的程式碼結構非常相似。籠統來說,它們都可以稱為 Wrapper 模式,也就是通過 Wrapper 類二次封裝原始類。

儘管程式碼結構相似,但這 4 種設計模式的用意完全不同,也就是說要解決的問題、應用場景不同,這也是它們的主要區別。

  • 代理模式:代理模式在不改變原始類介面的條件下,為原始類定義一個代理類,主要目的是控制訪問,而非加強功能,這是它跟裝飾器模式最大的不同。

  • 橋接模式:橋接模式的目的是將介面部分和實現部分分離,從而讓它們可以較為容易、也相對獨立地加以改變。

  • 裝飾器模式:裝飾者模式在不改變原始類介面的情況下,對原始類功能進行增強,並且支援多個裝飾器的巢狀使用。

  • 介面卡模式:介面卡模式是一種事後的補救策略。介面卡提供跟原始類不同的介面,而代理模式、裝飾器模式提供的都是跟原始類相同的介面。

門面模式

門面模式,也叫外觀模式,英文全稱是 Facade Design Pattern。在 GoF 的《設計模式》一書中,門面模式是這樣定義的:

門面模式為子系統提供一組統一的介面,定義一組高層介面讓子系統更易用。

//定義登入介面
interface AuthInterface(){
    public function login();
    public function logout();
}

//定義購物車介面
interface CartInterface(){
    public function getCartItem();
    public function deleteCartItem();
}

//定義登入授權類
class Auth implements AuthInterface(){
    public function login(){
        //登入相關操作
    }

    public function logout(){
        //登出相關操作
    }
}

class Cart implements CartInterface(){
    public function getCartItem(){
        //獲取使用者購物車操作
    }
    public function deleteCartItem(){
        //刪除使用者購物車操作
    }
}

class Facade{
    private $authInterface;
    private $cartInterface;
    public function __construct(AuthInterface $authInterface,CartInterface $cartInterface){
        $this->authInterface = $authInterface;
        $this->cartInterface = $cartInterface;
    }

    public function getUserCart(){
        $this->authInterface->login();
        $this->cartInterface->getCartItem();
    }
}

$facade = new Facade(new Auth(),new Cart());
$facade->getUserCart();

介面粒度設計得太大,太小都不好。太大會導致介面不可複用,太小會導致介面不易用。在實際的開發中,介面的可複用性和易用性需要“微妙”的權衡。針對這個問題,我的一個基本的處理原則是,儘量保持介面的可複用性,但針對特殊情況,允許提供冗餘的門面介面,來提供更易用的介面。

組合模式

在 GoF 的《設計模式》一書中,組合模式是這樣定義的:

將一組物件組織(Compose)成樹形結構,以表示一種“部分 - 整體”的層次結構。組合讓客戶端(在很多設計模式書籍中,“客戶端”代指程式碼的使用者。)可以統一單個物件和組合物件的處理邏輯。

假設需要構建整個公司的人員架構圖(部門、子部門、員工的隸屬關係),並且提供介面計算出部門的薪資成本(隸屬於這個部門的所有員工的薪資和)。

//定義基礎員工介面
interface HumanInterface {
    public function calculSalary();
}

//定義員工類
class Employee implements HumanInterface{
    protected $id;
    protected $salary;
    public function __construct($id,$salary){
        //員工id和員工薪資
        $this->id = $id;
        $this->salary = $salary;
    }

    //計算員工薪資
    public function calculSalary(){
        return $salary;
    }
}

//定義部門類
class Department implements HumanInterface{
    protected $id;
    protected $employee_ids = [];

    public function __construct($id){
        //部門id
        $this->id = $id;
    }

    public function addEmployee(Employee $employee){
        $this->employee_ids[] = $employee;
    }

    //計算部門薪資
    public function calculSalary(){
        $salary = 0;
        foreach($this->employee_ids as $employee){
            $salary += $employee->calculSalary();
        }
        return $salary;
    }
}

$department = new Department(1);
$employee1 = new Employee(1,5000);
$employee2 = new Employee(2,7000);
$employee3 = new Employee(3,10000);
$department->addEmployee($employee1);
$department->addEmployee($employee2);
$department->addEmployee($employee3);
$department->calculSalary();

享元模式

所謂“享元”,顧名思義就是被共享的單元。享元模式的意圖是複用物件,節省記憶體,前提是享元物件是不可變物件。

當一個系統中存在大量重複物件的時候,如果這些重複的物件是不可變物件,我們就可以利用享元模式將物件設計成享元,在記憶體中只保留一份例項,供多處程式碼引用。這樣可以減少記憶體中物件的數量,起到節省記憶體的目的。實際上,不僅僅相同物件可以設計成享元,對於相似物件,我們也可以將這些物件中相同的部分(欄位)提取出來,設計成享元,讓這些大量相似物件引用這些享元。

定義中的“不可變物件”指的是,一旦通過建構函式初始化完成之後,它的狀態(物件的成員變數或者屬性)就不會再被修改了。所以,不可變物件不能暴露任何 set() 等修改內部狀態的方法。之所以要求享元是不可變物件,那是因為它會被多處程式碼共享使用,避免一處程式碼對享元進行了修改,影響到其他使用它的程式碼。

//抽象享元角色
interface Flyweight{
     function show();
}
//共享的具體享元角色
class ConcreteFlyweight implements Flyweight{
    private $state;
    function __construct($state){
        $this->state = $state;
    }
    function show(){
        return $this->state;
    }
}

//享元工廠模式
class FlyweightFactory{
    private $flyweights = array();
    function getFlyweight($state){
        if(!isset($this->flyweights[$state])){
            $this->flyweights[$state]=new ConcreteFlyweight($state);
        }
        return $this->flyweights[$state];
    }
}

$flyweightFactory = new FlyweightFactory(); 
$flyweightOne = $flyweightFactory->getFlyweight("state A");

享元模式的程式碼實現非常簡單,主要是通過工廠模式,在工廠類中,通過一個陣列來快取已經建立好的享元物件,以達到複用的目的。

觀察者模式

觀察者模式(Observer Design Pattern)也被稱為釋出訂閱模式(Publish-Subscribe Design Pattern)。在 GoF 的《設計模式》一書中,它的定義是這樣的:

在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會自動收到通知。

一般情況下,被依賴的物件叫作被觀察者(Observable),依賴的物件叫作觀察者(Observer)。不過,在實際的專案開發中,這兩種物件的稱呼是比較靈活的,有各種不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎麼稱呼,只要應用場景符合剛剛給出的定義,都可以看作觀察者模式。

對於PHP來說,PHP內建提供了兩個介面來供外部應用區實現這個模式。

SplSubject 介面,它代表著被觀察的物件,其結構:
interface SplSubject{
    publicfunction attach(SplObserver $observer);

    publicfunction detach(SplObserver $observer);

    publicfunction notify();
}


SplObserver 介面,它代表著充當觀察者的物件,其結構:
interface SplObserver{
  public function update(SplSubject $subject);

}

//定義被觀察者類
class subject implements SplSubject{
    private $observers , $value;
    public function __construct(){
        $this->observers =array();  
    }

    public function attach(SplObserver $observer){
        $this->observers[] = $observer;
    }

    public function detach(SplObserver $observer){
        if($idx = array_search($observer, $this->observers,true)) {
            unset($this->observers[$idx]);    
        }
    }

    public function notify(){
        foreach($this->observers as $observer){
             $observer->update($this);
        }
    }
    public function setValue($value){
        $this->value = $value;
        $this->notify();
    }
    public function getValue(){
        return$this->value;     
    }
}

//定義觀察類
class observer implements SplObserver{
    public function update(SplSubject $subject){
        echo 'The new state ofsubject'.$subject->getValue();
    }   
}

$subject = new subject();
$observer = new observer();
$subject->attach($observer);
$subject->setValue(5);

模板模式

模板模式,全稱是模板方法設計模式,英文是 Template Method Design Pattern。在 GoF 的《設計模式》一書中,它是這麼定義的:

模板方法模式在一個方法中定義一個演算法骨架,並將某些步驟推遲到子類中實現。模板方法模式可以讓子類在不改變演算法整體結構的情況下,重新定義演算法中的某些步驟。

這裡的“演算法”,我們可以理解為廣義上的“業務邏輯”,並不特指資料結構和演算法中的“演算法”。這裡的演算法骨架就是“模板”,包含演算法骨架的方法就是“模板方法”,這也是模板方法模式名字的由來。

//定義模版抽象類
abstract class template{

    final public function templateMethod(){
        $this->method1();
        $this->method2();
    }
    //繼承類必須實現該方法
    abstract protected function method1();

    abstract protected function method2();
}

//定義子類繼承抽象模版類
class ConcreteClass extends template(){
    //實現模版類的方法
    protected function method1(){
        //do something
    }
    protected function method2(){
        //do something
    }
}
$concreate = new ConcreteClass();
$concreate->templateMethod();

模板模式有兩大作用:複用和擴充套件。其中,複用指的是,所有的子類可以複用父類中提供的模板方法的程式碼。擴充套件指的是,框架通過模板模式提供功能擴充套件點,讓框架使用者可以在不修改框架原始碼的情況下,基於擴充套件點定製化框架的功能。

策略模式

策略模式,英文全稱是 Strategy Design Pattern。在 GoF 的《設計模式》一書中,它是這樣定義的:

定義一組演算法類,將每個演算法分別封裝起來,讓它們可以互相替換。策略模式可以使演算法的變化獨立於使用它們的客戶端(這裡的客戶端代指使用演算法的程式碼)。

//定義策略介面
interface Strategy{
    function AlgorithmInterface();
}

//定義A策略演算法類
class ConcreteStrategyA implements Strategy{
    function AlgorithmInterface(){
        echo "演算法A";
    }
}

//定義B策略演算法類
class ConcreteStrategyB implements Strategy{
    function AlgorithmInterface(){
        echo "演算法B";
    }
}

//定義C策略演算法類
class ConcreteStrategyC implements Strategy{
    function AlgorithmInterface(){
        echo "演算法C";
    }
}

//定義執行環境上下文。
class Context{
    private $strategy;
    function __construct(Strategy $s){
        $this->strategy = $s;
    }
    function ContextInterface(){

        $this->strategy->AlgorithmInterface();
    }
}
$strategyA = new ConcreteStrategyA();
$context = new Context($strategyA);
$context->ContextInterface();

這個模式和簡單工廠非常類似,那麼他們的區別呢?

  • 工廠相關的模式屬於建立型模式,顧名思義,這種模式是用來建立物件的,返回的是new出來的物件。要呼叫物件的什麼方法是由客戶端來決定的
  • 而策略模式屬性行為型模式,通過執行上下文,將要呼叫的函式方法封裝了起來,客戶端只需要呼叫執行上下文的方法就可以了

策略模式主要的作用還是解耦策略的定義、建立和使用,控制程式碼的複雜度,讓每個部分都不至於過於複雜、程式碼量過多。除此之外,對於複雜程式碼來說,策略模式還能讓其滿足開閉原則,新增新策略的時候,最小化、集中化程式碼改動,減少引入 bug 的風險。

責任鏈模式

責任鏈模式的英文翻譯是 Chain Of Responsibility Design Pattern。在 GoF 的《設計模式》中,它是這麼定義的:

將請求的傳送和接收解耦,讓多個接收物件都有機會處理這個請求。將這些接收物件串成一條鏈,並沿著這條鏈傳遞這個請求,直到鏈上的某個接收物件能夠處理它為止。

在責任鏈模式中,多個處理器(也就是剛剛定義中說的“接收物件”)依次處理同一個請求。一個請求先經過 A 處理器處理,然後再把請求傳遞給 B 處理器,B 處理器處理完後再傳遞給 C 處理器,以此類推,形成一個鏈條。鏈條上的每個處理器各自承擔各自的處理職責,所以叫作責任鏈模式。

//定義基礎責任鏈抽象類
abstract class Handler
{
    protected $successor;
    public function setSuccessor($successor)
    {
        $this->successor = $successor;
    }
    abstract public function HandleRequst($request);
}

//三個責任鏈條的具體實現,主要功能是判斷傳入的資料型別,如果是數字由第一個類處理,如果是字串,則第二個類處理。如果是其他型別,第三個類統一處理。
class ConcreteHandler1 extends Handler
{
    public function HandleRequst($request)
    {
        if (is_numeric($request)) {
            return '請求引數是數字:' . $request;
        } else {
            return $this->successor->HandleRequst($request);
        }
    }
}

class ConcreteHandler2 extends Handler
{
    public function HandleRequst($request)
    {
        if (is_string($request)) {
            return '請求引數是字串:' . $request;
        } else {
            return $this->successor->HandleRequst($request);
        }
    }
}

class ConcreteHandler3 extends Handler
{
    public function HandleRequst($request)
    {
        return '我也不知道請求引數是啥了,你猜猜?' . gettype($request);
    }
}

$handle1 = new ConcreteHandler1();
$handle2 = new ConcreteHandler2();
$handle3 = new ConcreteHandler3();

$handle1->setSuccessor($handle2);
$handle2->setSuccessor($handle3);

$requests = [22, 'aaa', 55, 'cc', [1, 2, 3], null, new stdClass];

foreach ($requests as $request) {
    echo $handle1->HandleRequst($request) . PHP_EOL;
}
  • 責任鏈非常適合的一種場景,就是對請求引數進行逐層過濾,就像我們工作時使用釘釘之類的辦公軟體。當需要提加班或者休假申請時,那一層層的審批流程就是對這個模式最完美的解釋
  • 我們可以攔截請求,直接返回,也可以對請求內容進行完善修改交給下一個類來進行處理,但至少有一個類是要返回結果的。
  • 請求不一定都會被處理,也有可能完全不處理就返回或者傳遞給下一個處理類來進行處理

狀態模式

參考PHP設計模式——狀態模式

狀態模式當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類。狀態模式主要解決的是當控制一個物件狀態的條件表示式過於複雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類中,可以把複雜的判斷邏輯簡化。


//狀態介面
interface IState
{
    function WriteCode(Work $w);
}

//上午工作狀態
class AmState implements IState
{
    public function WriteCode(Work $w)
    {
        if($w->hour<=12)
        {
            echo "當前時間:{$w->hour}點,上午工作;犯困,午休。<br/>";
        }
        else
        {
            $w->SetState(new PmState());
            $w->WriteCode();
        }
    }
}

//下午工作狀態
class PmState implements IState
{
    public function WriteCode(Work $w)
    {
        if($w->hour<=17)
        {
            echo "當前時間:{$w->hour}點,下午工作狀態還不錯,繼續努力。<br/>";
        }
        else
        {
            $w->SetState(new NightState());
            $w->WriteCode();
        }
    }
}

//晚上工作狀態
class NightState implements IState
{

    public function WriteCode(Work $w)
    {
        if($w->IsDone)
        {
            $w->SetState(new BreakState());
            $w->WriteCode();
        }
        else
        {
            if($w->hour<=21)
            {
                echo "當前時間:{$w->hour}點,加班哦,疲累至極。<br/>";
            }
            else
            {
                $w->SetState(new SleepState());
                $w->WriteCode();
            }
        }
    }
}

//休息狀態
class BreakState implements IState
{

    public function WriteCode(Work $w)
    {
        echo "當前時間:{$w->hour}點,下班回家了。<br/>";
    }
}

//睡眠狀態
class SleepState implements IState
{

    public function WriteCode(Work $w)
    {
        echo "當前時間:{$w->hour}點,不行了,睡著了。<br/>";
    }
}

//工作狀態
class Work
{
    private $current;
    public function Work()
    {
        $this->current = new AmState();
    }

    public $hour;

    public $isDone;

    public function SetState(IState $s)
    {
        $this->current = $s;
    }

    public function WriteCode()
    {
        $this->current->WriteCode($this);
    }
}

$emergWork = new Work();
$emergWork->hour = 9;
$emergWork->WriteCode();
$emergWork->hour = 10;
$emergWork->WriteCode();
$emergWork->hour = 13;
$emergWork->WriteCode();
$emergWork->hour=14;
$emergWork->WriteCode();
$emergWork->hour = 17;
$emergWork->WriteCode();

$emergWork->IsDone = true;
$emergWork->IsDone = false;

$emergWork->hour = 19;
$emergWork->WriteCode();

$emergWork->hour = 22;
$emergWork->WriteCode();

適用場景:

1.一個物件的行為取決於它的狀態,並且它必須在執行時刻根據狀態改變它的行為。

2.一個操作中含有龐大的多分支結構,並且這些分支決定於物件的狀態。

迭代器模式

參考PHP 迭代器模式

迭代器:類繼承PHP的Iterator介面,批量操作。
1. 迭代器模式,在不需要了解內部實現的前提下,遍歷一個聚合物件的內部元素。
2. 相比傳統的程式設計模式,迭代器模式可以隱藏遍歷元素的所需操作。

介面Iterator

  • current() 返回當前元素
  • key() 返回當前元素的鍵
  • next() 向前移動到下一個元素
  • rewind() 返回到迭代器的第一個元素
class AllUser implements \Iterator
{
    protected $index = 0;
    protected $data = [];

    public function __construct()
    {
        $link = mysqli_connect('192.168.0.91', 'root', '123', 'xxx');
        $rec = mysqli_query($link, 'select id from doc_admin');
        $this->data = mysqli_fetch_all($rec, MYSQLI_ASSOC);
    }

    //1 重置迭代器
    public function rewind()
    {
        $this->index = 0;
    }
xxx
    //2 驗證迭代器是否有資料
    public function valid()
    {
        return $this->index < count($this->data);
    }

    //3 獲取當前內容
    public function current()
    {
        $id = $this->data[$this->index];
        return User::find($id);
    }

    //4 移動key到下一個
    public function next()
    {
        return $this->index++;
    }


    //5 迭代器位置key
    public function key()
    {
        return $this->index;
    }
}

//實現迭代遍歷使用者表
$users = new AllUser();
//可實時修改
foreach ($users as $user){
    $user->add_time = time();
    $user->save();
}
  • 迭代器模式封裝集合內部的複雜資料結構,開發者不需要了解如何遍歷,直接使用容器提供的迭代器即可;

  • 迭代器模式將集合物件的遍歷操作從集合類中拆分出來,放到迭代器類中,讓兩者的職責更加單一;

  • 迭代器模式讓新增新的遍歷演算法更加容易,更符合開閉原則。除此之外,因為迭代器都實現自相同的介面,在開發中,基於介面而非實現程式設計,替換迭代器也變得更加容易。

訪問者模式

訪問者模式的英文翻譯是 Visitor Design Pattern。在 GoF 的《設計模式》一書中,它是這麼定義的:

允許一個或者多個操作應用到一組物件上,解耦操作和物件本身。

案例參考PHP設計模式——訪問者模式

/*男人這本書的內容要比封面吸引人;女人這本書的封面通常比內容更吸引人
男人成功時,背後多半有一個偉大的女人;女人成功時,背後多半有一個失敗的男人
男人失敗時,悶頭喝酒,誰也不用勸;女人失敗時,眼淚汪汪,誰也勸不了
男人戀愛時,凡事不懂也要裝懂;女人戀愛時,遇事懂也要裝作不懂*/
//抽象狀態
abstract class State
{
    protected $state_name;

    //得到男人反應
    public abstract function GetManAction(VMan $elementM);
    //得到女人反應
    public abstract function GetWomanAction(VWoman $elementW);
}

//抽象人
abstract class Person
{
    public $type_name;

    public abstract function Accept(State $visitor);
}

//成功狀態
class Success extends State
{
    public function __construct()
    {
        $this->state_name="成功";
    }

    public  function GetManAction(VMan $elementM)
    {
        echo "{$elementM->type_name}:{$this->state_name}時,背後多半有一個偉大的女人。<br/>";
    }

    public  function GetWomanAction(VWoman $elementW)
    {
        echo "{$elementW->type_name} :{$this->state_name}時,背後大多有一個不成功的男人。<br/>";
    }
}

//失敗狀態
class Failure extends State
{
    public function __construct()
    {
        $this->state_name="失敗";
    }

    public  function GetManAction(VMan $elementM)
    {
        echo "{$elementM->type_name}:{$this->state_name}時,悶頭喝酒,誰也不用勸。<br/>";
    }

    public  function GetWomanAction(VWoman $elementW)
    {
        echo "{$elementW->type_name} :{$this->state_name}時,眼淚汪汪,誰也勸不了。<br/>";
    }
}

//戀愛狀態
class Amativeness  extends State
{
    public function __construct()
    {
        $this->state_name="戀愛";
    }

    public  function GetManAction(VMan $elementM)
    {
        echo "{$elementM->type_name}:{$this->state_name}時,凡事不懂也要裝懂。<br/>";
    }

    public  function GetWomanAction(VWoman $elementW)
    {
        echo "{$elementW->type_name} :{$this->state_name}時,遇事懂也要裝作不懂。<br/>";
    }
}

//男人
class VMan extends Person
{
    function __construct()
    {
        $this->type_name="男人";
    }

    public  function Accept(State $visitor)
    {
        $visitor->GetManAction($this);
    }
}

//女人
class VWoman extends Person
{
    public function __construct()
    {
        $this->type_name="女人";
    }

    public  function Accept(State $visitor)
    {
        $visitor->GetWomanAction($this);
    }
}

//物件結構
class ObjectStruct
{
    private $elements=array();
    //增加
    public function Add(Person $element)
    {
        array_push($this->elements,$element);
    }
    //移除
    public function Remove(Person $element)
    {
        foreach($this->elements as $k=>$v)
        {
            if($v==$element)
            {
                unset($this->elements[$k]);
            }
        }
    }

    //檢視顯示
    public function Display(State $visitor)
    {
        foreach ($this->elements as $v)
        {
            $v->Accept($visitor);
        }
    }
}

$os = new ObjectStruct();
$os->Add(new VMan());
$os->Add(new VWoman());

//成功時反應
$ss = new Success();
$os->Display($ss);

//失敗時反應
$fs = new Failure();
$os->Display($fs);

//戀愛時反應
$ats=new Amativeness();
$os->Display($ats);

備忘錄模式

備忘錄模式,也叫快照(Snapshot)模式,英文翻譯是 Memento Design Pattern。在 GoF 的《設計模式》一書中,備忘錄模式是這麼定義的:

在不違背封裝原則的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,以便之後恢復物件為先前的狀態。

編寫一個小程式,可以接收命令列的輸入。使用者輸入文字時,程式將其追加儲存在記憶體文字中;使用者輸入“:list”,程式在命令列中輸出記憶體文字的內容;使用者輸入“:undo”,程式會撤銷上一次輸入的文字,也就是從記憶體文字中將上次輸入的文字刪除掉。

如下所示:

>hello
>:list
hello
>world
>:list
helloworld
>:undo
>:list
hello
//定義文字輸入類
public class InputText{
    private $text;
    //獲取文字資訊
    public function getText(){
        return $this->text;
    }
    //追加文字資訊
    public function appendText($string){
        $this->text = $this->text.$string
    }
    //建立快照
    public function createSnapshot(){
        return new Snapshot($this->text);
    }
    //重置為快照資料
    public function restoreSnapshot(Snapshot $snapshot){
        $this->text = $snapshot->getText();
    }
}

//定義快照類
public class Snapshot{

    private String text;

    public function __construct($text)  {
        this.text = text;
    }
    //返回快照資訊
    public function getText()  {
        return this.text;
    }
}
//定義快照管理類
public class SnapshotHolder{
    private $snapshots = array();
    //返回最新快照
    public function popSnapshot(){
        return array_pop($this->snapshots);
    }
    //新增快照
    public function pushSnapshot(Snapshot $snapshot){
        array_push($this->snapshots,$snapshot);
    }
}

$inputText = new InputText();
$snapshotHolder = new SnapshotHolder();

while(讀取終端輸入){
    //$line為終端輸入值
    if($line == ':list'){
        echo $inputText->getText;
    }else if($line == ':undo'){
        $snapshot = $snapshotHolder->popSnapshot();
        $inputText->restoreSnapshot($snapshot);
    }else{
        $snapshotHolder->pushSnapshot($inputText->createSnapshot());
        $inputText->appendText($line);
    }
}

備忘錄模式也叫快照模式,具體來說,就是在不違背封裝原則的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,以便之後恢復物件為先前的狀態。這個模式的定義表達了兩部分內容:一部分是,儲存副本以便後期恢復;另一部分是,要在不違背封裝原則的前提下,進行物件的備份和恢復。

備忘錄模式的應用場景也比較明確和有限,主要是用來防丟失、撤銷、恢復等。它跟平時我們常說的“備份”很相似。兩者的主要區別在於,備忘錄模式更側重於程式碼的設計和實現,備份更側重架構設計或產品設計。

命令模式

命令模式的英文翻譯是 Command Design Pattern。在 GoF 的《設計模式》一書中,它是這麼定義的:

命令模式將請求(命令)封裝為一個物件,這樣可以使用不同的請求引數化其他物件(將不同請求依賴注入到其他物件),並且能夠支援請求(命令)的排隊執行、記錄日誌、撤銷等(附加控制)功能。

落實到編碼實現,命令模式用的最核心的實現手段,是將函式封裝成物件。當我們把函式封裝成物件之後,物件就可以儲存下來,方便控制執行。所以,命令模式的主要作用和應用場景,是用來控制命令的執行,比如,非同步、延遲、排隊執行命令、撤銷重做命令、儲存命令、給命令記錄日誌等等,這才是命令模式能發揮獨一無二作用的地方。

//定義執行命令介面
interface ICommand {
    function onCommand($name, $args);
}

//定義命令執行排程類
class CommandChain {
    private $_commands = array();
    public function addCommand($cmd) {
        $this->_commands []= $cmd;
    }

    public function runCommand($name, $args) {
         foreach($this->_commands as $cmd) {
             if ($cmd->onCommand($name, $args)) return;
         }
     }
 }

//定義User命令類
class UserCommand implements ICommand {
     public function onCommand($name, $args) {
         if ($name != 'addUser') return false;
         echo("UserCommand handling 'addUser'\n");
         return true;
     }
}
//定義郵件類
class MailCommand implements ICommand {
    public function onCommand($name, $args) {
        if ($name != 'mail') return false;
         echo("MailCommand handling 'mail'\n");
         return true;
     }
}

$cc = new CommandChain();
$cc->addCommand(new UserCommand());
$cc->addCommand(new MailCommand());
$cc->runCommand('addUser', null);
$cc->runCommand('mail', null);

中介模式

中介模式的英文翻譯是 Mediator Design Pattern。在 GoF 中的《設計模式》一書中,它是這樣定義的:

中介模式定義了一個單獨的(中介)物件,來封裝一組物件之間的互動。將這組物件之間的互動委派給與中介物件互動,來避免物件之間的直接互動。

參考PHP設計模式——中介者模式

//中介者介面:可以是公共的方法,如Change方法,也可以是小範圍的互動方法。
//同事類定義:比如,每個具體同事類都應該知道中介者物件,也就是每個同事物件都會持有中介者物件的引用,這個功能可定義在這個類中。

//抽象國家
abstract class Country
{
    protected $mediator;
    public function __construct(UnitedNations $_mediator)
    {
        $this->mediator = $_mediator;
    }
}

//具體國家類
//美國
class USA extends Country
{
    function __construct(UnitedNations $mediator)
    {
        parent::__construct($mediator);
    }

    //宣告
    public function Declared($message)
    {
        $this->mediator->Declared($message,$this);
    }

    //獲得訊息
    public function GetMessage($message)
    {
        echo "美國獲得對方訊息:$message<br/>";
    }
}
//中國
class China extends Country
{
    public function __construct(UnitedNations $mediator)
    {
        parent::__construct($mediator);
    }
    //宣告
    public function  Declared($message)
    {
        $this->mediator->Declared($message, $this);
    }

    //獲得訊息
    public function GetMessage($message)
    {
        echo "中方獲得對方訊息:$message<br/>";
    }
}

//抽象中介者
//抽象聯合國機構
abstract class UnitedNations
{
    //宣告
    public abstract function Declared($message,Country $colleague);
}

//聯合國機構
class UnitedCommit extends UnitedNations
{
    public $countryUsa;
    public $countryChina;

    //宣告
    public function Declared($message, Country $colleague)
    {
        if($colleague==$this->countryChina)
        {
            $this->countryUsa->GetMessage($message);
        }
        else
        {
            $this->countryChina->GetMessage($message);
        }
    }
}

$UNSC = new UnitedCommit();
$c1 = new USA($UNSC);
$c2 = new China($UNSC);
$UNSC->countryChina = $c2;
$UNSC->countryUsa =$c1;
$c1->Declared("姚明的籃球打的就是好");
$c2->Declared("謝謝。");
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章