在前面几篇文章中我们讲了创建型设计模式的单例模式,工厂方法,抽象工厂,以及建造者模式,在这篇文章我将为大家来介绍创建型模式的最后一个模式: 原型(Prototype) 模式

场景

现在我们有一个需求 要求有一个简历类,必须要有姓名,可以设置性别年龄,可以设置工作经历。我们可能需要用到多份简历。
很快我们就写出了第一个版本,代码如下:
简历类 Resume.php

<?php
namespace DesignPatterns\Prototype\V1;

class Resume
{
    private $name;
    private $sex;
    private $age;
    private $timeArea;
    private $company;

    /**
     * Resume constructor.
     * @param $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }
    //设置个人信息
    public function SetPersonalInfo(string $sex,string $age)
    {
        $this->sex = $sex;
        $this->age = $age;
    }
    //设置工作经历
    public function SetWorkExperience(string $timArea,string $company)
    {
        $this->timeArea = $timArea;
        $this->company = $company;
    }
    //显示
    public function Display(){
        return $this->name." ".$this->sex." ".$this->age."<br>工作经历:".$this->timeArea." ".$this->company."<br>";
    }

}

运行 client.php:

<?php
namespace DesignPatterns\Prototype\V1;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run(){
        $resumea = new Resume("张三");
        $resumea->SetPersonalInfo('男','18');
        $resumea->SetWorkExperience('2000-2004','xx公司');

        $resumeb = new Resume("张三");
        $resumeb->SetPersonalInfo('男','18');
        $resumeb->SetWorkExperience('2000-2004','xx公司');

        $resumec = new Resume("张三");
        $resumec->SetPersonalInfo('男','18');
        $resumec->SetWorkExperience('2000-2004','xx公司');
        
        echo $resumea->Display();
        echo $resumeb->Display();
        echo $resumec->Display();
    }
}
$client = new Client();
$client->run();

运行结果:

代码没有什么问题,但是试想一下代码中三份简历就需要实例化三次,如果是二十份简历那么我们是不是要实例化二十次,如果我们当中有些输入错了 例如年龄 那么我们就需要改二十次。这对客户端来说是十分糟糕的。这就需要用的我们开头讲的原型模式了。

原型(Prototype)模式

用已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。

原型模式的结构

  1. 原型 (Prototype) 接口将对克隆方法进行声明。 在绝大多数情况下, 其中只会有一个名为 clone克隆的方法。
  2. 具体原型 (Concrete Prototype) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
  3. 客户端 (Client) 可以复制实现了原型接口的任何对象。

使用原型模式实现简历

接下来我们用原型模式去实现简历 PHP的原型模式比较简单, 你只需要去克隆他就好了。代码如下:
原型类 Resume.php

<?php
namespace DesignPatterns\Prototype\V2;
class Resume
{
    private $name;
    private $sex;
    private $age;
    private $timeArea;
    private $company;

    /**
     * Resume constructor.
     * @param $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }
    //设置个人信息
    public function SetPersonalInfo(string $sex,string $age)
    {
        $this->sex = $sex;
        $this->age = $age;
    }
    //设置工作经历
    public function SetWorkExperience(string $timArea,string $company)
    {
        $this->timeArea = $timArea;
        $this->company = $company;
    }
    //显示
    public function Display(){
        return $this->name." ".$this->sex." ".$this->age."<br>工作经历:".$this->timeArea." ".$this->company."<br>";
    }
}

运行 Client.php

<?php
namespace DesignPatterns\Prototype\V2;

require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run(){
        $resumea = new Resume("张三");
        $resumea->SetPersonalInfo('男','18');
        $resumea->SetWorkExperience('2000-2004','xx公司');

        $resumeb = clone $resumea;
        $resumeb->SetPersonalInfo('男','20');
        $resumeb->SetWorkExperience('2000-2004','yy公司');

        $resumec = clone $resumea;
        $resumec->SetPersonalInfo('男','24');
        $resumec->SetWorkExperience('2004-2008','zz公司');

        echo $resumea->Display();
        echo $resumeb->Display();
        echo $resumec->Display();
    }
}
$client = new Client();
$client->run();

运行结果:

我们发现使用clone后 我们如果想修改某一份简历,只需要对这一份简历进行修改就可以了,不会影响到其他简历,使用原型模式对对性能也有很大的提高,如果我们使用第一版代码每new一次,都需要执行一次构造函数,如果构造函数运行时间过长,那么我们多次的执行初始化操作效率就太低了,使用clone后,我们隐藏了对象创建的细节,又极大的提高了性能。

浅复制与深复制

在上面的代码中就是一个原型模式浅复制的例子,clone关键字可以帮我们一个对象的浅副本,克隆出来的产品与原对象有相同的属性。如果对象中有属性是对象,那么它们是不会复制到新产品的对象中的。意思就是如果我们的简历类中如果有对象引用。那么引用的对象数据是不会克隆过来的。
举个例子,我们的简历类中有一个设置工作经验的方法,一般会再有一个工作经历类,当中有时间区间,和公司名称等属性。
然后我们对代码进行改造如下:
工作经历 WorkExperience.php

<?php
namespace DesignPatterns\Prototype\V3;
/**
 * 工作经历
 * Class WorkExperience
 * @package DesignPatterns\Prototype\V3
 */
class WorkExperience
{
    private $workDate;
    private $company;

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

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

    /**
     * @param $workDate
     * @return mixed
     */
    public function SetWorkDate($workDate)
    {
        $this->workDate = $workDate;
    }


    /**
     * @param $company
     * @return mixed
     */
    public function SetCompany($company)
    {
        $this->company = $company;
    }
}

简历类 Resume.php

<?php
namespace DesignPatterns\Prototype\V3;
/**
 * 简历类
 * Class Resume
 * @package DesignPatterns\Prototype\V3
 */
class Resume
{
    private $name;
    private $sex;
    private $age;
    private $work;

    public function __construct($name)
    {
        $this->name = $name;
        $this->work = new WorkExperience();
    }
    //设置个人信息
    public function SetPersonalInfo(string $sex,string $age)
    {
        $this->sex = $sex;
        $this->age = $age;
    }
    //设置工作经历
    public function SetWorkExperience(string $timArea,string $company)
    {
        $this->work->SetWorkDate($timArea);
        $this->work->SetCompany($company);
    }
    //显示
    public function Display()
    {
        return $this->name." ".$this->sex." ".$this->age.
                "<br>工作经历:".$this->work->getWorkDate()." ".$this->work->getCompany()."<br>";
    }
}

运行Client.php

<?php
namespace DesignPatterns\Prototype\V3;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run(){
        $resumea = new Resume("张三");
        $resumea->SetPersonalInfo('男','18');
        $resumea->SetWorkExperience('2000-2004','xx公司');

        $resumeb = clone $resumea;
        $resumeb->SetPersonalInfo('男','20');
        $resumeb->SetWorkExperience('2000-2004','yy公司');

        $resumec = clone $resumea;
        $resumec->SetPersonalInfo('男','24');
        $resumec->SetWorkExperience('2004-2008','zz公司');

        echo $resumea->Display();
        echo $resumeb->Display();
        echo $resumec->Display();

    }
}
$client = new Client();
$client->run();


我们可以看到 使用浅复制对于值的引用没什么问题,但对于引用对象还是指向了原来的对象,虽然我们给三份简历都引用设置了工作经历,但可以看到三个引用都是最后一次设置,因为三个引用都指向了同一个对象。

简历的深复制

我们只需要在简历类中创建一个克隆方法__clone(),在克隆方法中让工作经验克隆完成,然后在对简历的相关字段赋值,最终返回一个深复制的简历对象。
代码如下:
Resume.php

    public function __clone()
    {
        $this->work = clone $this->work;
    }

然后我们再次运行 client.php,结果如下

总结

  1. 什么是原型模式?如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。
  2. 原型模式的两种实现方法原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。

github示例:https://github.com/yangpanyao/design-patterns/tree/master/Prototype
参考书籍: 大话设计模式

Last modification:June 15th, 2020 at 11:08 pm