15-状态模式
星际的一些兵种会有不止一种状态,比如坦克可以架起来,枪兵可以打兴奋剂,甚至还有一些被动的,比如被虫族女王喷洒绿色液体后,敌人的行动变慢。
如果按照一般的思路,每次我们对一个小兵进行操作的时候,比如一辆坦克,我们都要用if判断他的状态,这样代码中会有很多的if,else或者swith。
不过我们可以发现,我们需要的是他在某个状态下的行为,如果把这些行为按照状态封装起来,就可以减少大量的判断。
待解决的问题:封装坦克的状态,让状态自己去控制行为。
思路:把状态作为属性,兵种类本身只控制状态的变化,具体的行为由状态类定义。
状态(State)模式示例:
<?php //坦克状态的接口 interface TankState { //坦克的攻击方法 public function attack(); } //坦克普通状态 class TankState_Tank implements TankState { //坦克的攻击方法 public function attack() { //这里简单的输出当前状态 echo "普通状态"; } } //坦克架起来的状态 class TankState_Siege implements TankState { //坦克的攻击方法 public function attack() { //这里简单的输出当前状态 echo "架起来了"; } } //坦克类 class Tank { //状态 public $state; //坦克的攻击方法 public function __construct() { //新造出来的坦克当然是普通状态 $this->state = new TankState_Tank(); } //设置状态的方法,假设参数为玩家点击的键盘 public function setState($key) { //如果按了s if($key = 's') { $this->state = new TankState_Siege(); } //如果按了t elseif($key = 't') { $this->state = new TankState_Tank(); } } //坦克的攻击方法 public function attack() { //由当前状态自己来处理攻击 $this->state->attack(); } } //新造一辆坦克 $tank = new Tank(); //假设正好有个敌人路过,坦克就以普通模式攻击了 $tank->attack(); //架起坦克 $tank->setState('s'); //坦克再次攻击,这次是架起模式 $tank->attack(); ?>
用途总结:状态模式可以将和状态相关的行为和属性封装,除了切换状态时,其它地方就不需要大量的判断当前状态,只要调用当前状态的方法等。
实现总结:用一个接口规范状态类需要实现的方法,比如上面的TankState规定了attack()。把各个状态封装成类,将不同状态下的不同方法放入各自的状态类,比如上面的攻击方法,同时所有的状态执行接口。原来的事务类,比如上面的Tank类,只负责状态切换,一旦需要某一个方法的调用,只要交给当前状态就可以了。/***************************************************************/
16-中介者模式
星际的升级系统做得比较平衡,不过由于不少兵种和建筑的制造都需要有相关的科技建筑,所以关系比较复杂。
比如一个科学站造出来后,所有的飞机场都可以建造科技球了,但是一旦一个科学站被摧毁,就要看是否还有科学站,否则就得让所有的飞机场都不能造科技球。
我们可以用上次说的观察者模式解决问题,不过由于星际里面的升级相关比较多,似乎比较麻烦。
其实从实质来讲,任何升级一般只要知道某种建筑是否存在就行了,因此我们不必让他们多对多联系,设置一个中介者就行了。
这就好像我们不管买什么东西,到超市就可以了,而厂家也只要和超市联系,不必和我们每个消费者直接接触。
待解决的问题:不要让各个建筑互相联系,减少复杂程度。
思路:设置中介者,每次遇到制造科技相关的东西,询问中介者。中介者(Mediator)模式示例:
<?php //中介者 class Mediator { //存放科技建筑的数量,为了简单说明,用静态属性,其实也可以让各个对象来处理 public static $techBuilding; //根据参数$techBuildingName代表的建筑名称,返回是否存在相应的科技建筑,为了简单说明,用静态属性 public static function isTechAllow ($techBuildingName) { //如果科技建筑数量大于零,就返回true,否则返回false return self::$techBuilding[$techBuildingName]>0; } //一旦科技建筑造好了或者被摧毁,调用这个方法,参数$techBuildingName代表建筑名称,$add为布尔值,true表示增加(建造),false代表减少(摧毁) public static function changeTech ($techBuildingName, $add) { //建造 if ($add) { //增加数量 self::$techBuilding[$techBuildingName]++; } else { //减少数量 self::$techBuilding[$techBuildingName]--; } } } //科技站类 class ScienceFacility { //构造方法 public function __construct() { Mediator::changeTech('ScienceFacility', true); } //析构方法 public function __destruct() { Mediator::changeTech('ScienceFacility', false); } } //飞机场类 class Starport { //制造科技球的方法 public function createScienceVessel () { //询问中介者,决定是否能制造科技球 echo Mediator::isTechAllow('ScienceFacility')?'可以制造科技球':'不能制造科技球'; } } //造一个科技站 $scienceFacility1 = new ScienceFacility(); //再造一个科技站 $scienceFacility2 = new ScienceFacility(); //造一个飞机场 $starport = new Starport(); //建造科技球,结果是能够 $starport->createScienceVessel(); //一个科技站被摧毁 unset($scienceFacility1); //这时建造科技球,结果是能够,因为还有一个科技站 $starport->createScienceVessel(); //另一个科技站被摧毁 unset($scienceFacility2); //这时建造科技球,结果是不行 $starport->createScienceVessel(); ?>
用途总结:中介者模式可以减少各个对象的通讯,避免代码相互关联。
实现总结:中介者模式比较灵活,一般只要有中介者类和需要被协调的类,具体设计看遇到的问题。/***************************************************************/
17-适配器模式
星际的很多兵种,都有至少一项特殊技能。而且有些兵种的技能是相同的,比如虫族部队都会恢复血。
如果按照一般的思路,把技能的操作和控制作为方法,放在每个兵种的定义类来实现,代码会重复,也不容易修改。
那我们就会考虑用继承的办法,比如我们可以设计一个虫族的基类,里面有受伤后血恢复的方法。
在设计刺蛇(Hydralisk,口水兵)的时候,我们可以让刺蛇类继承虫族基类。
但是刺蛇是可以研发钻地的,而钻地不是刺蛇独有的功能,是虫族地面部队都有的特点,我们也要把钻地作为公共基类。
问题出来了,我们不能同时让刺蛇类继承两个类,这是php不允许的。待解决的问题:如何混合重用两个类,
思路:继承一个类,把新建其中一个类的对象作为属性,然后通过这个属性来调用第二个类的方法。适配器(Adapter)模式示例:
<?php //虫族基类 class Zerg { //血 public $blood; //恢复血的方法 public function restoreBlood() { //自动逐渐恢复兵种的血 } } //钻地的类 class Burrow { //钻地的方法 public function burrowOperation() { //钻地的动作,隐形等等 echo '我钻地了'; } } //刺蛇的类 class Hydralisk extends Zerg { //把一个属性来存放钻地对象 public $burrow; //构造方法,因为php不允许默认值采用对象,所以通过初始化赋值给$burrow public function __construct() { $this->burrow=new Burrow(); } //钻地的方法 public function burrowOperation() { //调用钻地属性存放的对象,使用钻地类的方法 $this->burrow->burrowOperation(); } } //制造一个刺蛇 $h1 = new Hydralisk(); //让他钻地 $h1->burrowOperation(); ?>
用途总结:适配器模式使得一个类可以同时使用两个基础类的功能,跳出了单纯继承的限制。有效的重用多各类。
实现总结:让新的类去继承一个基础类,然后通过新类的属性来存放其他类的对象,通过这些对象来调用其他类的方法。/***************************************************************/
18-备忘模式
我们在玩星际任务版或者单机与电脑对战的时候,有时候会突然要离开游戏,或者在出兵前面,需要存储一下游戏。
那么我们通过什么办法来保存目前的信息呢?而且在任何时候,可以恢复保存的游戏呢?待解决的问题:保存游戏的一切信息,如果恢复的时候完全还原。
思路:建立一个专门保存信息的类,让他来处理这些事情,就像一本备忘录。
为了简单,我们这里用恢复一个玩家的信息来演示。备忘(Memento)模式示例:
<?php //备忘类 class Memento { //水晶矿 public $ore; //气矿 public $gas; //玩家所有的部队对象 public $troop; //玩家所有的建筑对象 public $building; //构造方法,参数为要保存的玩家的对象,这里强制参数的类型为Player类 public function __construct(Player $player) { //保存这个玩家的水晶矿 $this->ore = $player->ore; //保存这个玩家的气矿 $this->gas = $player->gas; //保存这个玩家所有的部队对象 $this->troop = $player->troop; //保存这个玩家所有的建筑对象 $this->building = $player->building; } } //玩家的类 class Player { //水晶矿 public $ore; //气矿 public $gas; //玩家所有的部队对象 public $troop; //玩家所有的建筑对象 public $building; //获取这个玩家的备忘对象 public function getMemento() { return new Memento($this); } //用这个玩家的备忘对象来恢复这个玩家,这里强制参数的类型为Memento类 public function restore(Memento $m) { //水晶矿 $this->ore = $m->ore; //气矿 $this->gas = $m->gas; //玩家所有的部队对象 $this->troop = $m->troop; //玩家所有的建筑对象 $this->building = $m->building; } } //制造一个玩家 $p1 = new Player(); //假设他现在采了100水晶矿 $p1->ore = 100; //我们先保存游戏,然后继续玩游戏 $m = $p1->getMemento(); //假设他现在采了200水晶矿 $p1->ore = 200; //我们现在载入原来保存的游戏 $p1->restore($m); //输出水晶矿,可以看到已经变成原来保存的状态了 echo $p1->ore; ?>
用途总结:备忘模式使得我们可以保存某一时刻为止的信息,然后在需要的时候,将需要的信息恢复,就像游戏的保存和载入归档一样。
实现总结:需要一个备忘类来保存信息,被保存的类需要实现生成备忘对象的方法,以及调用备忘对象来恢复自己状态的方法。/***************************************************************/
19-组合模式
星际里面我们可以下载别人制作的地图,或者自己做地图玩。
我们在选择玩哪张地图的时候,可以看到游戏列出当前地图包里面的地图或地图包的名字。
虽然地图和地图包是通过文件和文件夹区分的,但是我们开发的时候,总希望能使用对象来进行抽象。
那么对于地图和地图包这两个相关的对象,我们能不能简化他们之间的区别呢?待解决的问题:尽量是调用这两种对象的代码一致,也就是说很多场合不必区分到底是地图还是地图包。
思路:我们做一个抽象类,让地图类和地图包类继承它,这样类的很多方法的名称一样。组合(Composite)模式示例:
<?php //抽象地图类 abstract class abstractMap { //地图或地图包的名称 public $name; //构造方法 public function __construct($name) { $this->name = $name; } //地图或地图包的名称,地图对象没有子对象,所以用空函数,直接继承 public function getChildren(){} //添加子对象,地图对象没有子对象,所以用空函数,直接继承 public function addChild(abstractMap $child){} //显示地图或地图包的名称 public function showMapName() { echo $this->name."<br>"; } //显示子对象,地图对象没有子对象,所以用空函数,直接继承 public function showChildren(){} } //地图类,继承抽象地图,这里面我们暂且使用抽象地图的方法 class Map extends abstractMap { } //地图包类,继承抽象地图,这里面我们就需要重载抽象地图的方法 class MapBag extends abstractMap { //子对象的集合 public $childern; //添加子对象,强制用abstractMap对象,当然地图和地图包由于继承了abstractMap,所以也是abstractMap对象 public function addChild(abstractMap $child) { $this->childern[] = $child; } //添加子对象 public function function showChildren() { if (count($this->childern)>0) { foreach ($this->childern as $child) { //调用地图或包的名称 $child->showMapName(); } } } } //新建一个地图包对象,假设文件夹名字为Allied,这个大家可以看看星际的地图目录,真实存在的 $map1 = new MapBag('Allied'); //新建一个地图对象,假设文件名字为(2)Fire Walker(也是真实的) $map2 = new Map('(2)Fire Walker'); //接下去可以看到组合模式的特点和用处。 //假设后面的代码需要操作两个对象,而我们假设并不清楚这两个对象谁是地图,谁是地图包 //给$map1添加一个它的子对象,是个地图,(4)The Gardens $map1->addChild(new Map('(4)The Gardens')); //展示它的子对象 $map1->showChildren(); //给$map2添加一个它的子对象,是个地图,(2)Fire Walker,这里不会报错,因为地图继承了一个空的添加方法 $map2->addChild(new Map('(2)Fire Walker')); //展示它的子对象,也不会出错,因为地图继承了一个空的展示方法 $map2->showChildren(); ?>
用途总结:组合模式可以对容器和物体(这里的地图包和地图)统一处理,其他代码处理这些对象的时候,不必过于追究谁是容器,谁是物体。这里为了简化说明,没有深入探讨,其实组合模式常常用于和迭代模式结合,比如我们可以用统一的方法(就像这里的showChildren方法),获取地图包下所有的地图名(包括子目录)
实现总结:用一个基类实现一些容器和物体共用的方法,比如上面的abstractMap,然后让容器和物体类继承基类。由于各自的特性不同,在容器和物体类中重载相应的方法,比如addChild方法。这样对外就可以用统一的方法操作这两种对象。/***************************************************************/
20-桥接模式
在面向对象设计的时候,我们一般会根据需要,设计不同的类。但是如果两个类需要类似的功能的时候,我们就会遇到问题,到底重用还是重写。
更复杂的是,如果一些功能需要临时转换就麻烦了,比如星际里面的虫族,地面部队可以钻到地下,然后隐形。
但是小狗在地下不能动,而地刺可以攻击。尽管可以设计不同的类,但问题是玩家可以看到自己的部队埋在地下(一个洞),而敌人看不到。
这涉及功能的切换,而且星际里面有很多探测隐形的东西,这样就更频繁了。待解决的问题:我们要临时切换一些功能的实现方式,而且在此基础上不同的调用类又有所不同。
思路:将钻地区分两种实现,不同的部队类在它的基础上进一步扩展。桥接(Bridge)模式示例:
<?php //虫族的基础类 class Zerg { //实现钻地的基本对象 public $imp; //负责切换钻地基本对象的方法 public function setImp($imp) { $this->imp = $imp; } //部队的钻地方法,可以扩展基本对象的钻地 public function underground() { $this->imp->underground(); } } //小狗的类 class Zergling extends Zerg { //调用基本的钻地方法,然后实现扩展,这里简单的echo public function underground() { parent::underground(); echo '小狗不能动<br>'; } } //地刺的类 class Lurker extends Zerg { //调用基本的钻地方法,然后实现扩展,这里简单的echo public function underground() { parent::underground(); echo '地刺能够进行攻击<br>'; } } //钻地的基本接口 interface Implementor { //基本的钻地方法 public function underground(); } //隐形钻地的基本类 class InvisibleImp implements Implementor { //基本的钻地方法 public function underground() { echo '隐形了,什么也看不到<br>'; } } //不隐形钻地的基本类,比如玩家自己看到的或被探测到的 class VisibleImp implements Implementor { //基本的钻地方法 public function underground() { echo '地上一个洞<br>'; } } //造一个小狗 $z1 = new Zergling(); //玩家把它埋入前沿阵地,假设此时有敌人经过,代码进行切换 $z1->setImp(new InvisibleImp()); //敌人看不到小狗,但是小狗也不能进攻 $z1->underground(); //造一个地刺 $l1 = new Lurker(); //玩家把它埋入前沿阵地,假设此时有敌人经过,代码进行切换 $l1->setImp(new InvisibleImp()); //敌人看不到地刺,但是地刺能攻击敌人 $l1->underground(); //敌人急了,马上飞过来一个科技球,代码进行切换 $l1->setImp(new VisibleImp()); //敌人看到地刺了,地刺继续攻击敌人 $l1->underground(); ?>用途总结:桥接模式可以将基本的实现和具体的调用类分开,调用类可以扩展更复杂的实现。
实现总结:需要一些基本执行类,实现基本的方法,比如上面的两个钻地类。同时我们可以设计多个不同的扩展调用类,将基本的功能扩展,比如地刺和小狗就进一步实现了不同的在地下的行为。