深入PHP面向对象,模式与实践 第10章 让面向对象编程更加灵活的模式

让面向对象编程更加灵活的模式

笔记

组合模式

使用场景

顾名思义,组合模式是适用于组合的,这里的组合不是只类之间功能的组合,而是类对象之间的组合,而且也不是完全不同的类,因为业务联系而组合在一起,而是继承自同一个父类的对象组合在一起。

示例1

<?php

abstract class Unit{
    abstract function bombardStrength();
}
/**
* 射手
**/
class Archer extends Unit{
    function bombardStrength()
    {
        return 4;
    }
}
/**
* 镭射炮
**/
class LaserCannonUnit extends Unit{
    function bombardStrength()
    {
        return 44;
    }
}

/**
 * 军队
 * Class Army
 */
class Army{
    private $units=array();
    function addUnit(Unit $unit){
        array_push($this->units,$unit);
    }
    function bombardStrength(){
        $ret=0;
        foreach ($this->units as $unit){
            $ret+=$unit->bombardStrength();
        }
        return $ret;
    }
}

Army是单独的一个类,其中bombardStrength方法是程序员自己定义的,他现在的只有一个职责,管理Unit

但是我现在希望两个Army之间能相互合作。

这个时候你可以修改Army类,使之能添加Army对象

/**
 * Class Army
 */
class Army{
    private $units=array();
    function addUnit(Unit $unit){
        array_push($this->units,$unit);
    }
    function bombardStrength(){
        $ret=0;
        foreach ($this->units as $unit){
            $ret+=$unit->bombardStrength();
        }
        // 判断相关军队的战斗力
        foreach ($this->armies as $army){
            $ret+=$army->bombardStrength();
        }
        return $ret;
    }
  	// 存储所有的军队
    private $armies=array();
    // 添加军队的接口
    function addArmy(Army $army){
        array_push($this->armies,$army);
    }
}

这个时候我希望再有一个TroopCarrier类*(运货船)*来和Army类配合,这个时候你不得不再次修改你的Army类。看出问题在哪里了吗?对象和对象之间开始产生组合了,TroopCarrier类可能还会和Archer类组合,LaserCannonUnit可能会和Army类组合,这个时候你的Army类已经无法管理这些组合了,这个时候你就可以尝试转换一下思路,为什么我不将每个兵种都当作一个最小单位,即使是Army也只是一个作战单位Unit

示例2

<?php

abstract class Unit{
    abstract function addUnit(Unit $unit);
    abstract function removeUnit(Unit $unit);
    abstract function attack();
}
/**
* 定义一个异常类
**/
class UnitException extends Exception{}


class Archer extends Unit{
    function removeUnit(Unit $unit)
    {
        throw new UnitException("射手无法移除其他兵种");
    }

    function attack()
    {
        return 4;
    }

    function addUnit(Unit $unit)
    {
        throw new UnitException("射手无法添加其他兵种");
    }
}

class LaserCannonUnit extends Unit{
    function removeUnit(Unit $unit)
    {
        throw new UnitException("镭射炮无法移除其他兵种");
    }

    function attack()
    {
        return 44;
    }

    function addUnit(Unit $unit)
    {
        throw new UnitException("镭射炮无法添加其他兵种");
    }

}


class Army extends Unit{
    private $units;
    function removeUnit(Unit $unit)
    {
        $this->units=array_udiff($this->units,array($unit),function ($a,$b){
            return ($a===$b)?1:0;
        });
    }

    function attack()
    {
        $ret=0;
        foreach ($this->units as $unit){
            $ret+=$unit->attack();
        }
        return $ret;
    }

    function addUnit(Unit $unit)
    {
        if(in_array($unit,$this->units,true)){
            return ;
        }
        array_push($this->units,$unit);
    }
}

class TroopCarrier extends Unit{
    function removeUnit(Unit $unit)
    {

    }

    function attack()
    {

    }

    function addUnit(Unit $unit)
    {

    }
}

/**
 * 随意组合都可以
 */
$army=new Army();
$army->addUnit(new Archer());
$army->addUnit(new Archer());
$army->addUnit(new LaserCannonUnit());

$troopCarrier=new TroopCarrier();
$troopCarrier->addUnit($army);

这里我们将Army继承了Unit,使原先特殊的Army类变成了与Archer一样的类,但是同时,Army也有其自身的特性。这样我们就像管理一般的Unit类一样管理Army类,同时TroopCarrier类也是同样的道理。

这里我们就完成了简单的组合模式。再从头看这个问题,我们当时为什么要引入组合模式?因为我们有很多类似的对象需要管理,同时这个管理类也会被类似的类管理,所以我们抽象出了一个抽象类:Unit,让所有的类都继承这个类。

示例3

那么这个组合模式有没有什么缺陷呢?有,就是在ArcherLaserCannonUnit中引入了两个没用的方法:addUnit()removeUnit(),为了解决这个问题,我们引入了一个特殊的异常类UnitException来管理这两个无效的方法。那么为了在一些不需要这些方法的类中不引入这些方法,这里再对代码进行一下修改。

<?php

abstract class Unit{
    abstract function attack();
}

abstract class CompositeUnit extends Unit{
    abstract function addUnit(Unit $unit);
    abstract function removeUnit(Unit $unit);
}

class Archer extends Unit{
    function attack()
    {
        return 4;
    }
}

class LaserCannonUnit extends Unit{
    function attack()
    {
        return 44;
    }
}

class Army extends CompositeUnit{
    function removeUnit(Unit $unit)
    {
        // TODO: Implement removeUnit() method.
    }

    function addUnit(Unit $unit)
    {
        // TODO: Implement addUnit() method.
    }

    function attack()
    {
        // TODO: Implement attack() method.
    }
}

class TroopCarrier extends CompositeUnit{
    function removeUnit(Unit $unit)
    {
        // TODO: Implement removeUnit() method.
    }

    function addUnit(Unit $unit)
    {
        // TODO: Implement addUnit() method.
    }

    function attack()
    {
        // TODO: Implement attack() method.
    }
}

这里我们在把一些组合类中的方法放到另一个单独的抽象类CompositeUnit中,并让需要这些方法的类继承这个类,同时让不需要这些方法的类继承Unit类。这样就可以解决上面的问题。

那么组合模式有没有什么致命的缺陷呢?肯定有,假设现在需求修改为TroopCarrier不能搭载LaserCannonUnit或者2个以上的Army,那么你就需要修改你的TroopCarrier类了。

示例4

class TroopCarrier extends CompositeUnit{
    private $armyCount;
  	// 这里就拿 addUnit 做例子了
    function removeUnit(Unit $unit)
    {
        // TODO: Implement removeUnit() method.
    }

    function addUnit(Unit $unit)
    {
      	// 不允许搭载镭射炮
        if($unit instanceof LaserCannonUnit){
            return ;
        }
       	// 不允许搭载两个以上的 Army
        if ($unit instanceof Army){
            $this->armyCount++;
        }
        if($this->armyCount==2){
            $this->armyCount-=1;
            return;
        }
        
    }

    function attack()
    {
        // TODO: Implement attack() method.
    }
}

可以看到,在addUnit中对类型做了判断:instanceof,随着后期规则的修改,这些判断会越来越多,那么你的代码就会有坏代码的味道了。

借用书上的一句话来总结就是:

  • 简化的前提是使所有的类都继承同一个基类。简化的好处有时会以降低对象类型安全为代价。
  • 在大部分局部对象可互换的情况下,组合模式才是最适用。
  • 但在另一方面,组合模式又依赖于其组成部分的简单性。随着我们引入复杂的规则,代码会变得越来越难以维护。组合模式不能很好地在关系数据库中保存数据,但却非常适合使用XML持久化。

装饰模式

使用场景

组合模式让我们可以对对象进行任意组合,那么装饰模式则可以帮助我们改变具体组件的功能。书上的例子很好,但是这里我原创一个。

示例1

class TroopCarrierDecorator extends CompositeUnit {
    private $troopCarrier;
    public function __construct(CompositeUnit $troopCarrier)
    {
        $this->troopCarrier=$troopCarrier;
    }

    function removeUnit(Unit $unit)
    {
        return $this->troopCarrier->removeUnit($unit);
    }

    function addUnit(Unit $unit)
    {
        return $this->troopCarrier->addUnit($unit);
    }

    function attack()
    {
        return $this->troopCarrier->attack();
    }
}

class LaserCannonUnitDecorator extends TroopCarrierDecorator {
    public function removeUnit(Unit $unit)
    {
        if($unit instanceof LaserCannonUnit){
            return ;
        }
        parent::removeUnit($unit); // TODO: Change the autogenerated stub
    }
    public function addUnit(Unit $unit)
    {
        if($unit instanceof LaserCannonUnit){
            return false;
        }
        return parent::addUnit($unit); // TODO: Change the autogenerated stub
    }
}

class ArmyDecorator extends TroopCarrierDecorator {
    function addUnit(Unit $unit)
    {
        if($unit instanceof Army && in_array($unit,$this->units)){
            return false;
        }
        return parent::addUnit($unit); // TODO: Change the autogenerated stub
    }


    function attack()
    {
        return parent::attack(); // TODO: Change the autogenerated stub
    }
}


$troopCarrier=new ArmyDecorator(
    new LaserCannonUnitDecorator(
        new TroopCarrier()
    )
);

$troopCarrier->addUnit(new Army());
$troopCarrier->addUnit(new Army());

这里我创建了3个装饰类TroopCarrierDecoratorLaserCannonUnitDecoratorArmyDecorator

这里我们在不改变原先类的代码上,增加了我们后期的限制,而且随着外部的调用,我们随时可以去掉对应的限制。是不是很爽!

下面记录一下书上的例子,其实书上的例子也很好:

示例2

<?php

abstract class Tile{
    abstract function getWealthFactor();
}
/**
* 平原 财富值 2
**/
class Plains extends Tile{
    private $wealthFactor=2;
    function getWealthFactor()
    {
        return $this->wealthFactor;
    }
}
/**
* 有钻石矿的平原 财富值+2
**/
class DiamonPlains extends Plains{
    function getWealthFactor()
    {
        return parent::getWealthFactor()+2; // TODO: Change the autogenerated stub
    }
}
/**
* 被污染的平原 财富值-4
**/
class PollutedPlains extends Plains{
    function getWealthFactor()
    {
        return parent::getWealthFactor()-4; // TODO: Change the autogenerated stub
    }
}

配合UML类图来说明:

在这里插入图片描述

这个一般情况下可以满足我们的需求,我们使用子类来扩充了父类的方法,但是现在有一个场地,即有钻石矿,又被污染了,那么如何计算该场地的财富值?

创建一个DiamonPollutedPlains的新类?肯定不行,因为鬼知道我们还会有多少子类,要是针对他们的组合都创建一个组合的类,那类的数量肯定爆炸。

使用上面的组合模式?也不行,因为DiamonPlainsPollutedPlains本身没有财富值,他们只能在一个基础的财富值上进行增加或者减少。

示例3

<?php

/**
 * 上面这部分我们不动
 */
abstract class Tile{
    abstract function getWealthFactor();
}

class Plains extends Tile{
    private $wealthFactor=2;
    function getWealthFactor()
    {
        return $this->wealthFactor;
    }
}

/**
 * 下面创建新的类
 */

abstract class TileDecorator extends Tile{
    protected $tile;
    function __construct(Tile $tile)
    {
        $this->tile=$tile;
    }
}

class DiamondDecrator extends TileDecorator{
    function getWealthFactor()
    {
        return $this->tile->getWealthFactor()+2;
    }
}

class PollutedDecrator extends TileDecorator{
    function getWealthFactor()
    {
        return $this->tile->getWealthFactor()-4;
    }
}


// 一般的平原场地
$plain=new Plains();

print $plain->getWealthFactor();

// 有钻石矿的平原场地
$diamondPlain=new DiamondDecrator(new Plains());

print $diamondPlain->getWealthFactor();

// 有钻石矿又被污染的平原场地
$diamondAndPollutedPlains=new DiamondDecrator(
  new PollutedDecrator(
      new Plains()
  )
);

print $diamondAndPollutedPlains->getWealthFactor();

这里我们创建了2个装饰类来实现平原被污染和有钻石矿的情况。从调用的调用看,我们很容易可以看出创建组合是件非常简单的事情。

为什么装饰类能很简单的创建组合呢?因为原先使用继承的方式,就限定了上下级关系,两个子类之间是相互独立的,无法互相调用的。而在装饰类中,我们只是在最开始的类外面包了一层皮,如果有需要就再包一层,然后给客户端调用的时候,就调用最外面那层皮,就像礼物一样,一层一层的拆,直到拆到我们放到最里面的那个类。

这里也配一样UML的类图来说明一下:

在这里插入图片描述

外观模式

使用场景

这个就是最简单的了,甚至我们常用,只是不知道他的名字而已。

当我们有多个步骤需要执行时,比如生成excel文件,那么我们通常有以下一些操作:

  • 从数据库取文件
  • 组建成我们所需要的格式
  • 调用相关拓展生成excel文件

如果我们的代码中有多个地方需要生成excel文件,那么我们肯定不想在很多地方都写这么多的代码,因为其中很多都是重复的,比如调用相关拓展生成excel文件,你肯定就是调用一下统一的第三方扩展,无非就是修改其中的一些参数配置,所以这个时候你就想简化一下代码。

示例1

<?php

class SalesmanReport{
    public function excelOutput(){
        // 首先写一大段的从数据库获取数据的代码
        // 再来就是调用第三方的拓展来生成excel,比如 PHPExcel
    }
}

class CustomerReport{
    public function outputExcel(){
        // 基本流程与上面一致
        // 再把第三方的库在这里调用一次
    }
}

这个代码是你肯定见过,甚至你肯定写过,这么写的优点就是快啊!自己写的类自己用着贼带劲,但是一旦别人要动你的代码,他估计先得从控制器层往下扒。

示例2

<?php

abstract class ExcelOperate{
    protected $list=[];
    abstract function organizeData();
    public function buildExcel(){
        $this->list=$this->organizeData();
        // 利用这个 $this->list 生成excel的代码就省略了
    }
}

class SalesmanReport extends ExcelOperate{
    function organizeData()
    {
        // 组建数据的代码这里就省略了
        return [];
    }
}


$salesmanReport=new SalesmanReport();
$salesmanReport->buildExcel();

这个也是我们一般做的方式,就是把一些相关联的操作都封装进一个类里面,并对用户隐藏其中的使用,无论用户创建的是SalesmanReport还是CustomerReport,调用的都是buildExcel方法,这就是外观模式。

问题

  • 组合模式适用于什么场景?有什么限制?
  • 组合模式中的示例2,示例3是为了说明什么问题?示例4呢?
  • 解释一下什么是组合模式中的局部对象组合对象
  • 装饰模式适用于什么场景?装饰模式与组合模式之间是什么关系?

如果说组合模式是将相似的类组合在一起使用,那么装饰模式就是在需求发生更改时,将这些相似的类做进一步优化,使原来的调用依然能跑通,但是扩充了原始类的功能。

  • 使用装饰模式时需要注意什么问题?

使用面向对象时绕不开的一个问题,装饰模式很好用,但是如果你的装饰模式只能针对一个特定的子类使用,那么这个装饰模式就显得很鸡肋了,装饰模式的目的就是为类似的类,提供指定的处理,所以你要有大局观。不要为了装饰模式而装饰模式。

  • 外观模式是为了解决什么问题?在什么场景下建议使用外观模式?
  • 使用外观模式需要注意什么问题?

外观模式的出发点是为了减少用户对内部操作的了解,减少耦合。由于他很常用,甚至很常见,导致当用户使用他时,就会忽略其他的设计模式,如果说外观模式是第一步的话,那么在内部你还是需要考虑在外观模式的类中,是否需要使用其他的设计模式。

总结

这一章初看时,你可能会陷入对什么是组合模式,装饰模式,外观模式的了解中,等你了解了之后,你就要回过头来想想,我为什么要使用这些模式,这些模式的目的是为了什么?如果说第9章是讲如何创建对象,那么第10章就是创建完这些对象之后怎么管理了。

并且在这里附上一句,当你学了设计模式之后你可能会发现,你不学你写的代码也能跑,甚至还会有人说你的代码怎么这么难懂,哪像我,什么都写在一个类中,改起来多方便之类的,对于这些人写的类有2个名词好像:黄金大锤和神仙大类。不可否认的是设计模式只是优化,是对未来可能发生的场景做一个提前的防范,如果你的产品都没有未来,甚至未来的修改超出你的预期,那么重构是必然的,所以不必觉得你使用设计模式的代码就牛,别人的代码就垃圾,只能说侧重点不同,看好客户需求才是王道!

发布了184 篇原创文章 · 获赞 72 · 访问量 40万+

猜你喜欢

转载自blog.csdn.net/YQXLLWY/article/details/89077797