深入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
那么这个组合模式有没有什么缺陷呢?有,就是在Archer
和LaserCannonUnit
中引入了两个没用的方法: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个装饰类TroopCarrierDecorator
,LaserCannonUnitDecorator
,ArmyDecorator
。
这里我们在不改变原先类的代码上,增加了我们后期的限制,而且随着外部的调用,我们随时可以去掉对应的限制。是不是很爽!
下面记录一下书上的例子,其实书上的例子也很好:
示例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
的新类?肯定不行,因为鬼知道我们还会有多少子类,要是针对他们的组合都创建一个组合的类,那类的数量肯定爆炸。
使用上面的组合模式?也不行,因为DiamonPlains
和PollutedPlains
本身没有财富值,他们只能在一个基础的财富值上进行增加或者减少。
示例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个名词好像:黄金大锤和神仙大类。不可否认的是设计模式只是优化,是对未来可能发生的场景做一个提前的防范,如果你的产品都没有未来,甚至未来的修改超出你的预期,那么重构是必然的,所以不必觉得你使用设计模式的代码就牛,别人的代码就垃圾,只能说侧重点不同,看好客户需求才是王道!