Decorator(装饰器)模式
Decorator模式的意图是在运行时组合操作的新变化.
1.经典范例:流和输出器:
Java类库中输入输出流的设计是应用Decorator模式的典型例子.流是一系列比特或字符的集合,比如文档中出现的字符集合.在Java中,Writer类是支持流的一个方法.有些输出器(writer)类的构造器的参数可以是其他输出器,这样可以基于其他输出器来创建输出器.这种组合就是Decorator模式的最典型结构.在Java中,Decorator模式体现在输出器方面.Decorator模式只需借助很少的代码,就可以大大扩展我们在输入和输出操作中融合小变化的能力.
举个Decorator模式的例子,看看如下代码,它创建一个小的文本文件:
package app.decorator;
import java.io.*;
public class ShowDecorator{
public static void main(String[] args) throws IOException{
FileWriter file = new FileWriter("sample.txt");
BufferedWriter writer = new BufferedWriter(file);
writer.writer("a small amount of sample text");
writer.newLine();
writer.close();
}
}
本程序使用FileWriter对象来创建新文件,把这个对象包容在BufferedWriter对象中.本程序需要注意的地方是我们可以从一个流组合另一个流:这部分代码从某FileWriter对象组合得到一个BufferedWriter对象.
在Oozinoz公司,销售人员需要把来自产品数据库的文本格式化为定制消息.这些消息不使用多种多样的字体或者风格,但是现在销售人员希望能够进行格式化调整,使之更加整洁漂亮.为支持这个需求,我们创建了装饰器的框架.这些装饰器类允许我们组合很多种输出过滤器.
为了开发过滤器类的集合,有必要首先创建一个抽象类,这个抽象类定义过滤器希望支持的操作.通过选择已经存在于Writer类中的操作,可以创建继承Writer类所有行为的一个类.图1给出了这种设计思路.
OozinozFilter类是那些装饰输出字符流的类的父类
我们将定义一个过滤器类,其构造器把某输出器作为参数,并且在其write()方法中融合新的行为.
为创建可组合输出流的工具集,下面步骤将介绍一个具有多个关键属性的过滤器超类.该过滤器类将:
(1)支持构造器接收某Writer对象为参数;
(2)充当过滤器类层次的超类;
(3)提供除write(:int)外所有Writer方法的默认实现.
图2给出了这种设计:
OozinozFilter类构造器接收Writer类的任何子类的实例
OozinozFilter类使用如下所示的短小代码来满足其设计目标:
这部分代码就是保证Decorator模式应用实现所需的代码.OozinozFilter类的子类可以提供write(:int)的新实现,这个实现在把字符传递给底层流的write(:int)之前对其进行修改.OozinozFilter类的其他方法提供子类通常希望得到的行为.该类只是把close()和flush()方法调用留给其父类型(FilterWrite).考虑到抽象的write(:int)方法,OozinozFilter类也解释write(:char[]).
现在,我们很容易创建和使用新的流过滤器.比如,如下代码把文本全部变成小写格式:
package com.oozinoz.filter;
import java.io.*;
public class LowerCaseFilter extends OozinozFilter
{
public LowerCaseFilter(Write out){
super(out);
}
public void write(int c) throws IOException{
out.write(Charater.toLowerCase((char)c));
}
}
下面是使用小写过滤器的程序范例:
package app.decorator;
import java.io.IOException;
import java.io.Writer;
import com.oozinoz.filter.ConsoleWriter;
import com.oozinoz.filter.LowerCaseWriter;
public class ShowLowerCase
{
public static void main(String[] args) throws IOException{
Writer out = new ConsoleWriter();
out = new LowerCaseFilter(out);
out.write("This Text,notably All in LoWeR CasE!");
out.close();
}
}
本程序将文本"this text,notably all in lower case!"输出到控制台.
UpperCaseFilter类的代码类似于LowCaseFilter类的代码,差别在于write()方法,代码为:
public void write(int c) throws IOException{
out.write(Character.toUpperCase((char)c));
}
TitleCaseFilter类的代码稍微有些复杂,因为它跟踪空格:
package com.oozinoz.filter;
import java.io.*;
public class TitleCaseFilter extends OozinozFilter
{
boolean inWhite = true;
public TitleCaseFilter(Writer out){
super(out);
}
public void write(int c) throws IOException{
out.write(inWhite ?
Character.toUpperCase((char)c)
:Character.toLowerCase((char)c);
inWhite = Character.isWhitespace((char)c) || c == '"';
}
}
CommaListFilter类在元素之间加上逗号:
这些过滤器的主旨是相同的:开发任务主要是选择和重写合适的write()方法.write()方法装饰接收到的文本流,并把修改后的文本继续传递给接下来的流.
突破题:请完成RandomcaseFilter.java的程序代码.
答:一种解决方案如下所示:
随机大小比较有意思,请参看如下程序代码:
此程序使用ConsoleWriter类.运行此程序会得到如下输出.
bUy tWO pAcks NOw ANd geT A ZippIE PoCkEt RocKeT -- frEe!
相对于其他过滤器,WrapFilter类的代码更加复杂.它需要对输出进行居中处理,因此必须缓冲输出,并在传递给后继流之前计算输出字符的数量.代码如下:
package com.oozinoz.filter;
import java.io.*;
import java.util.*;
/**
* A WrapFilter object compresses whitespace and wraps text at a specified
* width, optionally centering it. The class constructor requires a
* BufferedWriter object and the line width. A typical filter class will accept
* any Writer object in its constructor. The WrapFilter class requires a
* BufferedWriter object because it requires the ability to write newlines in a
* platform- independent way, and BufferedWriter supplies this ability.
* <p>
* The WrapFilter class expects line break indicators to appear as newline
* characters. One way to arrange for this is to read input with a
* BufferedReader object that handles platform differences in how line breaks
* are indicated.
* @author Steven J. Metsker
*/
public class WrapFilter extends OozinozFilter {
protected int lineLength;
protected StringBuffer lineBuf = new StringBuffer();
protected StringBuffer wordBuf = new StringBuffer();
protected boolean center = false;
protected boolean inWhite = false;
protected boolean needBlank = false;
/**
* Construct a filter that will wrap its writes at the specified length.
* @param out a writer to which to pass down writes
* @param lineLength the length at which to wrap text
*/
public WrapFilter(BufferedWriter out, int lineLength) {
super(out);
this.lineLength = lineLength;
}
/**
* Flush and close the stream.
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
flush();
out.close();
}
/**
* Write out any characters that were being held, awaiting a full line.
* @throws IOException if an I/O error occurs
*/
public void flush() throws IOException {
if (wordBuf.length() > 0) postWord();
if (lineBuf.length() > 0) postLine();
out.flush();
}
/**
* Write out the characters in the line buffer, optionally centering this
* output.
*/
protected void postLine() throws IOException {
if (center) {
int adjustment = Math.max(0, (lineLength - lineBuf.length()) / 2);
char[] skootch = new char[adjustment];
Arrays.fill(skootch, ' ');
out.write(skootch);
}
out.write(lineBuf.toString());
}
/**
* Add the word buffer to the line buffer, unless this would make the line
* buffer too long. In that case, post the line buffer and then reset the
* line buffer to the word buffer.
*/
protected void postWord() throws IOException {
if (lineBuf.length() + 1 + wordBuf.length() > lineLength) {
postLine();
((BufferedWriter) out).newLine();
lineBuf = wordBuf;
wordBuf = new StringBuffer();
} else {
if (needBlank)
lineBuf.append(" ");
lineBuf.append(wordBuf);
needBlank = true;
wordBuf = new StringBuffer();
}
}
/**
* @param center If true, output text will be centered.
*/
public void setCenter(boolean center) {
this.center = center;
}
/**
* Add the given character to the current word buffer, unless the character
* is whitespace. Whitespace marks the end of words. On seeing end of a
* word, "post" it.
* @param c the character to write
* @throws IOException if an I/O error occurs
*/
public void write(int c) throws IOException {
if (Character.isWhitespace((char) c)) {
if (!inWhite)
postWord();
inWhite = true;
} else {
wordBuf.append((char) c);
inWhite = false;
}
}
}
WrapFilter类的构造器接收一个Writer对象和一个width参数,通过后者可以得知从何处换行.你可以混合使用本过滤器和其他过滤器,这样能够产生更多输出结果.比如,如下程序会对输入文本中的文本进行居中、换行以及首字母大写等处理。
为查看本程序运行效果,假设输入文本文件包含如下内容:
The "SPACESHOT" shell hovers
at 100 meters for 2 to 3
minutes, erupting star bursts every 10 seconds that
generate ABUNDANT reading-level light for a
typical stadium.
例如如下命令行来执行ShowFilters程序:
>ShowFilters adcopy.txt adout.txt
adout.txt文件的内容应该类似于:
The "Spaceshot" Shell Hovers At 100
Meters For 2 To 3 Minutes, Erupting Star
Bursts Every 10 Seconds That Generate
Abundant Reading-level Light For A
Typical Stadium.
如果不写入文件,也可以把输出字符直接写到控制台。图3给出了Writer类的子类ConsoleWriter的设计思路,把输出字符直接输出到控制台。
ConsoleWriter对象可以作为任何OozinozFilter子类的构造器的参数
突破题:请写出ConsoleWriter.java的代码:
答:
输入和输出流是Decorator模式在运行时组合对象行为的一个典型例子。Decorator模式的另一个重要的应用是在运行时构建数学函数。
2.函数包装器:
将Decorator模式与把函数作为对象的思想结合起来,就可以在运行时组合出新的函数。这种在运行时创建新函数的能力可以传递给用户,使得他们通过GUI或者简单语言就可以实现新函数。通过把数学函数作为对象而不是新方法来创建,你也可以减少代码中方法的数量,并获得更好的灵活性。
为创建函数装饰器库,我们可以使用类似于I/O流的类结构。对于函数包装器超类名,我们使用Function。对于Function类的最初设计,我们可以模仿OozinozFilter类的设计思路,如图4所示:
函数包装器类层次结构的最初设计,类似于I/O过滤器的设计
OozinozFilter类是FilterWriter类的子类,其构造器可以接收其他Writer对象。Function类的设计与此类似。该类的构造器不接收单个IFunction对象,而是接收一个数组。有些函数,诸如算术函数,需要多个后继函数。
对于函数包装器,没有类像Writer一样已经实现我们所需的操作。因此,对IFunction接口没有实际需求。不使用这个接口可以更简单地定义Funtion类层次结构,如图5所示:
不定义独立的接口,Function类的简化设计也可以实现需求
像OozinozFilter类一样,Function类也定义了其子类必须实现的公共操作。将此操作的名称命名为f。我们可以计划实现参数化函数,所有函数都基于规格化的、变化范围在0--1之间的"time"参数。
对于我们希望包装的每个函数,都将创建Function类的一个子类。图6给出最初的Function类层次结构。
Function的每个子类根据其类名的含义来实现f()
Function超类的代码也许主要用于声明sources数组:
Function子类通常比较简单。比如,下面是Cos类的代码:
Cos类构造器希望接收一个Function对象参数,并把这个参数继续传递给超类的构造器。Cos.f()方法在时间t时对源函数求值,把该值传递给Math.Cos(),然后返回结果。
Abs类和Sin类几乎与Cos类完全一样。借助于Constant类,我们可以创建拥有一个常量值的Function对象。作为f()方法的调用返回。Arithmetic类接收一个操作符指示器,在其f()方法中会用到。Arithmetic类的代码如下所示:
package com.oozinoz.function;
public class Arithmetic extends Function
{
protected char op;
public Arithmetic(char op,Function t1,Function t2){
super(new Function[]{f1,f2});
this.op = op;
}
public double f(double t){
switch(op){
case '+':
return sources[0].f(t)+sources[1].f(t);
case '-':
return sources[0].f(t)-sources[1].f(t);
case '*':
return sources[0].f(t)*sources[1].f(t);
case '/':
return sources[0].f(t)/sources[1].f(t);
default:
return 0;
}
}
}
T类返回传入的t值。如果希望某变量与时间保持线性变化,这样做很有用处。比如,如下代码创建一个Function对象,随着时间从0变化到1,该对象的f()值从0变化到2 pi:
new Arithmetic('*',new T(),new Constant(2*Math.PI))
可以使用Function类来组合新的数学函数,而无需编写新方法。为了其x和y函数,FunPanel类接收Function对象。这个类也使得这些函数可以在固定面板范围内使用。相关代码如下所示:
package app.decorator;
import app.decorator.brightness.FunPanel;
import com.oozinoz.function.*;
import com.oozinoz.ui.SwingFacade;
public class ShowFun
{
public static void main(String[] args){
Function theta = new Arithemetic('*',new T(),new Constant(2*Math.PI));
Function theta2 = new Arithemetic('*',new T(),new Constant(2*Math.PI*5));
Function x = new Arithemetic('+',new Cos(theta),new Cos(theta2));
Function y = new Arithemetic('+',new Sin(theta),new Sin(theta2));
Funpanel panel = new FunPanel(1000);
panel.setPreferredSize(new java.awt.Dimension(200,200));
panel.setXY(x,y);
SwingFacade.launch(panel,"Chrysanthemum");
}
}
本程序使得一个圆循环出现,运行结果是一组相互嵌套的圆形。
如果希望扩展函数包装器,可以根据需要向Function类层次结构中加入其他的数学函数。
突破题:请编写Exp函数包装器类的代码。
答:代码如下:
package com.oozinoz.function;
public class Exp extends Function{
public Exp(Function f)[
super(f);
}
public double f(double t){
return Math.exp(sources[0].f(t));
}
}
假设火药球的亮度是随时间指数下降的正弦函数:
brightness = e^(-4t)*sin(PI*t)
像前面一样,绘制亮度随时间变化的曲线的函数并不需要新的类或者方法:
package app.decorator.brightness;
import com.oozinoz.function.*;
import com.oozinoz.ui.SwingFacade;
public class ShowBrightness
{
public static void main(String[] args){
FunPanel panel = new FunPanel();
panel.setPreferredSize(
new java.awt.Dimension(200,200));
Function brightness = new Arithmetic(
'*',
new Exp(
new Arithmetic('*',new Constant(-4),new T())),
new Sin(new Arithmetic('*',new Constant(Math.PI),new T()))
);
panel.setXY(new T(),brightnewss);
SwingFacade.launch(panel,"Brightness");
}
}
这段代码所绘制出的曲线:火药球的高度迅速达到顶点随后便开始衰减。
突破题:请编写代码来定义表示亮度函数的Brightness对象。
答:
package app.decorator.brightness;
import com.oozinoz.function.Function;
public class Brightnewss extends Function{
public Brightness(Function f){
super(f);
}
public double f(double f){
return Math.exp(-4*sources[0].f(t))* Math.sin(Math.PI * sources[0].f(t));
}
}
我们可以根据需要向Function类层次结构加入其他函数。例如,我们可以加入Random类、Sqrt类以及Tan类。我们也可以创建新的类层次结构来处理其他不同的类型,比如字符串类型。或者,我们还可以采用不同的方法来定义f()操作。例如,我们可以把f()定义为时间的二维或者三维函数。不论创建了什么样的类层次结构,只要使用Decorator模式来包装不同的函数,我们就能够在运行时组合出丰富的函数。
3.与其他模式相关的Decorator模式:
Decorator模式机制中有一个类层次结构到处实现的公共操作。从这个角度来讲,Decorator模式类似于State模式、Strategy模式和Interpreter模式。在Decorator模式中,类通常拥有需要其他后继装饰器对象的构造器。在这一点上,Decorator模式类似于Composite模式。Decorator模式也类似于Proxy模式,因为装饰器类通常通过把调用转发给后继装饰器对象以实现公共操作。
4.小结:
Decorator模式使得我们可以混合某操作的不同变化。经典例子是输入输出流,可以从其他流构造一个新的流。Java类库在其I/O流实现中支持Decorator模式。可以扩展这个思路,创建自己的I/O过滤器集合。在应用程序代码中,我们可以应用Decorator模式来建立函数包装器,这样我们能够根据有限的函数类集合来创建各种各样的函数对象。借助于Decorator模式,我们能够灵活设计具有公共操作的类,可以在运行时集成不同的变化。