PHP高階特性-反射Reflection以及Factory工廠設計模式的結合使用[程式碼例項]

janrs_com發表於2021-12-01

利用反射來實現工廠模式的生產而無需建立特定的工廠類

本文地址http://janrs.com/?p=833轉載無需經過作者本人授權

轉載請註明來源


反射[Relfection]

JANRS.COM - PHP Reflection 反射

什麼是Reflection

Reflection,即反射。反射提供給物件導向程式設計可以自省的能力

這麼理解有點太過於概念化,通俗地講,就是能根據事件的結果反查出原因。在程式設計中,可以根據一個被例項化的物件,反查出這個物件屬於的類以及該類擁有所有屬性以及方法,甚至可以讀取文件註釋。這個反查的過程就叫做反射

PHP 提供了完整的反射 API ,提供了內省類、介面、函式、方法和擴充套件的能力。此外,反射 API 提供了方法來取出函式、類和方法中的文件註釋。詳細見PHP官網 PHP反射簡介

Reflection能幹什麼

在上面講到的,可以使用反射來獲取一個類的所有屬性以及方法還有註釋文件,甚至可以獲取類屬性和方法的訪問許可權[protected/private],這些特性使得PHP的使用靈活性得到非常大的提高。例如:

- Laravel 框架的所謂優雅所在,即容器、依賴注入、IOC 控制反轉就是依靠這些特性實現的
- Hyperf 框架的註解路由也是根據反射獲得註釋來實現的
- 生成文件 因為反射可以獲取類屬性和方法的訪問許可權,可以掃描整個專案的所有檔案再使用反射來生成文件
- 測試驅動開發 利用反射獲取該類的所有方法的特性,進行測試驅動開發
- 開發外掛 利用反射獲取類的內部結構的特性,實現 Hook 功能,例如框架外掛的實現

Reflection的優缺點

優點 反射提供了對類的反解析,從而相比原本物件導向的程式設計方式獲得了極高的靈活性,以及合理的使用能夠讓程式碼看起來更加優雅以及簡潔。原本在物件導向的程式設計方式中,使用一個類的例項需要先 new 出一個物件再使用方法,但是使用了反射機制,只需要提供一個該類的方法然後使用反射機制即可使用該物件或者方法。Laravel 框架正是使用了大量的反射才獲得了優雅的美譽,SwooleHyperf 框架的註解路由的實現也是使用了反射

缺點 同時,由於反射是類例項化的反過程,破壞了物件導向的封裝性,直接將類的整個內部結構暴露,這就導致了反射一旦濫用,程式碼將難於管理,整個專案將非常混亂,甚至導致業務執行錯亂。尤其在大專案幾十人的團隊中,試想一下,原本的物件導向,只告訴什麼可以用,什麼不可以用,CTO寫好了底層程式碼,其他人繼承後然後使用就行,內部結構啥的其他人都不知道。一旦用上了反射,如果有一個程式設計師不小心將原本是 protected 或者是 private 的屬性或者方法設定成了可以訪問,其他程式設計師在不知情的情況呼叫了本該隱藏的資料或者方法,那將導致不可預測的災難【見下面示例程式碼】

其次,由於反射的靈活性極高,這導致了無法在 IDE 中通過直接直接點選程式碼溯源,對於新手真的是很蛋疼,LaravelHyperf 都是如此

在下面的程式碼中,反射的機制直接將 private 方法設定成外部可訪問

#Example:

<?php
class Foo {
  private function myPrivateMethod() {
    return 7;
  }
}

$method = new ReflectionMethod('Foo', 'myPrivateMethod');

//該反射功能直接將原本是private許可權的方法設定成可訪問
$method->setAccessible(true);

echo $method->invoke(new Foo);
// echos "7"
?>

工廠設計模式

三種工廠設計模式 [簡單工廠模式] [工廠模式] [抽象工廠模式]

簡單工廠模式 又稱為靜態工廠方法模式。簡單的說,就是建立物件的方式是通過一個靜態方法來實現的。在簡單工廠模式中,根據傳遞的引數來返回不同的類的例項

PHP中在簡單工廠模式中,有一個抽象的產品類【即abstract class Calculate】,這個抽象類可以是介面/抽象類/普通類。這個抽象的產品類可以派生出多個具體的產品類【即class CalculateAdd以及class CalculateSub】。最後再由一個具體的工廠類【即class CalculateFactory】來獲取所需要的產品類的例項

[JARNS.COM - 工廠模式[簡單工廠UML圖]](cdn.learnku.com/uploads/images/202...) JARNS.COM - 工廠模式[簡單工廠UML圖]

程式碼實現

1) 抽象產品生產類:運算抽象類
//生產抽象類
abstract class Calculate{

    //數字A
    protected $number_a = null;

    //數字B
    protected $number_b = null;

    //設定數字A
    public function setNumberA( $number ){
        $this->number_a = $number;
    }

    //設定數字B
    public function setNumberB( $number ){
        $this->number_b = $number;
    }

    //獲取數字A
    public function getNumberA(){
        return $this->number_a;
    }

    //獲取數字B
    public function getNumberB(){
        return $this->number_b;
    }

    //獲取計算結果【獲取生產出的產品】
    public function getResult(){
        return null;
    }
}
2) 具體產品生產類:加法運算 / 減法運算 等等
//加法運算
class CalculateAdd extends Calculate{

    //獲取運算結果【獲取具體的產品】
    public function getResult(){
        return $this->number_a + $this->number_b;
    }
}
//減法運算
class CalculateSub extends Calculate{

    //獲取運算結果【獲取具體的產品】
    public function getResult(){
        return $this->number_a - $this->number_b;
    }
}
//乘法 / 除法 等等其他運算【其他產品】
3) 工廠:工廠類。即用一個單獨的類來創造例項化的過程,這個類就是工廠。也就是 簡單工廠模式
php 中,實現的方式其實就一個 switch 函式或者是 php8 新出的 match 函式來例項化所需要的產品生產類

//根據運算不同例項化不同的物件
//【也就是根據所需產品,例項化對應的產品類進行生產】
//對應的實現其實就是一個switch或者php8函式新出的match函式
//下面用最新的match函式做演示
class CalculateFactory{

    public static function setCalculate( $type = null ){
        return match( $type ){
            'add' => (function(){
                return new CalculateAdd();
            })(),
            'sub' => (function(){
                return new CalculateSub();
            })(),
            default => null;
        };
    }

}

//具體使用

$calculate = CalculateFactory::setCalculate('add');
$calculate->setNumberA = 1;
$calculate->setNumberB = 2;

//計算
echo $calculate->getResult;//echo 3
總結:簡單工廠模式其實就是建立一個基類【abstract】,該類存放所有具體生產產品類的共用的程式碼,但是沒有執行過程,然後具體生產產品的類全部繼承基類再實現各自的生產過程。最後建立一個工廠類,該類用來根據傳入的引數來獲取所需的生產類

工廠方法模式 又稱為工廠模式,屬於創造型模式。在工廠模式中,工廠類的父類只負責定義公共介面,並不執行實際的生產動作。實際的生產動作則交給工廠的子類來完成。這樣做將類的的例項化延遲到了工廠的子類,通過工廠的子類來完成例項化具體的產品,也就是生產

在工廠模式中,跟簡單工廠模式不一樣的是,有一個抽象的工廠類【即interface CalculateFactory】,可以是介面/抽象類,這個抽象的工廠類可以派生出多個具體的工廠類【即FactoryAdd以及FactorySub

[JARNS.COM - 工廠模式[工廠UML圖]](cdn.learnku.com/uploads/images/202...) JARNS.COM - 工廠模式[工廠UML圖]

程式碼實現【以下程式碼需要用到上面的生產抽象類】

以下程式碼需要用到上面的生產抽象類:abstract class Calculate
以及具體的生產類,即:CalculateAdd 以及 CalculateSub。下面不再重複實現


interface CalculateFactory{

    public function CreateCalculate();

}

class FactoryAdd implements CalculateFactory{

    public function CreateCalculate(){
        return new CalculateAdd();
    }

}

class FactorySub implements CalculateFactory{

    public function CreateCalculate(){
        return new CalculateSub();
    }

}

//具體使用

//建立工廠例項
$calculateFactory = new FactoryAdd();
$add = $calculateFactory->CreateCalculate();
$add->setNumberA( 1 );
$add->setNumberB( 2 );

//計算
echo $add->getResult();//echo 3
總結:工廠模式相比於簡單工廠模式的區別在於,在簡單工廠模式中,只有一個工廠來生產對應的生產物件【即CalculateFactory】。而在工廠模式中,每一個生產產物件都由自己的工廠來生產,並且這些工廠都繼承自同一個介面【即 interface CalculateFactory

抽象工廠模式 抽象工廠模式提供建立一系列相關或相互依賴物件的介面,而且無需指定它們具體的類。這麼理解很抽象。通俗一點的解釋就是,相比於上面的工廠模式來講,抽象工廠模式在每個不同的工廠之上又有一個超級工廠,這個超級工廠是抽象的介面【interface】,用來生產具體的工廠

在抽象工廠模式中,有多個抽象的產品類【即abstract class Phone以及abstract class Android】,可以是介面/抽象類/普通類,每個抽象產品類可以派生出多個具體產品類【即class IPhone / class MiPhone 以及 class IOS / class Android】。一個抽象的工廠類【即interface AbstractFactory】可以派生出多個具體的工廠類【即class iPhoneFactory以及class MiFactory】,且每個具體的工廠類可以建立多個產品類的例項【即都有createPhonecreateSystem

[JARNS.COM - 抽象工廠模式[工廠UML圖]](cdn.learnku.com/uploads/images/202...) JARNS.COM - 抽象工廠模式[工廠UML圖]

程式碼實現

//抽象的產品類
abstract class Phone{}
abstract class System{}

//具體的產品類
class IPhone extends Phone{}
class MiPhone extends Phone{}

//具體的產品類
class IOS extends System{}
class Android extends System{}

//超級工廠
interface AbstractFactory{
    public function createPhone();
    public function createSystem();
}

//具體的蘋果工廠
class iPhoneFactory implements AbstractFactory{

    //生產蘋果手機
    public function createPhone(){
        return new IPhone();
    }

    //生產蘋果系統
    public function createSystem(){
        return new IOS();
    }
}

//具體的小米工廠
class MiFactory implements AbstractFactory{

    //生產小米手機
    public function createPhone(){
        return new MiPhone();
    }

    //生產安卓系統
    public function createSystem(){
        return new Android();
    }
}
總結:抽象工廠模式相比於工廠模式,抽象工廠模式提供了一個介面用來規定所需要生產的產品。每個繼承於該介面的工廠都能按照指定的模式進行生產【程式碼中的AbstarctFactory

以上三種工廠模式,最終都是為了將重複的程式碼提取出來,並且按照特定的需求場景歸納好,進行解耦和複用,以便在需要的場景中直接使用

三種模式的概括為:

簡單工廠:

  • 一個抽象產品類(可以是:介面,抽象類,普通類),可以派生出多個具體產品類
  • 單獨一個具體的工廠類
  • 每個具體工廠類只能建立一個具體產品類的例項

    工廠模式:

  • 一個抽象產品類(可以是:介面,抽象類,普通類),可以派生出多個具體產品類
  • 一個抽象工廠類(可以是:介面,抽象類),可以派生出多個具體工廠類
  • 每個具體工廠類只能建立一個具體產品類的例項

    抽象工廠:

  • 多個抽象產品類(可以是:介面,抽象類,普通類),每個抽象產品類可以派生出多個具體產品類
  • 一個抽象工廠類(可以是:介面,抽象類),可以派生出多個具體工廠類
  • 每個具體工廠類可以建立多個具體產品類的例項

三個模式之間的區別:

  • 簡單工廠模式只有一個抽象產品類,只有一個具體的工廠類
  • 工廠方法模式只有一個抽象產品類,而抽象工廠模式有多個抽象產品類
  • 工廠方法模式的具體工廠類只能建立一個具體產品類的例項,而抽象工廠模式可以建立多個具體產品類的例項

工廠模式與反射的結合使用

可以利用反射的特性來實現工廠模式的生產過程,結合Laravel-admin進行舉例

先看下以下的程式碼,需求背景:需要根據角色不同顯示不同的許可權按鈕

<?php

class TaskController extends BaseController
{
    use HasResourceActions;

    /**
     * Make a grid builder.
     *
     * @return Grid
     */
    protected function grid()
    {
            //Grid Columns...

            if (Admin::user()->inRoles([AdminUserModel::getAssignmentRole()])) {
                $grid->disableBatchActions();
                $grid->disableEditButton();
                $grid->disableCreateButton();
                $grid->disableDeleteButton();
            } elseif (Admin::user()->inRoles([AdminUserModel::getEvaluatorRole()])) {
                $grid->disableBatchActions();
                $grid->disableEditButton();
                $grid->disableCreateButton();
                $grid->disableDeleteButton();
                $grid->actions(function (Grid\Displayers\Actions $actions) {
                    $actions->append(new ConfirmCloseTaskAction());
                });
            } else {
                $grid->disableCreateButton();
                $grid->disableDeleteButton();
                $grid->disableEditButton();
                $grid->disableBatchActions();
                $grid->disableViewButton();
                $grid->disableActions();
            }
    }
}

以上的程式碼很明顯一看就顯得很臃腫。且隨著業務的增加【即Controller的增加】以及角色的增加,需要寫更多重複的判斷以及重複的程式碼

解決思路:不同的角色需要擁有的不同的許可權,每個角色都可以用一個固定的方法來設定許可權,這個固定的方法可以為不同的角色設定許可權。這些條件剛好滿足工廠模式的使用場景:即:

  • 抽象出一個產品類來派生出多個角色的許可權產品類
  • 抽象出一個工廠類來派生出多個具體的工廠類,這些工廠類表現為對應要使用許可權按鈕的場景
  • 每個具體工廠【使用許可權按鈕的場景】可以建立多個具體產品類【即例項化多個角色的許可權產品】

程式碼如下【在下面的程式碼中,將使用反射來代替工廠的生產】

1) 抽象出一個產品類來派生出多個角色的許可權產品類
<?php

namespace App\GridActionFactory;

use Dcat\Admin\Grid;

/**
 * 工廠介面
 */
interface GridActionInterface
{
    //業務員角色的許可權
    function salesmanAction(Grid $grid);
    //分配員角色的許可權
    function assignmentAction(Grid $grid);
    //財務角色的許可權
    function financeAction(Grid $grid);

    //....其他角色的許可權
}
2,3) 2,3兩個步驟包含在一起。抽象出一個工廠類來派生出多個具體的工廠類,這些工廠類表現為對應要使用許可權按鈕的場景。其中,setRoleAction方法使用反射來直接生產,也就是替代了每個具體工廠類建立例項的過程
<?php

namespace App\GridActionFactory;

use Dcat\Admin\Admin;
use Dcat\Admin\Grid;

/**
 * 設定Action許可權抽象類
 */
abstract class GridActionAbstract
{
    //
    abstract public static function setAction(Grid $grid, string $role);

    /**
     * 過濾角色
     *
     * @param string $role
     * @return bool
     */
    protected static function isInRoles(string $role): bool
    {
        return Admin::user()->inRoles([$role]);
    }

    /**
     * 呼叫對應的方法
     * [該方法其實就是工廠模式中的工廠,專門來生產的]
     * [多個工廠對應的就是各個需要用到Action許可權的Controller控制器]
     * [每個Controller控制器來生產自己的Action許可權]
     * [這個生產是通過反射來實現]
     *
     * @param Grid $grid
     * @param string $role
     * @param string $class
     * @throws \ReflectionException
     */
    protected static function setRoleAction(Grid $grid, string $role, string $class)
    {
        $r = new \ReflectionClass($class);

        $methodName = $role . 'Action';
        if (!$r->hasMethod($methodName))
            throw new \Exception('Method Not Found [ method : ' . $methodName . ' ] ');

        $method = $r->getMethod($methodName);
        $method->invoke($r->newInstance(), $grid);
    }
}

根據以上的反射來實現例項化的過程,上面的TaskController的許可權可以簡化成下面的程式碼:

<?php

namespace App\GridActionFactory;

use Dcat\Admin\Grid;

class TaskAction extends GridActionAbstract implements GridActionInterface
{
    /**
     * @param Grid $grid
     * @param string $role
     * @throws \ReflectionException
     */
    public static function setAction(Grid $grid, string $role)
    {
        if (!parent::isInRoles($role)) return;

        //通過呼叫父類的setRoleAction直接實現生產的過程
        parent::setRoleAction($grid, $role, self::class);
    }

    //在TaskController下有需要使用許可權按鈕的角色
    //分配員角色
    public function assignmentAction(Grid $grid)
    {
        //許可權按鈕
        $grid->showActions();
        $grid->showViewButton();
    }

    //在TaskController下有需要使用許可權按鈕的角色
    //財務角色
    public function financeAction(Grid $grid)
    {
        $grid->showActions();
        $grid->showViewButton();
    }

    //在TaskController下有需要使用許可權按鈕的角色
    //業務員角色
    public function salesmanAction(Grid $grid)
    {
    }

    //....其他角色
}

經過使用設計模式封裝後,上面TaskController中控制許可權的程式碼直接優化成如下:【優雅了不少~

<?php

class TaskController extends BaseController
{
    use HasResourceActions;

    /**
     * Make a grid builder.
     *
     * @return Grid
     */
    protected function grid()
    {
            //Grid Columns...
            //財務角色按鈕
              TaskAction::setAction($grid, AdminUserModel::getFinanceRole());
            //分配員角色按鈕
              TaskAction::setAction($grid, AdminUserModel::getAssignmentRole());

            //...其他角色按鈕
            /*
            if (Admin::user()->inRoles([AdminUserModel::getAssignmentRole()])) {
                $grid->disableBatchActions();
                $grid->disableEditButton();
                $grid->disableCreateButton();
                $grid->disableDeleteButton();
            } elseif (Admin::user()->inRoles([AdminUserModel::getEvaluatorRole()])) {
                $grid->disableBatchActions();
                $grid->disableEditButton();
                $grid->disableCreateButton();
                $grid->disableDeleteButton();
                $grid->actions(function (Grid\Displayers\Actions $actions) {
                    $actions->append(new ConfirmCloseTaskAction());
                });
            } else {
                $grid->disableCreateButton();
                $grid->disableDeleteButton();
                $grid->disableEditButton();
                $grid->disableBatchActions();
                $grid->disableViewButton();
                $grid->disableActions();
            }
            */
    }
}

總結:設計模式以及反射通常在寫框架的時候用的比較多。但是在專案中,適當的使用設計模式以及反射,能夠讓程式碼更加健壯以及可擴充套件,也很優雅~

歡迎來我的部落格逛一逛 楊建勇的個人部落格http://janrs.com

本作品採用《CC 協議》,轉載必須註明作者和本文連結
做人要像被壓著的彈簧一樣。時刻保持壓力,隨時準備反彈

相關文章