现在的位置: 首页 > 综合 > 正文

【转】PHP之星际设计模式上(转自lightsaber)

2013年10月11日 ⁄ 综合 ⁄ 共 12283字 ⁄ 字号 评论关闭

1-简单工厂模式

不熟悉面向对象的朋友,建议先看看用星际快速入门PHP面向对象编程
上次用星际争霸讨论了面向对象的基础知识,似乎面向对象能解决很多问题。
但是还会有很多问题,简单的靠类和对象解决不得太好。
比如如何根据玩家输入的内容(尽管可以转化为其他字符串),来确定要制造的兵种,玩家不会输入代码:new Marine()。
和星际一样,PHP也没有终极兵种,如果类和接口是兵种的话,那么设计模式就是你的战术和控制,它可以让你靠各种兵种的搭配获胜。
待解决的问题:在人族的兵营,我们靠相应玩家的输入来动态确定要造的兵种,假设是机枪兵和火焰兵。

思路:动态的根据传递的数据,新建相应的类的对象。
简单工厂模式示例:
我们把机枪兵类的代码放入一个文件,Marine.php,它的代码如下:

<?php
class Marine {
    //机枪兵攻击的方法
public function attack()
{
    echo 'Marine attack';
}
}
?>

我们把火焰兵类的代码放入一个文件,Firebat.php,它的代码如下:

<?php
class Firebat {
    //火焰兵攻击的方法
public function attack()
{
    echo 'Firebat attack';
}
}
?>

主文件中的内容如下:

<?php
//兵种制造器的类
class BarracksCreator {
    //制造兵种的方法
public create($createWhat)
{
   //根据输入的参数,动态的把需要的类的定义文件载入
    require_once($createWhat.'.php');
   //根据输入的参数,动态的返回需要的类的对象
    return new $createWhat;
}
}

//新建一个兵种制造器对象
$creator = new BarracksCreator();

//靠接收参数制造一个火焰兵对象
$troop1 = $creator->create('Marine');
$troop1->attack();

//靠接收参数制造一个机枪兵对象
$troop2 = $creator->create('Firebat');
$troop2->attack();
?>

用途总结:简单工厂模式可以将新建对象的任务进行封装,一旦需要增加新的返回类,只要修改负责新建对象的那部分代码。
实现总结:需要一个自动根据参数返回新建对象的工厂,比如上面兵种制造器BarracksCreator,使用的时候只需要将参数传递给他的生产方法create(),无需考虑具体的生产细节。

/***************************************************************/

2-工厂方法模式

PHP手册上提到的工厂模式,其实是简单工厂模式。这里来讨论简单工厂模式的扩展:工厂方法模式。
待解决的问题:虽然简单工厂解决了动态返回不同类型对象的问题,但是实际情况当中,往往在新建一个对象的时候,需要做一些额外处理,比如制造机枪兵的时候需要判断水晶矿是否大于50,而制造火焰兵的时候需要同时判断水晶矿是否大于50和气矿大于25,还有是否建造了研究院。如果把这些代码全部放到工厂制造类里面,会使得制造类很臃肿,而且随着工厂生产的对象的种类越来越多,工厂制造类的代码会越来越难以维护。

思路:简单工厂模式中的工厂类(兵种制造器的类)保持不变,增加一个制造接口,定义一个实际制造对象的方法,然后定义各个具体制造不同对象的工厂,同时要求这些工厂执行这个制造接口,让这些工厂去实现实际制造对象的方法。

工厂方法模式示例:
我们把机枪兵类和制造机枪兵的类的代码放入一个文件,Marine.php,它的代码如下:
<?php
//机枪兵类
class Marine {
    //机枪兵攻击的方法
public function attack()
{
    echo 'Marine attack';
}
}
//制造机枪兵的类,执行接口abstractCreator
class MarineCreator implements abstractCreator {
    //实际制造机枪兵的方法
public function realCreate()
{
    //如果水晶矿大于50,这里只是作为说明,因为并不存在ore这个变量,也不考虑水晶少于50的处理
    if($ore>50)
    {
    return new Marine();
    }
}
}
?>
我们把火焰兵类和制造火焰兵的类的代码放入一个文件,Firebat.php,它的代码如下:
<?php
//火焰兵类
class Firebat {
    //火焰兵攻击的方法
public function attack()
{
    echo 'Firebat attack';
}
}
//制造火焰兵的类,执行接口abstractCreator
class FirebatCreator implements abstractCreator
    //实际制造火焰兵的方法
public function realCreate()
{
    //如果水晶矿大于50同时气矿大于25,并且研究院已经存在。这里只是作为说明,因为并不存在ore和gas和Academy变量,也不考虑资源不够时的处理
    if($ore>50 && $gas>25 && Academy>1)
    {
    return new Firebat();
    }
}
}
?>

主文件中的内容如下:
<?php
//各个具体工厂必须执行的接口
interface abstractCreator {
//规定各个具体工厂要实现的方法
public function realCreate();
}
//兵种制造器的类,也就是主工厂
class BarracksCreator {
    //制造兵种的方法
public create($createWhat)
{
   //根据输入的参数,动态的把需要的类的定义文件载入
    require_once($createWhat.'.php');
   //根据输入的参数,动态的获取相应的具体工厂的类的名字
    $creatorClassName = $createWhat.'Creator';
   //新建具体工厂对象
    $creator = new $creatorClassName;
   //用具体工厂来实际生产,然后返回需要的类的对象。因为它们都执行了接口abstractCreator,所以肯定实现了方法realCreate()
    return $creator->realCreate();
}
}
//新建一个兵种制造器对象
$creator = new BarracksCreator();
//靠接收参数制造一个火焰兵对象
$troop1 = $creator->create('Marine');
$troop1->attack();

//靠接收参数制造一个机枪兵对象
$troop2 = $creator->create('Firebat');
$troop2->attack();
?>

用途总结:工厂方法模式将新建对象的任务将给对应的具体工厂类,不必因为某些生产的对象需要进行额外处理而修改对外的主工厂。
实现总结:需要接收参数的主工厂类,比如上面兵种制造器BarracksCreator,还需要声明具体制造方法的一个接口,比如上面abstractCreator,然后定义具体生产各个产品的具体工厂类,每个具体工厂类必须执行接口abstractCreator,这样他们就必须实现制造对象的方法,比如上面的realCreate()。使用的时候只需要将参数传递给主工厂类工厂的生产方法create(),然后由create()根据参数生成具体工厂类的对象,并调用具体工厂类realCreate()获取制造的产品对象并返回,对外界使用来说,只需调用主工厂类工厂进行生产。

说明:其实这篇文章内的工厂方法模式和有些文章写的不同,标准的工厂模式往往是用一个抽象类来代替上面的接口abstractCreator,然后让所有的具体工厂类来继承它,但使用的时候,由于抽象类不能实例化(新建它的对象),所以经常是代码中直接new FirebatCreator(),但是简单工厂模式可以解决直接new的问题,所以我这里将简单工厂模式和工厂方法模式一起使用,使这里的示例更加实用。同时由于PHP是单继承,而执行接口的数量是没有限制的,所以使用接口abstractCreator更加灵活。

/***************************************************************/

3-抽象工厂模式

星际争霸是战略游戏,所以同样的兵种,敌我显示是不同的。
典型的就是鼠标的颜色,点中自己的物体的时候,鼠标颜色变成绿色,点中敌人的物体的时候,鼠标颜色变成红色。
还有就是每个物体的状态,点中自己的物体的时候,状态区显示完整的状态,点中敌人的物体的时候,状态区显示一部分信息。
我们假设只考虑鼠标和人族的运输船,玩家自己的运输船点中后状态区会显示里面装载的部队,而点中敌人的则不会显示里面是否装载部队。

这样我们就有四种对象:点中自己的鼠标,点中敌人的鼠标,自己的运输船状态,敌人的运输船状态。
如果用工厂方法模式,就要建立四个具体工厂(或者子工厂),这样的代码不便于维护和修改,因为我们以后要增加另一种情况:盟友。
待解决的问题:我们希望将这些对象联系起来,使得工厂的操作更加有逻辑性。
思路:既然我们通过自己和敌人来区分对象,那么统一归属的对象放入相同的具体工厂,每个具体工厂负责制造多种对象。
抽象工厂模式示例:
<?php
//四个产品类
//点中自己的物体时的鼠标
class mineMouse {
//鼠标的颜色
$color = 'green';
}
//点中敌人的物体时的鼠标
class enemyMouse {
//鼠标的颜色
$color = 'red';
}
//自己的运输船状态
class mineDropship {
//显示装载的情况,假设2辆坦克
$loading = '2 tanks';
}
//敌人的运输船状态
class enemyDropship {
//不显示装载的情况
$loading = '';
}
//主工厂类,也叫抽象工厂类
class abstractCreator {
    //根据参数分配工作到具体的工厂,并返回具体工厂对象
public function getCreator($belong)
{
    //获取具体工厂的类名
    $creatorClassName = $belong.'Creator';
    //返回具体工厂对象
    return new $creatorClassName();
}
}
//具体工厂必须执行的接口
interface productCreator {
//制造方法,或者说根据参数返回产品(鼠标,运输船)的方法
public function creatProduct($productName);
}
//制造属于自己的物体的具体工厂,执行接口
class mineCreator implements productCreator {
    //根据参数生产并返回属于自己的产品
public function creatProduct($productName)
{
    //获取产品的类名
    $productClassName = 'mine'.$productName;
    //返回产品对象
    return new $productClassName;
}
}
//制造属于敌人的物体的具体工厂,执行接口
class enemyCreator implements productCreator {
    //根据参数生产并返回属于敌人的产品
public function creatProduct($productName)
{
    //获取产品的类名
    $productClassName = 'enemy'.$productName;
    //返回产品对象
    return new $productClassName;
}
}
//开始操作
//新建抽象工厂对象
$abstractCreator = new abstractCreator();
//根据归属,得到具体工厂对象,这里先演示敌人的
$realCreator1 = $abstractCreator->getCreator('enemy');
//让具体工厂对象生产鼠标对象
$product1 = $realCreator1->creatProduct('Mouse');
//让鼠标对象显示颜色,显示结果red
echo $product1->color;

//根据归属,得到另一个具体工厂对象,这里演示自己的
$realCreator2 = $abstractCreator->getCreator('mine');
//让具体工厂对象生产运输船
$product2 = $realCreator2->creatProduct('Dropship');
//让运输船对象显示装载对象,显示结果2 tanks,两辆坦克
echo $product2->loading;
?>
用途总结:抽象工厂模式将拥有相同属性的产品归类到同一个具体工厂,减少具体工厂的数量,操作的时候,可以理清职责。
实现总结:需要一个根据属性返回具体工厂对象的抽象工厂,比如上面abstractCreator,同时需要将各个产品的属性(自己的,敌人的)进行归类,根据属性建立各个具体工厂,每个具体工厂制造多个具有相同属性的不同产品(鼠标和运输船)。

/***************************************************************/

4-单件和单态模式

星际争霸允许玩家作弊,当然这是在人和电脑对战的时候。而且作弊有个特点,比如快速建造,能量无限是对所有的玩家(包括电脑)都生效,如果关闭了作弊,对所有的玩家的作用都同时消失。
这也就是说如果我们把作弊状态作为一个类,他只能有一个对象。
待解决的问题:确保某个类只能有一个对象。
思路:把对外新建对象的权利都收回,包括new,clone。为了防止通过子类来覆盖父类的方法和成员,将类设置为final。用static成员来保存唯一的对象
单件模式示例:
<?php
//将类设置为final,禁止其他类继承它
final class cheat {
//快速建造的生效状态,用private保护
private $fastBuild = false;
//用static成员来保存唯一的对象
private static $instance;
//设置快速建造的生效状态的方法,用public为了能够公开调用
public function setStatus($input)
{
        //如果输入的秘籍正确,operation cwal是快速建造的秘籍
        if($input === 'operation cwal')
        {
        //像开关一样,逆反状态
        $this->fastBuild = !$this->fastBuild ;
        }
}
//读取快速建造的生效状态的方法,用public为了能够公开调用
public function getStatus()
{
    return $this->fastBuild ;
}
//获取唯一对象的唯一方法
public function getInstance()
{
        //如果对象没有被新建,则新建它
        if(!isset(self::$instance))
        {
        self::$instance = new cheat() ;
        }
    return self::$instance ;
}
//用private来禁止在本类以外new对象
private function __construct(){}
//用private来禁止在本类以外clone对象
private function __clone(){}
}
//获取作弊对象的唯一办法
$cheatInstance = cheat::getInstance();
//现在输出为0(这和操作系统有关,有些可能输出false,最好用var_dump来代替echo)
echo $cheatInstance->getInstance();
//输入秘籍
$cheatInstance->setInstance('operation cwal');
//现在输出为1(这和操作系统有关,有些可能输出true,最好用var_dump来代替echo)
echo $cheatInstance->getInstance();
//再次输入秘籍,取消作弊
$cheatInstance->setInstance('operation cwal');
//现在输出为又变成0(这和操作系统有关,有些可能输出false,最好用var_dump来代替echo)
echo $cheatInstance->getInstance();
?>

星际里面有些特殊的秘籍,比如无敌和增加矿,这是仅仅对于人玩家产生作用,对电脑玩家没有作用(大家都无敌还怎么玩)。我们希望有个人类作弊的类来继承作弊类,而子类中新增一些秘籍,比如无敌。单件模式不允许继承,我们可以采用单态模式。
单态模式不是通过唯一对象来保持一致,它将相关的成员设置为static,这样即使存在很多个它的对象,但它们共享成员,保持状态的一致。同时也允许继承。
<?php
//不使用final,允许继承
class cheat {
//快速建造的生效状态,用private保护,同时设置static让所有的作弊对象共享
private static $fastBuild = false;
//设置快速建造的生效状态的方法,用public为了能够公开调用
public function setStatus($input)
{
        //如果输入的秘籍正确,operation cwal是快速建造的秘籍
        if($input === 'operation cwal')
        {
        //像开关一样,逆反状态
        self::$fastBuild = !self::$fastBuild ;
        }
}
//读取快速建造的生效状态的方法,用public为了能够公开调用
public function getStatus()
{
    return self::$fastBuild ;
}
}

//新增一个作弊对象
$cheatInstance1 = new cheat();
//现在输出为0(这和操作系统有关,有些可能输出false,最好用var_dump来代替echo)
echo $cheatInstance1->getInstance();
//输入秘籍
$cheatInstance1->setInstance('operation cwal');
//现在输出为1(这和操作系统有关,有些可能输出true,最好用var_dump来代替echo)
echo $cheatInstance1->getInstance();
//新增一个作弊对象
$cheatInstance2 = new cheat();
//现在也是输出为1,因为它们共享$fastBuild
echo $cheatInstance2->getInstance();
?>
作弊的子类可以新增一些成员,这里不再详述。
单件模式用途总结:确保某个类的对象的唯一性,常用来节省系统的资源,比如防止重复的数据库连接。
单件模式实现总结:收回所有新建对象的权利,把对象放入类的一个private成员中,仅提供一个对外的新建对象的方法,调用的时候判断对象是否已经新建

单态模式用途总结:确保某个类所有的对象的成员都一致,同时允许灵活的继承这各类。但相对单件模式而言,系统资源开销要大一些。
单态模式实现总结:把所有相关的成员设置为static。

/***************************************************************/

5-模板模式

星际中的虫族部队有个特别的进化兵种,就是飞龙,飞龙可以变成空中卫士(天蟹)或者吞噬者(对空的)。另外还有口水兵可以进化变成地刺。
这三个变化过程都是类似的:变化的原部队消失,产生一个蛋或茧,孵化一会儿,蛋消失,新的部队产生。

如果我们把这三个进化独立开,就会产生重复的代码,冗余度增大了,所以我们要设法减少多余的代码。
待解决的问题:要经历同样的几个步骤,只是每个步骤的细节会有不同。

思路:做一个进化工程的框架,我们只要控制细节就可以了。
模板模式模式示例:
<?php
//进化的框架类,它是个抽象类
abstract class evolution {
    //框架方法,由它来实施各个步骤,用final禁止子类覆盖
final public function process($troop)
{
    生成一个蛋,参数为进化的部队
    $egg = $this->becomeEgg($troop);
    等待蛋孵化,参数为蛋
    $this->waitEgg($egg);
    孵化后产生新部队
    return $this->becomeNew($egg);
}
下面三个抽象方法,由具体子类来实现
abstract public function becomeEgg($troop);
abstract public function waitEgg($egg);
abstract public function becomeNew($egg);
}
为了简单说明,这里用空中卫士(天蟹)的进化类来演示,地刺等的处理方法类似
//天蟹的进化类继承抽象进化类
class GuardianEvolution extends evolution {
    //实现生成一个蛋
public function becomeEgg($troop)
{
   //销毁飞龙,返回一个蛋的对象的代码
}
    //等待蛋孵化
public function waitEgg($troop)
{
   //等待几十秒钟的代码
}
    //孵化后产生新部队
public function becomeNew(($troop)
{
   //销毁蛋,返回一个天蟹
}
}
//新建一个天蟹进化的对象
$e1 = new GuardianEvolution();
//让它调用父类的进化框架函数,自动完成三个步骤
$e1->process($sds);
?>

用途总结:模板模式可以将一系列的步骤自动化,同时又可以满足不同的细节变化。
实现总结:需要一个抽象类来包含框架函数,让具体的子类继承它,并实现所有的步骤。使用的时候只要调用框架函数就自动完成了。

/***************************************************************/

6-正面模式

星际里面的战斗都是在地图上进行的,只要我们可以编辑地图,就可以创造一些新的战役。可是,星际里面的地图绘制相关的代码如果开放出来,估计大多数万家都看不懂,更不要说自己编辑地图了。

待解决的问题:在不了解地图代码的结构下,我们要让玩家自己编辑地图。

思路:对于玩家而言,他熟悉的是水晶矿,高地这些形状,他和系统通过鼠标交互。我们可以设计一个地图编辑器让玩家使用,而无需让他研究绘制地图的
细节代码。(实际上暴雪公司就是这样做的,很多玩家甚至暴雪内部人员都是用星际中的地图编辑器制作地图)

正面模式(Facade)示例:
<?php
//玩家的鼠标对象,记录鼠标在编辑其中的状态
class mouse {
//鼠标所处的X轴坐标
public static $X;
//鼠标当前能绘制的对象,比如水晶矿,河流等
public static $object;
//鼠标所处的Y轴坐标
public static $Y;
}
//地图编辑器
class mapEdit {
    //绘制方法
public static function draw()
{
    //根据鼠标对象的状态在地图上绘制各种东西
    //如果是水晶矿
    if(mouse::$object == "ore")
    {
    //调用水晶矿类的绘制方法,这个类定义在下面,这是真正的绘制,但玩家不必学习他的细节
    ore::draw();
    //如果是河流
    }elseif(mouse::$object == "river"){
    //调用河流类的绘制方法,这个类定义在下面,这是真正的绘制,但玩家不必学习他的细节
    river::draw();
    }
}
}
//水晶矿类
class ore {
    //剩余的矿,以及其他属性,这里略过
public $remain;
    //绘制水晶矿
public static function draw()
{
   //实际的绘制水晶矿的底层细节代码
}
}
//河流类
class river {
    //绘制河流
public static function draw()
{
   //实际的绘制河流的底层细节代码
}
}

//玩家在地图编辑器上点击绘制对象列表上的水晶矿对象
mouse::$object = "ore";
//玩家移动鼠标
mouse::$X = 311;
mouse::$Y = 126;
//在地图上点击,表示绘制当前对象,也就是一个水晶矿
mapEdit::draw();
?>

用途总结:正面模式让使用者集中于他所要进行的工作,而不必知道全部细节,或者说提供了一个容易使用的工具,同时屏蔽了底层细节,不必让使用者重新学习。
实现总结:需要一个类似上面地图编辑器的代码类,帮助玩家方便的进行操作。

/***************************************************************/

7-观察者模式

当我们在星际中开地图和几家电脑作战的时候,电脑的几个玩家相当于结盟,一旦我们出兵进攻某一家电脑,其余的电脑会出兵救援。
那么如何让各家电脑知道自己的盟友被攻击了呢?并且自动做出反应?
待解决的问题:一旦某个电脑被我们进攻,其他电脑就获知,并且自动出兵救援。
思路:为电脑设置一些额外的观察系统,由他们去通知其他电脑。

观察者(Observer)模式示例:
<?php
//抽象的结盟类
abstract class abstractAlly {
//放置观察者的集合,这里以简单的数组来直观演示
public $oberserverCollection;
    //增加观察者的方法,参数为观察者(也是玩家)的名称
public function addOberserver($oberserverName)
{
    以元素的方式将观察者对象放入观察者的集合
    $this->oberserverCollection[] = new oberserver($oberserverName);
}
//将被攻击的电脑的名字通知各个观察者
public function notify($beAttackedPlayerName)
{
        //把观察者的集合循环
        foreach ($this->oberserverCollection as $oberserver)
        {
        //调用各个观察者的救援函数,参数为被攻击的电脑的名字,if用来排除被攻击的电脑的观察者
        if($oberserver->name != $beAttackedPlayerName) $oberserver->help($beAttackedPlayerName);
        }
}
abstract public function beAttacked($beAttackedPlayer);
}
//具体的结盟类
class Ally extends abstractAlly {
    //构造函数,将所有电脑玩家的名称的数组作为参数
public function __construct($allPlayerName)
{
        //把所有电脑玩家的数组循环
        foreach ($allPlayerName as $playerName)
        {
        //增加观察者,参数为各个电脑玩家的名称
        $this->addOberserver($playerName);
        }
}
//将被攻击的电脑的名字通知各个观察者
public function beAttacked($beAttackedPlayerName)
{
     //调用各个观察者的救援函数,参数为被攻击的电脑的名字,if用来排除被攻击的电脑的观察者
     $this->notify($beAttackedPlayerName);
}
}
//观察者的接口
interface Ioberserver {
//定义规范救援方法
function help($beAttackedPlayer);
}
//具体的观察者类
class oberserver implements Ioberserver {
//观察者(也是玩家)对象的名字
public $name;
    //构造函数,参数为观察者(也是玩家)的名称
public function __construct($name)
{
    $this->name = $name;
}
//观察者进行救援的方法
public help($beAttackedPlayerName)
{
        //这里简单的输出,谁去救谁,最后加一个换行,便于显示
        echo $this->name." help ".$beAttackedPlayerName."<br>";
}
abstract public function beAttacked($beAttackedPlayer);
}
//假设我一对三,两家虫族,一家神族
$allComputePlayer = array('Zerg1', 'Protoss2', 'Zerg2');
//新建电脑结盟
$Ally = new Ally($allComputePlayer);
//假设我进攻了第二个虫族
$Ally->beAttacked('Zerg2');
?>

用途总结:观察者模式可以将某个状态的变化立即通知所有相关的对象,并调用对方的处理方法。
实现总结:需要一个观察者类来处理变化,被观察的对象需要实现通知所有观察者的方法。

 

抱歉!评论已关闭.