欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Yii2设计模式——工厂方法模式

程序员文章站 2022-05-04 13:25:50
工厂方法模式(Factory Method Pattern)定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类吧实例化推迟到子类。 ......

应用举例

yii\db\schema抽象类中:

//获取数据表元数据
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的子类中去实现。

工厂方法模式

模式定义

工厂方法模式(factory method pattern)定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类吧实例化推迟到子类。

什么意思?说起来有这么几个要点:

  • 对象不是直接new产生,而是交给一个类方法去完成。比如loadtableschema()方法

  • 这个方法是抽象的,且必须被子类所实现
  • 这个提供实例的抽象方法需要参与到其他逻辑中,去完成另一项功能。比如loadtableschema()方法出现在gettableschema()方法中,参与实现获取数据表元数据的功能

代码实现

现在,我们打算用利用面向对象的手段——工厂方法模式为你开一家披萨连锁店。

假如你现在打算开一家披萨连锁店,分店以加盟的方式加入。加盟店提供加盟费以及利润提成,而总店提供配方,烹饪方法,以及选址和其他方面的建议。这种场景够常见了吧?什么奶茶加盟店、花甲加盟店、火锅加盟店等等都是这个套路。那么,现在你打算怎么做?

首先,我希望为各个加盟店制定一定的规范以保证基本口味和品牌形象,比如配方、原料、加工方式都我来提供

其次,我也允许加盟店可以适当的扩展以增加其灵活性和丰富性,比如切块和包装可以采用自己的

如果你想明白这两点,那恭喜你!你已经基本搞清楚了抽象和具体的关系了。在这里总店就是是抽象,加盟店就是具体

第一步,我们的披萨是什么样子的,配方原料加工方式是什么。因此我们要有个抽象的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实例。