装饰模式可以动态的加入程序功能,避免因为过度子类化带来的耦合,相比较用继承方式的静态,装饰更为灵活.
意图
动态地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类更为灵活。 --GOF
动态是指在程序运行时来决定,而静态则是在编译时就已经确定,例如使用组合的方式,可以动态决定功能,但如果使用继承,则是在编译时就要确定,这也就是在一些场景中推荐使用组合的原因.装饰模式与我们上一篇所讲的桥接模式,都是用了组合方式来解决因为多个变化维度导致的多继承的问题.
解决的是什么问题
装饰模式是结构型模式,解决的问题是因为功能在多个维度上存在变化,为了避免多继承而产生的,相对于多继承实现多功能扩展,装饰可以灵活的扩展,也可以让子类职责清晰,不会顺着一个方向变化膨胀.按照惯例,我们还是要举例说明,同样还是之前用过的红警游戏.
举例说明
游戏中有不同的坦克,比如天启,光棱,幻影,灰熊等,我们在上一节中实现了坦克的绘制,开火,发动等基础.现在需求来了,我们需要给这些坦克加入一些功能,例如卫星定位,炮弹消音,甚至挖掘功能,当然这里的功能也许也现实情况不符合,目的只是为说明模式,需要说明的是,加入的是功能,这个功能是有一定重量的,比如隐身和变色这种应该不属于功能一级,而是坦克自身的属性,功能级别是如卫星定位,炮弹消音这种需要一系列工作组件来协作的.
初期分析
游戏中的坦克类型不是固定的,所以为了适应以后的变化,抽象化一个Tank基类来实现,至于不同的坦克,天启,光棱等都可以由不同的子类来实现,这样在需要加入新的坦克类型时,只需要扩展方式增加子类既可,符合开闭原则
关系结构如图所示,这是我们的主体类,现在要加入新的功能来丰富坦克,卫星定位,炮弹消音,挖掘,而且某一个坦克有什么新功能是不一定的,也就是有可能天启坦克会有卫星定位,也有可能会炮弹消音,也可能要变挖掘机了,也有可能是只基中的2种功能,或者三种功能的组合..光棱坦克也是一样.
膨胀的子类,臃肿的继承
顺着之前的思路,要加入新的功能,我们考虑将三种功能抽象化为接口,分别为ISatellites(卫星定位),ISilencer(消音),IExcavate(挖掘),然后为天启和光棱坦克加入接口实现.
如图中关系所示,这里象征性的将天启坦克实现了消音功能,将光棱坦克实现了挖掘与定位功能.如果按照我们的需求,这将是一个排列组合,我们将要实现好多种坦克子类,大约有,消音的天启,能定位的天启,能挖掘的天启.消音+定位的天启,消音+定位+挖掘的天启.....等,试问如果这样实现下去,子类结构是不是膨胀了,如果面临修改时将是一个恶梦,而且继承结构也很复杂,显的很臃肿,这也是一个典型的继承滥用的例子,如果那样我们的关系结构图大概会变成这个样子.
大量的实现子类的出现让我们的关系结构杂乱不堪,图中并未将所有排列都画出.可想而知,如果再加一种坦克呢?
模式的引入
之所以有这样的问题,其实就是有不同维度的功能变化,我们的主体Tank是顺着坦克的不同类型来变化,而新加入的功能有着各种组合关系,还各种有着不同的实现内容,如果将这些变化混在一起,就变成了上面的样子.当然并不是说继承就一定有错,只不过是在一些变化的点上需要用更为灵活的动态方式来实现,我们的例子中Tank与它的子类之间继承就是稳固的,而另一条线扩展功能一系列接口(消音,定位..)则是不稳定的,也就是说一个坦克到底有什么新功能,希望更灵活的来改变,而不是以代码的形式将它定死在系统中.理解这一层动态与静态的概念,是整个装饰模式的重要基础.
如装饰模式的意图中所述,装饰模式可以动态的加入程序功能,避免因为过度子类化带来的耦合,相比较用继承方式的静态,装饰更为灵活.我们需要将新加入的三个坦克功能变成动态加入,而非继承实现.这样就不用写一大堆的实现类,同时还需要说明一点,装饰模式的使用是不会破坏主体类的结构,也就是说Tank类与它的子类们是不会感知到自己被装饰了,加入了新的功能,这一点至关重要,满足这一点,才是真的解耦.而继承方案中,是以侵入的方式强迫Tank的子类们来实现一些与它主职没什么关系的内容.这也是违背单一职责原则的.
其实软件模式中最基础的考虑都是找出变化点,抽象化封装稳定的部分,让变化的部分由不同的实现来做,如果发现变化的方向有不只一个,那就要让它们分别对待,让各自沿着自己的方向去演变,如上图所示,我们将新引入的三个坦克功能做了一个抽象基类TankDecorate,代表所有要用来装饰坦克功能的类的基础装饰,同时也是一个容器,可以看到它与Tank之间既有组合也有继承,这可能会让大伙感觉到不明其意,就像前边所讲,继承并不是天生就有错的,只是用错了场景才会有问题,这里的继承是为了让TankDecorate能够知道坦克的原生动作,才能在调用加一层包装,这一点与AOP面向切面编程时有一点相似.而组合关系则需要合到一个被包装的Tank的实例.这里需要用心体会一下,也就是装饰类既伪装成坦克,但是并不实再具体内容,只是在其的关键业务方法时加入动作而已.
SatellitesTank,ExcavateTank,SilencerTank实现了具体的装饰功能,而TankTqianqiImpl与TankGuanglengImpl并未有任何修改.
代码示例
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 坦克基类
*
* @author aladdinty
* @create 2018-01-30
**/
public abstract class Tank
{
/**开炮*/
public abstract void fire() ;
}
坦克基类,定义开火动作.
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 天启实现
*
* @author aladdinty
* @create 2018-01-30
**/
public class TankTianqiImpl extends Tank
{
@Override
public void fire ()
{
System.out.println ("天启坦克开炮了");
}
}
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 光棱实现
*
* @author aladdinty
* @create 2018-01-30
**/
public class TankGuanglengImpl extends Tank
{
@Override
public void fire ()
{
System.out.println ("光棱坦克开炮了");
}
}
具体坦克的实现,光棱与天启坦克.
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 装饰基类
*
* @author aladdinty
* @create 2018-01-30
**/
public abstract class TankDecorate extends Tank
{
private Tank tank ;
public TankDecorate( Tank tank )
{
this.tank = tank ;
}
public Tank getTank()
{
return this.tank;
}
}
装饰基类,继承自Tank,然后组合一个Tank实例,这里既有Is A同时还有Has a,其实这是很好的协调了is a 与 ha a ,并无冲突,因为这种继承关系表示的是需要静态绑定Tank的动作,这一点是稳固的.而has a 解决的部分是动态的,所传入的tank实例是需要在它之前之后加入我们的装饰功能的.
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 挖掘装饰
*
* @author aladdinty
* @create 2018-01-30
**/
public class ExcavateTank extends TankDecorate
{
public ExcavateTank (Tank tank)
{
super (tank);
}
@Override
public void fire ()
{
System.out.println ("启动了挖掘功能.");
this.getTank ().fire ();
}
}
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 定位装饰
*
* @author aladdinty
* @create 2018-01-30
**/
public class SatellitesTank extends TankDecorate
{
public SatellitesTank (Tank tank)
{
super (tank);
}
@Override
public void fire ()
{
System.out.println ("启动了定位功能.");
this.getTank ().fire ();
}
}
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 消音装饰
*
* @author aladdinty
* @create 2018-01-30
**/
public class SilencerTank extends TankDecorate
{
public SilencerTank (Tank tank)
{
super (tank);
}
@Override
public void fire ()
{
System.out.println ("启动了消音功能.");
this.getTank ().fire ();
}
}
这三个实现类分别都实现了各自的功能,并且调用原对象的fire方法,这其实就像链表中所涉及到的指针一样,用引用一环套着一环,一个对象持有着下一个对象的引用,同时也有可能被别人持有着自己的引用,让整个对象型成一个链表的结构.只不过我们只体现的部分是加入动作.并未有别的操作.
package com.j2kaka.coolka.examples.pattern.decorate;
/**
* 客户端调用代码
*
* @author aladdinty
* @create 2018-01-30
**/
public class Client
{
public static void main(String [] args )
{
//天启坦克
Tank tank = new TankTianqiImpl () ;
//定位装饰
SatellitesTank sateTank = new SatellitesTank ( tank ) ;
//消音装饰
SilencerTank sileTank = new SilencerTank ( sateTank ) ;
sileTank.fire ();
//光棱坦克
Tank gtank = new TankGuanglengImpl () ;
//定位装饰
SatellitesTank sateGLTank = new SatellitesTank ( gtank ) ;
//消音装饰
SilencerTank sileGlTank = new SilencerTank ( sateGLTank ) ;
//挖掘装饰
ExcavateTank excaGLTank = new ExcavateTank ( sileGlTank ) ;
excaGLTank.fire ();
}
}
调用处代码可以看到,这里就可以灵活的组合使用装饰功能了,一个坦克想消音,还是想挖掘,这些全由调用处来决定,而不需要绑定一个臃肿的类结构了.
动行结果
最后总结
说到最后个人感觉,这个装饰模式就是像一句广告词一样,男人有自己的很多面,今天你要秀出你的哪一面,七匹狼性格男装... 其实是这样,对外的表现不同,但核心并未改变,你我都在这个浮华的社会坚持着自己,曾几何时那个少年望着满天星发呆,一心想着外面的世界很精彩..不管世界怎么变,我希望我们的初心不变,最多不过加了几层装饰吧.