设计模式之装饰模式

装饰模式非常强调实现技巧,我们一般用它应对类体系快速膨胀的情况。

在项目中,是什么原因导致类型体系会快速膨胀呢?在多数情况下是因为我们经常要为类型增加新的职责(功能),尤其在软件开发和维护阶段,这方面需求更为普遍。

面向对象中每一个接口代表我们看待对象的一个特定方面。在Java编码实现过程中由于受到单继承的约束,我们通常也会将期望扩展的功能定义为新的接口,进而随着接口不断增加,实现这些接口的子类也在快速膨胀,如新增3个接口的实现,就需要8个类型(包括MobileImpl),4个接口则是16个类型,这种几何基数的增长我们承受不了。为了避免出现这种情况,之前我们会考虑采用组合接口的方式解决,但客户程序又需要从不同角度看待组合后的类型,也就是可以根据里氏替换原则用某个接口调用这个子类。所以面临的问题是,既要has a、又要is a,装饰模式解决的就是这类问题。

经典回顾

装饰模式的意图非常明确:动态为对象增加新的职责。

 

这里有两个关键词:动态和增加,也就是说,这些新增的功能不是直接从父类继承或是硬编码写进去的,而是在运行过程中通过某种方式动态组装上去的。例如:我们在录入文字的时候,最初只需要一个Notepad功能的软件,然后增加了很多需求

字体可以加粗。

文字可以显示为不同颜色。

字号可以调整。

字间距可以调整。

……

不仅如此,到底如何使用这些新加功能,需要在客户使用过程中进行选择,也就是说,新的职责(功能或成员)是需要动态增加的。为了解决这类问题,装饰模式给出的解决方案如图12-1所示。

 

 

根据Notepad的示例要求,设计如图12-2所示的静态结构。

 

 

 

区别于经典装饰模式设计,在图12-2中我们将Decorator定义为具体类,而不是抽象类,主要是为了简化示例结构。

首先,我们需要的是显示文字,这时可以指定一个名为Text的接口,它有名为Content的读/写属性。

然后,我们把所有需要用来装饰的类型抽象出一个名为Decorator的接口,它继承自这个Text,因此其实体类必须实现Content属性方法。

接着,我们把没有任何新增功能的Text做出一个“毛坯”的实体类型,命名为TextImpl

最后,我们把操作字体bold()setColor()的方法填充到每个具体的装饰类型中。

这样,概念上当TextImpl需要某种新增功能时,直接为其套上某个具体装饰类型就可以了。这就好像给TextImpl类穿上一层又一层的“马甲”。该模式这么做主要是为了适用于哪些情景呢?

在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

毕竟客户程序依赖的仅仅是Component接口,至于这个接口被做过什么装饰,只有实施装饰的对象才知道,而客户程序只是依据Component的接口方法调用它,这样,装饰类在给Component穿上新“马甲”的同时也会随着客户程序的调用一并执行了。

屏蔽某些职责,也就是在套用某个装饰类型时,并不增加新的特征,只把既有方法屏蔽。

也就是说,装饰类不仅能否充当“马甲”,也能起到“口罩”的作用,让Component现有的某些方法“闭嘴”。尽管我们使用装饰模式一般是为了增加功能(做“加法”),但并不排斥它也具有方法屏蔽的作用(做“减法”),只不过平时用的比较少而已。

避免因不断调整职责导致类型快速膨胀的情况。

下面看一段示例代码:

Java  抽象部分

public interface Text {

    String getContent();

    void setContent(String content);

}

 

/** implements Text 说明Decorator is a Text*/

public class Decorator implements Text{

 

    /** 构造方式注入has atext接口 */

    private Text text;

    public Decorator(Text text){

        this.text = text;

    }

 

    @Override

    public String getContent(){

        return text.getContent();

    }

   

    @Override

    public void setContent(String content){

        text.setContent(content);  

    }

}

Java  具体装饰类型

 

/**

 * 具体装饰类,属于“马甲”

 */

class BoldDecorator extends Decorator{

   

    public BoldDecorator(Text text){

        super(text);

    }

 

    public String bold(String data){

        return "<b>" + data + "</b>";

    }

   

    @Override

    public String getContent() {   

        return bold(super.getContent());

    }

 

    @Override

    public void setContent(String content) {

        super.setContent(content);

    }

   

}

 

/**

 * 具体装饰类

 * 属于“马甲”

 */

class ColorDecorator extends Decorator{

   

    public ColorDecorator(Text text){

        super(text);

    }

   

    public String setColor(String data){

        return "<color>" + data + "</color>";  

    }

 

    @Override

    public String getContent() {   

        return setColor(super.getContent());

    }

 

    @Override

    public void setContent(String content) {

        super.setContent(content);

    }

   

}

 

/**

 * 具体装饰类

 * 属于“口罩”

 */

class BlockAllDecorator  extends Decorator{

   

    public BlockAllDecorator (Text text){

        super(text);

    }

 

    @Override

    public String getContent() {   

        return null;

    }

 

    @Override

    public void setContent(String content) {

        super.setContent(content);

    }

   

}

Unit Test

Text text;

 

@Before

public void setUp(){

    text = new TextImpl();

}

 

/** 验证套用单个装饰对象的效果 */

@Test

public void testSingleDecorator(){

    text = new Decorator(text);

   

    //下面开始套用装饰模式,开始给text穿“马甲”

    text = new BoldDecorator(text);

    text.setContent("H");

    assertEquals("<b>H</b>", text.getContent());

}

 

/** 验证套用多个装饰对象的效果 */

@Test

public void testMultipleDecorators(){

    text = new Decorator(text);

   

    //下面开始套用装饰模式,开始给text穿“马甲”

    text = new BoldDecorator(text);

    text = new ColorDecorator(text);

    text = new BoldDecorator(text);

    text.setContent("H");

    assertEquals("<b><color><b>H</b></color></b>",text.getContent());

}

 

/** 验证装饰类型的撤销(/“口罩”)效果*/

@Test

public void testBlockEffectDecorator(){

    text = new Decorator(text);

   

    //下面开始套用装饰模式,开始给text穿“马甲”

    text = new BoldDecorator(text);

    text = new ColorDecorator(text);

    text.setContent("H");

    assertEquals("<color><b>H</b></color>", text.getContent());

   

    //下面开始套用装饰模式,开始给text戴“口罩”

    text = new BlockAllDecorator(text);

    assertNull(text.getContent());

}

从上面的示例中不难看出,装饰模式实现上特别有技巧,也很“八股”,它的声明要实现Component定义的方法,但同时也会保留一个对Component的引用,Component接口方法的实现其实是通过自己保存的Component成员完成的,而装饰类只是在这个基础上增加一些额外的处理。而且,使用装饰模式不仅仅是为了“增加”新的功能,有时候我们也用它“撤销”某些功能。项目中我们有3个要点必须把握:

Component不要直接或间接地使用Decorator,因为它不应该知道Decorator的存在,装饰模式的要义在于通过外部has a + is a的方式对目标类型进行扩展,对于待装饰对象本身不应有太多要求。

Decorator也仅仅认识Component抽象依赖于抽象、知识最少。

某个ConcreteDecorator最好也不知道ComponentImpl的存在,因为ConcreteDecorator只能服务于这个ComponentImpl(及其子类)。

此外,使用装饰模式解决一些难题的同时,我们也要看到这个模式的缺点:

开发阶段需要编写很多ConcreteDecorator类型。

运行时动态组装带来的结果就是排查故障比较困难,从实际角度看,Component提交给客户程序的是最外层ConcreteDecorator的类型,但它的执行过程是一系列ConcreteDecorator处理后的结果,追踪和调试相对困难。

在实际项目中,我们往往会将一些通用的功能做成装饰类型单独编译,而且一般也鼓励这么做,因为可以减少重复开发,但这样会人为增加排查和调试的难度。好在反编译Java byte code不是太难。

 

本文节选自《模式——工程化实现及扩展(设计模式Java 版)》一书。

本书详细信息:

 http://bvbroadview.blog.51cto.com/addblog.php