設計模式初探

Neilyozの魚不浪發表於2020-04-17

前言

設計模式作為一個程式設計師,相信大家肯定不會陌生,它是一些成熟的且通用的程式設計解決方案,針對這些肯定會存在一些理論基礎,來為這些這些模式提供理論依據,這裡我們就要先搞明白這些理論到底是什麼,這樣我們對設計模式有事半功倍的效果。

設計模式的基本原則:
1. 單一職責原則
2. 開閉原則
3. 裡式替換原則
4. 依賴倒轉原則
5. 介面隔離原則
6. 合成複用原則
7. 迪米特法則

單一職責原則

定義:規定每個類都應該有一個單一的功能,並且該功能應該由這個類完全封裝起來。
作用:它用於控制類的粒度大小
這個原則很好理解,我們的類在做某些事情的時候只專注與自己領域內的事兒就可以了,譬如我們的模型類就只針對特定模型進行操作,而不會去關心操作類裡面的邏輯,這樣單一的職責隔離,可以方便我們維護。
舉個理想化例子:
現實生活中,我們的攝影師是什麼都乾的,佈景、服裝、燈光、拍照,可以說是累成狗。
但是在程式設計的世界裡面,我們更加希望的是這樣:

  • 我們的攝影師主要負責就是控制相機,指揮助手。
  • 指導助手佈景,而具體的佈景、服裝和燈光佈置可以交給我們助手。

我們用程式碼模擬下現實生活:

<?php
declare(strict_types=1);
namespace  Neilyoz\DesignPatternBase\SingleResponsibility;
class  RealPhotographer
{
    private  string  $name;
    public  function  __construct(string  $name)
    {
        $this->name  =  $name;
    }

    // 溝通
    public  function  communicate():  void
    {
        echo  $this->name  .  ' 在溝通'  .  PHP_EOL;
    }

    // 佈置場景
    public  function  layout()
    {
        echo  $this->name  .  ' 在佈置場景'  .  PHP_EOL;
    }

    // 搭配服裝
    public  function  matchingClothing()
    {
        echo  $this->name  .  ' 搭配衣服'  .  PHP_EOL;
    }

    // 調整燈光
    public  function  adjustTheLights()
    {
        echo  $this->name  .  ' 調整燈光'  .  PHP_EOL;
    }

    // 控制相機
    public  function  controlCamera()
    {
        echo  $this->name  .  ' 控制相機'  .  PHP_EOL;
    }
}

我們可以看到一個攝影師負責了方方面面,俗話就是管的太寬了,我們要縮小粒度,讓我們的攝影師只是專注拍照的本質,所以我們可以把 佈置場景搭配服裝調整燈光 交給助手去完成。我們來修改下這個方法,讓特定的事兒給專業的人去完成。

我們修改下這個類:

<?php
declare(strict_types=1);
namespace  Neilyoz\DesignPatternBase\SingleResponsibility;

// 攝影師類
class  Photographer
{
    private  string  $name;
    public  function  __construct(string  $name)
    {
        $this->name  =  $name;
    }

    // 控制相機
    public  function  controlCamera()
    {
        echo  $this->name  .  " 操作控制相機拍照"  .  PHP_EOL;
    }

    // 指揮助手
    public  function  commandAssistant(Helper  $helper)
    {
        $helper->receivedCommand();
        $helper->matchingClothing();
        $helper->adjustTheLights();
        $helper->layout();
    }
}

// 助手類
class  Helper
{
    private  string  $name;
    public  function  __construct(string  $name)
    {
        $this->name  =  $name;
    }

    public  function  receivedCommand()
    {
        echo  $this->name  .  '收到指揮'  .  PHP_EOL;
    }

    // 佈置場景
    public  function  layout()
    {
        echo  $this->name  .  ' 在佈置場景'  .  PHP_EOL;
    }

    // 搭配服裝
    public  function  matchingClothing()
    {
        echo  $this->name  .  ' 搭配衣服'  .  PHP_EOL;
    }

    // 調整燈光
    public  function  adjustTheLights()
    {
        echo  $this->name  .  ' 調整燈光'  .  PHP_EOL;
    }
}

這樣我們就把職責更加明確的分配了,其實程式設計師有時候更像是一個管理者的覺得,我們需要管理具體的類去做具體的事兒。在管理這些類的時候,我們要合理的劃分這些類的職責,否則職責到後面越來越混亂,反而影響我們的管理。所以單一職責讓我們能更好的控制類的粒度。

開閉原則

定義:一個軟體實體應當對擴充套件開放,對修改關閉。
我們的軟體隨著時間推移是會發生一些變化的,但是已有的程式碼已經是穩定執行的,我們不應該去修改這些成熟的程式碼擴充套件他們的功能,除非逼不得已。所以這就考驗到我們的設計水平了。
我們還是看看下面這個場景:

  • 攝影師工作時不只是只用一個牌子的相機,不同的廠商的相機,有不同的效果

我們來看看我們通常專注於實現的程式碼是什麼樣子的:

<?php
declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\OpenAndClose;

class  Photographer
{
    private  string  $name;
    public  function  __construct(string  $name)
    {
        $this->name  =  $name;
    }

    public  function  photograph(string  $camera)
    {
        switch ($camera) {
            case  '佳能':
                echo  $this->name  .  ' 使用佳能拍'  .  PHP_EOL;
                break;
            case  '尼康':
                echo  $this->name  .  ' 使用尼康拍'  .  PHP_EOL;
                break;
            case  '索尼':
                echo  $this->name  .  ' 使用索尼拍'  .  PHP_EOL;
                break;
            default:
                echo  $this->name  .  ' 使用手機拍'  .  PHP_EOL;
                break;
        }
    }
}

這裡程式碼看著是實現了我們的需求,但是如果客戶要求用 哈蘇賓得富士拍照呢?你是不是要去修改 photograph 這個方法。這樣就違背了我們的開閉原則。那我們要怎麼才能不修改程式碼的情況下,去完成我們的進擊的需求呢?我們可以這樣改:

<?php
declare(strict_types=1);
namespace  Neilyoz\DesignPatternBase\OpenAndClose;
interface  Camera
{
    function  photograph():  string;
}

class  CannonCamera  implements  Camera
{
    public  function  photograph():  string
    {
        return  ' 使用佳能拍'  .  PHP_EOL;
    }
}

class  NikonCamera  implements  Camera
{
    public  function  photograph():  string
    {
        return  ' 使用尼康拍'  .  PHP_EOL;
    }
}

class  SonyCamera  implements  Camera
{
    public  function  photograph():  string
    {
        return  ' 使用索尼拍'  .  PHP_EOL;
    }
}

class  FujiCamera  implements  Camera
{
    public  function  photograph():  string
    {
        return  ' 使用富士拍'  .  PHP_EOL;
    }
}

class  Photographer
{
    private  string  $name;

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

    public  function  photograph(Camera  $camera):  void
    {
        echo  $this->name  .  $camera->photograph();
    }
}

$photographer  =  new  Photographer("魚不浪");
$photographer->photograph(new  CannonCamera());
$photographer->photograph(new  NikonCamera());
$photographer->photograph(new  SonyCamera());
$photographer->photograph(new  FujiCamera());

我們可以使用介面把相機的拍照功能抽象出來,這樣即使是有新的相機進來,我們無非就是實現這個介面就能達到擴充套件的目的,而不需要去修改我們的現有程式碼。

裡式替換原則

定義:所有引用基類的地方必須透明地使用其子類的物件。
使用裡式替換原則時需要注意如下:

  • 子類的所有方法必須在父類中宣告,或子類必須實現父類中宣告的所有方法。根據里氏代換原則,為了保證系統的擴充套件性,在程式中通常使用父類來進行定義,如果一個方法只存在子類中,在父類中不提供相應的宣告,則無法在以父類定義的物件中使用該方法。
  • 我們在運用里氏代換原則時,儘量把父類設計為抽象類或者介面,讓子類繼承父類或實現父介面,並實現在父類中宣告的方法,執行時,子類例項替換父類例項,我們可以很方便地擴充套件系統的功能,同時無須修改原有子類的程式碼,增加新的功能可以透過增加一個新的子類來實現。里氏代換原則是開閉原則的具體實現手段之一。

其實還是一個抽象的概念,我們還是拿攝影師來說:

  • 攝影師拿相機,至於什麼牌子的相機我們不管,我們只是抽象相機這個概念
<?php
declare(strict_types=1);
namespace  Neilyoz\DesignPatternBase\Liskv;

abstract  class  Camera
{
    public  function  open()
    {
        echo  "相機開機"  .  PHP_EOL;
    }

    public  abstract  function  screen():  void;
}

class  CannonCamera  extends  Camera
{
    public  function  screen():  void
    {
        echo  "佳能拍照"  .  PHP_EOL;
    }
}

class  NikonCamera  extends  Camera
{
    public  function  screen():  void
    {
        echo  "尼康拍照"  .  PHP_EOL;
    }
}

class  Photographer
{
    /**
    * 這裡引數是父類,我們可以傳入子類
    * @param  Camera $camera
    */
    public  function  screen(Camera  $camera)
    {
        $camera->open();
        $camera->screen();
    }
}

$photographer  =  new  Photographer();
$photographer->screen(new  CannonCamera());
$photographer->screen(new  NikonCamera());

依賴倒轉原則

定義:抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對介面程式設計,而不是針對實現程式設計。

這個我們在開閉原則中已經給出了例項,我們就是針對上層抽象進行的程式設計。

我們來看看常用的三種注入方式:

  • 構造注入
  • 設值注入
  • 介面傳遞注入

我們挨個看,為了省事兒我就只用一個類來演示:

<?php
declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\Dependency;

interface  Camera
{
    public  function  open():  void;
    public  function  screen():  void;
}

// 實現相機介面

class  CannonCamera  implements  Camera
{
    public  function  open():  void
    {
        echo  "開啟佳能相機"  .  PHP_EOL;
    }

    public  function  screen():  void
    {
        echo  "佳能相機拍照"  .  PHP_EOL;
    }
}

// 實現相機介面
class  NikonCamera  implements  Camera
{
    public  function  open():  void
    {
        echo  "開啟尼康相機"  .  PHP_EOL;
    }

    public  function  screen():  void
    {
        echo  "尼康相機拍照"  .  PHP_EOL;
    }
}

class  Photographer
{
    private  Camera  $camera;

    // 使用構造注入
    public  function  __construct(Camera  $camera)
    {
        $this->camera  =  $camera;
    }

    /**
    * 使用設值注入
    * @param  Camera $camera
    */
    public  function  setCamera(Camera  $camera):  void
    {
        $this->camera  =  $camera;
    }

    /**
    * 使用介面傳遞注入
    * @param  Camera $camera
    */
    public  function  open(Camera  $camera):  void
    {
        $camera->open();
    }

    public  function  screen()
    {
        $this->open($this->camera);
        $this->camera->screen();
    }
}

// 我們攝影師本來有自己佳能相機
$photographer  =  new  Photographer(new  CannonCamera());

// 並用它進行拍照
$photographer->screen();

// 朋友帶著尼康相機來了,他拿朋友的尼康相機來玩兒
$photographer->setCamera(new  NikonCamera());
$photographer->screen();

介面隔離原則

定義:

  • 客戶端不應該依賴它不需要的介面。
  • 類間的依賴關係應該建立在最小的介面上。

我們透過上面的例子,發現介面真的是一個好東西,可以讓我們解耦很多我們的程式。

還是攝影師的例子,不同攝影師有不同的行為,我們不排除有的攝影師啥都 OK,有的攝影師也缺乏一些能力。

  • 攝影師可以拍攝表達作品想法
  • 攝影師也要和客戶溝通,但有的攝影師只是工具人,不需要溝通
  • 有的攝影師自己重洗膠片,有的直接用數碼

對於這些介面我們的攝影師不用都實現,實現自己需要的介面就好了。


<?php
declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\InterfaceSegregation;

interface  Screen
{
    public  function  screen();
}

interface  Communicate
{
    public  function  communicate();
}

interface  RinseTheFilm
{
    public  function  rinseTheFilm();
}

class  PhotographerOne  implements  Screen, Communicate
{
    public  function  screen()
    {
        echo  "PhotographerOne 拍照"  .  PHP_EOL;
    }

    public  function  communicate()
    {
        echo  "PhotographerOne 溝通"  .  PHP_EOL;
    }
}

class  PhotographerTwo  implements  Screen, Communicate, RinseTheFilm
{
    public  function  screen()
    {
        echo  "PhotographerTwo 拍照"  .  PHP_EOL;
    }

    public  function  communicate()
    {
        echo  "PhotographerTwo 溝通"  .  PHP_EOL;
    }

    public  function  rinseTheFilm()
    {
        echo  "PhotographerTwo 沖洗照片了"  .  PHP_EOL;
    }
}

合成複用原則

定義:儘量使用合成、聚合的方式,而不是使用繼承

這裡有很多種情況,還是用程式碼來舉例說明,我們在有時候在開發中,有時候類可能會用到別的類的方法,我們可以有繼承,依賴,聚合,組合的關係。繼承會導致我們的程式耦合度過高,所以我們會選擇另外的三種方式:

  • 依賴
<?php

declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\Composite;

class  CompositeParent
{
    public  function  method1()
    {
        echo  "方法1"  .  PHP_EOL;
    }

    public  function  method2()
    {
        echo  "方法2"  .  PHP_EOL;
    }

    public  function  method3()
    {
        echo  "方法3"  .  PHP_EOL;
    }
}

class  CompositeChild
{
    public  function  method1(CompositeParent  $compositeParent)
    {
        $compositeParent->method1();
    }
}
  • 聚合

<?php
declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\Composite;

class  CompositeParent
{
    public  function  method1()
    {
        echo  "方法1"  .  PHP_EOL;
    }

    public  function  method2()
    {
        echo  "方法2"  .  PHP_EOL;
    }

    public  function  method3()
    {
        echo  "方法3"  .  PHP_EOL;
    }
}

class  CompositeChild
{
    private  CompositeParent  $compositeParent;
    public  function  setCompositeParent(CompositeParent  $compositeParent):  void
    {
        $this->compositeParent  =  $compositeParent;
    }
}
  • 組合
<?php

declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\Composite;

class  CompositeParent
{
    public  function  method1()
    {
        echo  "方法1"  .  PHP_EOL;
    }

    public  function  method2()
    {
        echo  "方法2"  .  PHP_EOL;
    }

    public  function  method3()
    {
        echo  "方法3"  .  PHP_EOL;
    }
}

class  CompositeChild
{
    private  CompositeParent  $compositeParent;
    public  function  __construct()
    {
        $this->compositeParent  =  new  CompositeParent();
    }
}

迪米特法則

定義:

  • 一個物件應該對其他物件保持最少的瞭解
  • 類與類關係越密切,耦合度越大
  • 一個類對自己依賴的類知道的越少越好,對於被依賴的類不管多複雜,都儘量將邏輯封裝在內部。對外部除了提供 public 方法,不要透露任何資訊
  • 只與直接的朋友通訊
  • 直接朋友:每個物件都會與其他物件有耦合關係,只要兩個物件之間有耦合關係,我們就說這兩個物件之間是朋友關係。耦合的方式很多,依賴、關聯、聚合等。其中我們稱出現成員變數,方法引數,方法返回值中的類為直接朋友,而區域性變數中的類不是直接的朋友。也就是說,陌生的類最好不要以區域性變數的形式出現在類的內部。
<?php
declare(strict_types=1);

namespace  Neilyoz\DesignPatternBase\LawOfDemeter;

class  Student
{
}

class  Police
{
}

class  LawOfDemeter
{
    // 直接朋友
    private  Student  $student;
    public  function  getStudent():  Student
    {
        return  $this->student;
    }

    public  function  setStudent(Student  $student):  void
    {
        $this->student  =  $student;
    }

    public  function  doSomething()
    {
        // 非直接朋友
        $police  =  new  Police();
    }
}

總結

根據這些基本的原則去理解設計模式,感覺不會太困難,當然會 UML 類圖就最好了。

原文連結

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Neilyozの魚不浪

相關文章