全部博文(465)
分类: Java
2012-04-11 16:13:19
设计模式之装饰模式
装饰模式非常强调实现技巧,我们一般用它应对类体系快速膨胀的情况。
在项目中,是什么原因导致类型体系会快速膨胀呢?在多数情况下是因为我们经常要为类型增加新的职责(功能),尤其在软件开发和维护阶段,这方面需求更为普遍。
面向对象中每一个接口代表我们看待对象的一个特定方面。在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 a的text接口 */
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 "" + data + "";
}
@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 "
}
@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("H", 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("
}
/** 验证装饰类型的撤销(/“口罩”)效果*/
@Test
public void testBlockEffectDecorator(){
text = new Decorator(text);
//下面开始套用装饰模式,开始给text穿“马甲”
text = new BoldDecorator(text);
text = new ColorDecorator(text);
text.setContent("H");
assertEquals("
//下面开始套用装饰模式,开始给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://blog.chinaunix.net/space.php?uid=13164110&do=blog&id=3170605