Yii2設計模式——工廠方法模式

應用舉例

yii\db\Schema抽象類中:mysql

//獲取數據表元數據
public function getTableSchema($name, $refresh = false)
{
    if (array_key_exists($name, $this->_tables) && !$refresh) {
        return $this->_tables[$name];
    }

    $db = $this->db;
    $realName = $this->getRawTableName($name);

    if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) {
        /* @var $cache Cache */
        $cache = is_string($db->schemaCache) ? Yii::$app->get($db->schemaCache, false) : $db->schemaCache;
        if ($cache instanceof Cache) {
            $key = $this->getCacheKey($name);
            if ($refresh || ($table = $cache->get($key)) === false) {
                //經過工廠方法loadTableSchema()去獲取TableSchema實例
                $this->_tables[$name] = $table = $this->loadTableSchema($realName);
                if ($table !== null) {
                    $cache->set($key, $table, $db->schemaCacheDuration, new TagDependency([
                        'tags' => $this->getCacheTag(),
                    ]));
                }
            } else {
                $this->_tables[$name] = $table;
            }

            return $this->_tables[$name];
        }
    }
    //經過工廠方法loadTableSchema()去獲取TableSchema實例
    return $this->_tables[$name] = $this->loadTableSchema($realName);
}

//獲取TableSchema實例,讓子類去實現
abstract protected function loadTableSchema($name);

這裏使用了工廠方法模式。loadTableSchema()就是工廠方法,它負責提供一個具體的TableSchema類以供getTableSchema()使用。而要提供具體的TableSchema類,顯然要到各個Schema的子類中去實現。sql

工廠方法模式

模式定義

工廠方法模式(Factory Method Pattern)定義了一個建立對象的接口,但由子類決定要實例化的類是哪個。工廠方法讓類吧實例化推遲到子類。數據庫

什麼意思?提及來有這麼幾個要點:數組

  • 對象不是直接new產生,而是交給一個類方法去完成。好比loadTableSchema()方法app

  • 這個方法是抽象的,且必須被子類所實現框架

  • 這個提供實例的抽象方法須要參與到其餘邏輯中,去完成另外一項功能。好比loadTableSchema()方法出如今getTableSchema()方法中,參與實現獲取數據表元數據的功能yii

代碼實現

如今,咱們打算用利用面向對象的手段——工廠方法模式爲你開一家披薩連鎖店。this

假如你如今打算開一家披薩連鎖店,分店以加盟的方式加入。加盟店提供加盟費以及利潤提成,而總店提供配方,烹飪方法,以及選址和其餘方面的建議。這種場景夠常見了吧?什麼奶茶加盟店、花甲加盟店、火鍋加盟店等等都是這個套路。那麼,如今你打算怎麼作?spa

首先,我但願爲各個加盟店制定必定的規範以保證基本口味和品牌形象,好比配方、原料、加工方式都我來提供code

其次,我也容許加盟店能夠適當的擴展以增長其靈活性和豐富性,好比切塊和包裝能夠採用本身的

若是你想明白這兩點,那恭喜你!你已經基本搞清楚了抽象和具體的關係了。在這裏總店就是是抽象,加盟店就是具體

第一步,咱們的披薩是什麼樣子的,配方原料加工方式是什麼。所以咱們要有個抽象的Pizza類,規定要作披薩的原料有面團、醬汁、各類配菜;加工程序有準備、烘烤、切塊、包裝。

abstract class Pizza
{
    /**
     * @var
     */
    protected $name;
    /**
     * @var
     */
    protected $dough;
    /**
     * @var
     */
    protected $sauce;
    /**
     * @var array
     */
    protected $toppings = [];

    public function prepare()
    {
        print_r('Preparing '.$this->name.'<br>');
        print_r('Tossing dough...'.'<br>');
        print_r('Adding sauce...'.'<br>');
        print_r('Adding toppings...'.'<br>');
        foreach ($this->toppings as $topping) {
            print_r("$topping".'<br>');
        }
    }

    public function bake()
    {
        print_r('Bake: Bake for 25 minutes at 350'.'<br>');
    }

    public function cut()
    {
        print_r('Cut: Cutting the pizza into diagonal slices'.'<br>');
    }

    public function box()
    {
        print_r('Box: Place pizza in official PizzaStore box'.'<br>');
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }
}

你是總店,你能夠規定哪些工序能夠用我總店的,而哪些必須你本身去實現。好比,加盟店選擇開在哪裏必須加盟店本身去完成,而原料和作法則總店來決定。

體如今上面的代碼,就是abstract Pizza類全部方法均可以根據須要改爲abstract的。當你要求必須子類去完成的就是abstract的;當你提供了默認的行爲,可以讓子類繼承直接使用的就是具體的。

規定了披薩如何構成的,總店第二步還須要指導下分店怎麼把披薩作出來,所以還須要有個抽象的披薩店類PizzaStore:

abstract class PizzaStore
{
    /**
     * @var Pizza
     */
    protected $pizza;

    /**
     * @param $type
     *
     * @return Pizza
     */
    public function orderPizza($type)
    {
        // create a pizza
        $this->pizza = $this->createPizza($type);

        // handle the pizza
        $this->pizza->prepare();
        $this->pizza->bake();
        $this->pizza->cut();
        $this->pizza->box();

        // return the prepared pizza
        return $this->pizza;
    }

    /**
     * Create a Pizza.
     *
     * @param $type
     *
     * @return Pizza
     */
    abstract protected function createPizza($type);
}

總店在抽象的PizzaStore規定了披薩加工的基本流程,全部的加盟店加工披薩都必須按照準備->烘烤->切塊->裝盒這幾個固定的工序進行。至於誰提供披薩,這些披薩原料是啥,烘烤多久,切成啥形狀,包裝成啥樣子,這些都是具體的Pizza自己所的細節,由各個加盟店本身決定的。

所以,各個加盟店必需要繼承抽象的createPizza()方法,去具體實現本身的細節,作出不一樣口味的披薩來。

最後一步,咱們可讓別人來加盟了。

有人打算在紐約開一家披薩加盟店:

class NYPizzaStore extends PizzaStore
{
    /**
     * Create a Pizza.
     *
     * @param $type
     *
     * @return Pizza
     */
    public function createPizza($type)
    {
        if ($type == 'cheese') {
            return new NYStyleCheesePizza();
        } elseif ( $type == 'clam') {
            return new NYStyleClamPizza();
        }
    }
}

紐約店暫時提供兩種口味的披薩:奶酪味和蛤蜊味。

//奶酪味
class NYStyleCheesePizza extends Pizza
{
    /**
     * NYStyleCheesePizza constructor.
     */
    public function __construct()
    {
        $this->name = 'NY Style Sauce and Cheese Pizza';
        $this->dough = 'Thin Crust Dough';
        $this->sauce = 'Marinara Sauce';

        $this->toppings[] = 'Grated Reggiano Cheese';
    }

    /**
     * {@inheritdoc}
     */
    public function box()
    {
        print_r('Box: Place pizza in NY PizzaStore box'.'<br>');
    }
}

//蛤蜊味
class NYStyleClamPizza extends Pizza
{
    /**
     * NYStyleClamPizza constructor.
     */
    public function __construct()
    {
        $this->name = 'NY Style Sauce and Clam Pizza';
        $this->dough = 'Thin Crust Dough';
        $this->sauce = 'Marinara Sauce';

        $this->toppings[] = 'Grated Reggiano Clam';
    }

    /**
     * {@inheritdoc}
     */
    public function box()
    {
        print_r('Box: Place pizza in NY PizzaStore box'.'<br>');
    }
}

紐約店兩種口味披薩的特色是:

  • 奶酪味:薄面團、Marinara醬料,配菜是Grated Reggiano Cheese(一種奶酪),採用本身的包裝,烘烤和切塊採用總店的

  • 蛤蜊味:薄面團、Marinara醬料,配菜是Grated Reggiano Clam(一種蛤蜊),採用本身的包裝,烘烤和切塊採用總店的

看來紐約喜歡的披薩麪糰要薄一點....

不久,芝加哥又想加盟一家披薩店,和紐約人不一樣的是,芝加哥人但願披薩的麪糰厚一些,所謂一方一俗吧。因而咱們把店先開起來,而後再作披薩:

class ChicagoPizzaStore extends PizzaStore
{
    /**
     * @param $type
     *
     * @return Pizza
     */
    public function createPizza($type)
    {
        if ($type == 'cheese') {
            return new ChicagoStyleCheesePizza();
        } elseif ($type' == 'clam) {
            return new ChicagoStyleClamPizza();
        }
    }
}

芝加哥的分店暫時也只提供兩種口味:奶酪味和蛤蜊味。

//奶酪味
class ChicagoStyleCheesePizza extends Pizza
{
    /**
     * ChicagoStyleCheesePizza constructor.
     */
    public function __construct()
    {
        $this->name = 'Chicago Style Deep Dish Cheese Pizza';
        $this->dough = 'Extra Thick Crust Dough';
        $this->sauce = 'Plum Tomato Sauce';

        $this->toppings[] = 'Shredded Mozzarella Cheese';
    }

    public function cut()
    {
        print_r('Cut: Cutting the pizza into square slices').'<br>';
    }
}

//蛤蜊味
class ChicagoStyleClamPizza extends Pizza
{
    /**
     * ChicagoStyleClamPizza constructor.
     */
    public function __construct()
    {
        $this->name = 'Chicago Style Deep Dish Clam Pizza';
        $this->dough = 'Extra Thick Crust Dough';
        $this->sauce = 'Plum Tomato Sauce';

        $this->toppings[] = 'Shredded Mozzarella Clam';
    }

    public function cut()
    {
        print_r('Cut: Cutting the pizza into square slices'.'<br>');
    }
}

芝加哥店兩種口味披薩的特色是:

  • 奶酪味:加厚麪糰、Plum Tomato醬料,配菜是Shredded Mozzarella Cheese(一種奶酪),切成方塊,烘烤和包裝採用總店的

  • 蛤蜊味:加厚麪糰、Marinara醬料,配菜是Shredded Mozzarella Clam(一種蛤蜊),切成方塊,烘烤和包裝採用總店的

如今咱們就有了兩家分店,四種不一樣口味披薩了。你已經等了好久了,來吃些披薩吧:

class Test 
{
    public function run()
    {
        $nyStore = new NYPizzaStore();   
        print_r("Terry ordered a NY Style Cheese Pizza" . '<br>');
        $pizza1 = $nyStore->orderPizza('cheese');

        echo '<br><br>';

		$chicagoStore = new ChicagoPizzaStore();
        print_r("Json ordered a Chicago Style Clam Pizza" . '<br>');
        $pizza2 = $chicagoStore->orderPizza('clam');
    }
}

我想嚐嚐紐約奶酪風味的,那我首先要有個紐約店,再由紐約店給我提供奶酪味的;我想嚐嚐芝加哥蛤蜊味的,那我首先要有個芝加哥店,再有芝加哥店給我提供蛤蜊味的。

認識工廠方法模式

全部的工廠模式都是用來封裝對象的建立。工廠方法模式經過讓子類來決定建立的對象是什麼,從而達到將對象建立的過程封裝的目的。

披薩店經過orderPizza()提供最終的披薩,在orderPizza()看來,我須要一個工廠方法createPizza()給我提供一個未加工的,而後我來作準備、烘烤、切塊、包裝,最終返回。orderPizza()無需瞭解披薩具體細節,由於反正全部的披薩都這麼個過程。而在抽象的PizzaStore中也createPizza()也不肯定細節,它只能保證本身要提供這麼一個。具體的細節是其子類去規定。

所以,表如今代碼中,就是在abstract class PizzaStore中,我尚未一個Pizza的實例呢,但prepare()->bake()->cut()->box()->return 也照樣這麼作了下來。

public function orderPizza($type)
{
    // create a pizza
    $this->pizza = $this->createPizza($type);

    // handle the pizza
    $this->pizza->prepare();
    $this->pizza->bake();
    $this->pizza->cut();
    $this->pizza->box();

    // return the prepared pizza
    return $this->pizza;
}

此時的$this->pizza是實現abstract class Pizza的規定的一種抽象,而還不是一個具體的實例。

這就是依賴抽象而不依賴具體

工廠方法模式的另外一種認識,就是將orderPizza()和一個工廠方法createPizza()聯合起來,加上其餘的prepare()/bake()等邏輯,就組成了一個框架,一種規範。子類繼承這個抽象類也就得到了這個規範。若是說createPizza()是子類自由發揮的部分,那麼orderPizza()就給你規定了自由發揮的一些前提,從而是有限度的自由。這是總店但願看到的,但願你在這個框框裏面去開你的分店,而不要自由發揮得太離譜。

工廠方法和簡單工廠

工廠方法和簡單工廠的區別在於,簡單工廠把所有的事情,在一個地方都處理完了,然而工廠方法倒是在建立一個框架,讓子類決定要如何實現。好比說,在工廠方法中,orderPizza()方法提供了通常的框架,以便建立披薩,orderPizza()方法依賴工廠方法建立具體類,再通過一系列其餘操做,最終制造出實際的披薩。可經過繼承PizzaStore類,決定實際造出的披薩是什麼。簡單工廠的作法,能夠將對象的建立封裝起來,可是一會兒給你一個完整的,沒有那種子類的「推遲」,所以不具有工廠方法的彈性。

工廠方法和抽象工廠

細心的讀者可能會發現一個問題,就是各家披薩店的原料都是本身提供(參看各個披薩店的__construct()),這樣口味仍是有較大的隨意性。爲了保證各分店口味大體相同,咱們須要對原料作統一管理,讓總店來統一供給作披薩的原料。

爲了吃一個披薩,咱們首先要有個披薩工廠(店),爲了供給披薩原料,咱們須要什麼呢?——原料工廠唄!若是想到這一層,那恭喜你,已經進階到抽象工廠的層次了。

抽象工廠將上面的dough,sauce,cheese,clams等等——凡是出現過的原料都讓一個抽象方法去實現,將全部這些抽象方法集合起來放到一個類裏,就是抽象工廠。例如,原料工廠應該實現下面的接口:

interface PizzaIngredientFactory
{
    /**
     * @return Dough
     */
    public function createDough();

    /**
     * @return Sauce
     */
    public function createSauce();

    /**
     * @return Cheese
     */
    public function createCheese();

    /**
     * @return Veggie[]
     */
    public function createVeggies();

    /**
     * @return Clams
     */
    public function createClams();
}

實現了這個接口的就是具體原料工廠。在讓PizzaStore建立披薩時,先往PizzaStore注入一個PizzaIngredientFactory實例,而後委託這個實例去提供各類原料。

所以,抽象工廠實際上是基於工廠方法的。工廠方法定義建立一種產品,而抽象工廠負責定義常見一組產品的接口,這個接口的每一個方法都像工廠方法同樣建立一個具體產品,同時咱們用抽象工廠的子類去提供這些具體的作法,從而最終提供一組一組的形形色色的產品來。

Yii2中的工廠方法模式

咱們已經走得夠遠了,讓咱們回到Yii2框架中來。

本文開頭的那個例子中,yii\db\Schema是爲各個DBMS提供了一個統一的、抽象的數據庫信息的基類。getTableSchema()方法是獲取表的元數據,而loadTableSchema()將一個數據表填充爲一個TableSchema類。

也就說說loadTableSchema()須要返回一個TableSchema的具體類。這個類包含了schema名、表名、主鍵foreignKeys以及表明數據表字段信息的ColumnSchema的數組colums[]。

顯然,各個數據庫的表和字段類型是有差別的,所以loadTableSchema()必須爲抽象方法,由子類去作具體實現。mysql/mssql/cubrid/sqlite等DBMS的Schema也確實繼承了yii\db\Schema類,實現了各自的loadTableSchema()具體方法。

以mysql的爲例:

class Schema extends \yii\db\Schema
{
    //...

    /**
     * Loads the metadata for the specified table.
     * @param string $name table name
     * @return TableSchema driver dependent table metadata. Null if the table does not exist.
     */
    protected function loadTableSchema($name)
    {
        $table = new TableSchema;
        $this->resolveTableNames($table, $name);

        if ($this->findColumns($table)) {
            $this->findConstraints($table);

            return $table;
        } else {
            return null;
        }
    }

    //...
}

它就在loadTableSchema()方法中返回了一個具體的TableSchema實例。

相關文章
相關標籤/搜索