(1)模式功能
装饰模式能够实现动态的为对象添加功能,是从一个对象外部来给对象增加功能,相当于是改变了对象的外观。当装饰过后,从外部使用系统的角度看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。
这样就能够灵活的改变一个对象的功能,只要动态组合的装饰器发生了改变,那么最终所得到的对象的功能也就发生了改变。
变相的还得到了另外一个好处,那就是装饰器功能的复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。
(2)对象组合
前面已经讲到了,一个类的功能的扩展方式,可以是继承,也可以是功能更强大、更灵活的对象组合的方式。
其实,现在在面向对象设计中,有一条很基本的规则就是“尽量使用对象组合,而不是对象继承”来扩展和复用功能。装饰模式的思考起点就是这个规则,可能有些朋友还不太熟悉什么是“对象组合”,下面介绍一下“对象组合”。
什么是对象组合
直接举例来说吧,假若有一个对象A,实现了一个a1的方法,而C1对象想要来扩展A的功能,给它增加一个c11的方法,那么一个方案是继承,A对象示例代码如下:
public class A {
public void a1(){
System.out.println("now in A.a1");
}
}
C1对象示例代码如下:
public class C1 extends A{
public void c11(){
System.out.println("now in C1.c11");
}
}
另外一个方案就是使用对象组合,怎么组合呢?就是在C1对象里面不再继承A对象了,而是去组合使用A对象的实例,通过转调A对象的功能来实现A对象已有的功能,写个新的对象C2来示范,示例代码如下:
public class C2 {
/**
* 创建A对象的实例
*/
private A a = new A();
public void a1(){
//转调A对象的功能
a.a1();
}
public void c11(){
System.out.println("now in C2.c11");
}
}
大家想想,在转调前后是不是还可以做些功能处理呢?对于A对象是不是透明的呢?
对象组合是不是也很简单,而且更灵活了:
public class B {
public void b1(){
System.out.println("now in B.b1");
}
}
同时拥有A对象功能,B对象的功能,还有自己实现的功能的C3对象示例代码如下:
public class C3 {
private A a = new A();
private B b = new B();
public void a1(){
//转调A对象的功能
a.a1();
}
public void b1(){
//转调B对象的功能
b.b1();
}
public void c11(){
System.out.println("now in C3.c11");
}
}
最后再说一点,就是关于对象组合中,何时创建被组合对象的实例:
示例如下:
public class C4 {
//示例直接在属性上创建需要组合的对象
private A a = new A();
//示例通过外部传入需要组合的对象
private B b = null;
public void setB(B b){
this.b = b;
}
public void a1(){
//转调A对象的功能
a.a1();
}
public void b1(){
//转调B对象的功能
b.b1();
}
public void c11(){
System.out.println("now in C4.c11");
}
}
(3)装饰器
装饰器实现了对被装饰对象的某些装饰功能,可以在装饰器里面调用被装饰对象的功能,获取相应的值,这其实是一种递归调用。
在装饰器里不仅仅是可以给被装饰对象增加功能,还可以根据需要选择是否调用被装饰对象的功能,如果不调用被装饰对象的功能,那就变成完全重新实现了,相当于动态修改了被装饰对象的功能。
另外一点,各个装饰器之间最好是完全独立的功能,不要有依赖,这样在进行装饰组合的时候,才没有先后顺序的限制,也就是先装饰谁和后装饰谁都应该是一样的,否则会大大降低装饰器组合的灵活性。
(4)装饰器和组件类的关系
装饰器是用来装饰组件的,装饰器一定要实现和组件类一致的接口,保证它们是同一个类型,并具有同一个外观,这样组合完成的装饰才能够递归的调用下去。
组件类是不知道装饰器的存在的,装饰器给组件添加功能是一种透明的包装,组件类毫不知情。需要改变的是外部使用组件类的地方,现在需要使用包装后的类,接口是一样的,但是具体的实现类发生了改变。
(5)退化形式
如果仅仅只是想要添加一个功能,就没有必要再设计装饰器的抽象类了,直接在装饰器里面实现跟组件一样的接口,然后实现相应的装饰功能就可以了。但是建议最好还是设计上装饰器的抽象类,这样有利于程序的扩展。
1:Java中典型的装饰模式应用——I/O流
装饰模式在Java中最典型的应用,就是I/O流,简单回忆一下,如果使用流式操作读取文件内容,会怎么实现呢,简单的代码示例如下:
public class IOTest {
public static void main(String[] args)throws Exception {
//流式读取文件
DataInputStream din = null;
try{
din = new DataInputStream(
new BufferedInputStream(
new FileInputStream("IOTest.txt")
)
);
//然后就可以获取文件内容了
byte bs []= new byte[din.available()];
din.read(bs);
String content = new String(bs);
System.out.println("文件内容===="+content);
}finally{
din.close();
}
}
}
仔细观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理过后,再把处理过后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器。
可能有朋友会问,装饰器和具体的组件类是要实现同样的接口的,上面这些类是这样吗?看看Java的I/O对象层次图吧,由于Java的I/O对象众多,因此只是画出了InputStream的部分,而且由于图的大小关系,也只是表现出了部分的流,具体如图4所示:
图4 Java的I/O的InputStream部分对象层次图
查看上图会发现,它的结构和装饰模式的结构几乎是一样的:
同样的,输出流部分也类似,就不去赘述了。
既然I/O流部分是采用装饰模式实现的,也就是说,如果我们想要添加新的功能的话,只需要实现新的装饰器,然后在使用的时候,组合进去就可以了,也就是说,我们可以自定义一个装饰器,然后和JDK中已有的流的装饰器一起使用。能行吗?试试看吧,前面是按照输入流来讲述的,下面的示例按照输出流来做,顺便体会一下Java的输入流和输出流在结构上的相似性。
2:自己实现的I/O流的装饰器——第一版
来个功能简单点的,实现把英文加密存放吧,也谈不上什么加密算法,就是把英文字母向后移动两个位置,比如:a变成c,b变成d,以此类推,最后的y变成a,z就变成b,而且为了简单,只处理小写的,够简单的吧。
好了,还是看看实现简单的加密的代码实现吧,示例代码如下:
/**
* 实现简单的加密
*/
public class EncryptOutputStream extends OutputStream{
//持有被装饰的对象
private OutputStream os = null;
public EncryptOutputStream(OutputStream os){
this.os = os;
}
public void write(int a) throws IOException {
//先统一向后移动两位
a = a+2;
//97是小写的a的码值
if(a >= (97+26)){
//如果大于,表示已经是y或者z了,减去26就回到a或者b了
a = a-26;
}
this.os.write(a);
}
}
测试一下看看,好用吗?客户端使用代码示例如下:
public class Client {
public static void main(String[] args) throws Exception {
//流式输出文件
DataOutputStream dout = new DataOutputStream(
new BufferedOutputStream(
//这是我们加的装饰器
new EncryptOutputStream(
new FileOutputStream("MyEncrypt.txt"))));
//然后就可以输出内容了
dout.write("abcdxyz".getBytes());
dout.close();
}
}
运行一下,打开生成的文件,看看结果,结果示例如下:
cdefzab
很好,是不是被加密了,虽然是明文的,但已经不是最初存放的内容了,一切显得非常的完美。
再试试看,不是说装饰器可以随意组合吗,换一个组合方式看看,比如把BufferedOutputStream和我们自己的装饰器在组合的时候换个位,示例如下:
public class Client {
public static void main(String[] args) throws Exception {
//流式输出文件
DataOutputStream dout = new DataOutputStream(
//换了个位置
new EncryptOutputStream (
new BufferedOutputStream(
new FileOutputStream("MyEncrypt.txt"))));
dout.write("abcdxyz".getBytes());
dout.close();
}
}
再次运行,看看结果。坏了,出大问题了,这个时候输出的文件一片空白,什么都没有。这是哪里出了问题呢?
要把这个问题搞清楚,就需要把上面I/O流的内部运行和基本实现搞明白,分开来看看具体的运行过程吧。
(1)先看看成功输出流中的内容的写法的运行过程:
(2)再来看看不能输出流中的内容的写法的运行过程:
3:自己实现的I/O流的装饰器——第二版
要让我们写的装饰器跟其它Java中的装饰器一样用,最合理的方案就应该是:让我们的装饰器继承装饰器的父类,也就是FilterOutputStream类,然后使用父类提供的功能来协助完成想要装饰的功能。示例代码如下:
public class EncryptOutputStream2 extends FilterOutputStream{ private OutputStream os = null; public EncryptOutputStream2(OutputStream os){ //调用父类的构造方法 super(os); } public void write(int a) throws IOException { //先统一向后移动两位 a = a+2; //97是小写的a的码值 if(a >= (97+26)){ //如果大于,表示已经是y或者z了,减去26就回到a或者b了 a = a-26; } //调用父类的方法 super.write(a); } } |
再测试看看,是不是跟其它的装饰器一样,可以随便换位了呢?
未完待续