研磨设计模式之 装饰模式-3

来源:互联网 发布:吉他软件哪个好 编辑:程序博客网 时间:2024/04/30 07:41

3  模式讲解

3.1  认识装饰模式

(1)模式功能
        装饰模式能够实现动态的为对象添加功能,是从一个对象外部来给对象增加功能,相当于是改变了对象的外观。当装饰过后,从外部使用系统的角度看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。
        这样就能够灵活的改变一个对象的功能,只要动态组合的装饰器发生了改变,那么最终所得到的对象的功能也就发生了改变。
        变相的还得到了另外一个好处,那就是装饰器功能的复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。


(2)对象组合
        前面已经讲到了,一个类的功能的扩展方式,可以是继承,也可以是功能更强大、更灵活的对象组合的方式。
        其实,现在在面向对象设计中,有一条很基本的规则就是“尽量使用对象组合,而不是对象继承”来扩展和复用功能。装饰模式的思考起点就是这个规则,可能有些朋友还不太熟悉什么是“对象组合”,下面介绍一下“对象组合”。
什么是对象组合
        直接举例来说吧,假若有一个对象A,实现了一个a1的方法,而C1对象想要来扩展A的功能,给它增加一个c11的方法,那么一个方案是继承,A对象示例代码如下:

?
1
2
3
4
5
public class A {
    public void a1(){
        System.out.println("now in A.a1");
    }
}

 

C1对象示例代码如下:

?
1
2
3
4
5
public class C1extends A{
    public void c11(){
        System.out.println("now in C1.c11");
    }
}

 

    另外一个方案就是使用对象组合,怎么组合呢?就是在C1对象里面不再继承A对象了,而是去组合使用A对象的实例,通过转调A对象的功能来实现A对象已有的功能,写个新的对象C2来示范,示例代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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对象是不是透明的呢?

对象组合是不是也很简单,而且更灵活了:

  • 首先可以有选择的复用功能,不是所有A的功能都会被复用,在C2中少调用几个A定义的功能就可以了;
  • 其次在转调前后,可以实现一些功能处理,而且对于A对象是透明的,也就是A对象并不知道在a1方法处理的时候被追加了功能;
  • 还有一个额外的好处,就是可以组合拥有多个对象的功能,假如还有一个对象B,而C2也想拥有B对象的功能,那很简单,再增加一个方法,然后转调B对象就好了,B对象示例如下:
    ?
    1
    2
    3
    4
    5
    public class B {
        public void b1(){
            System.out.println("now in B.b1");
        }
    }

同时拥有A对象功能,B对象的功能,还有自己实现的功能的C3对象示例代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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");
    }
}

  最后再说一点,就是关于对象组合中,何时创建被组合对象的实例

  • 一种方案是在属性上直接定义并创建需要组合的对象实例
  • 另外一种方案是在属性上定义一个变量,来表示持有被组合对象的实例,具体实例从外部传入,也可以通过IoC/DI容器来注入


示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)退化形式
        如果仅仅只是想要添加一个功能,就没有必要再设计装饰器的抽象类了,直接在装饰器里面实现跟组件一样的接口,然后实现相应的装饰功能就可以了。但是建议最好还是设计上装饰器的抽象类,这样有利于程序的扩展。


3.2  Java中的装饰模式应用

1:Java中典型的装饰模式应用——I/O流
        装饰模式在Java中最典型的应用,就是I/O流,简单回忆一下,如果使用流式操作读取文件内容,会怎么实现呢,简单的代码示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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部分对象层次图

 


 查看上图会发现,它的结构和装饰模式的结构几乎是一样的:

  • InputStream就相当于装饰模式中的Component。
  • 其实FileInputStream、ObjectInputStream、StringBufferInputStream这几个对象是直接继承了InputSream,还有几个直接继承InputStream的对象,比如:ByteArrayInputStream、PipedInputStream等。这些对象相当于装饰模式中的ConcreteComponent,是可以被装饰器装饰的对象。
  • 那么FilterInputStream就相当于装饰模式中的Decorator,而它的子类DataInputStream、BufferedInputStream、LineNumberInputStream和PushbackInputStream就相当于装饰模式中的ConcreteDecorator了。另外FilterInputStream和它的子类对象的构造器,都是传入组件InputStream类型,这样就完全符合前面讲述的装饰器的结构了。

        同样的,输出流部分也类似,就不去赘述了。
        既然I/O流部分是采用装饰模式实现的,也就是说,如果我们想要添加新的功能的话,只需要实现新的装饰器,然后在使用的时候,组合进去就可以了,也就是说,我们可以自定义一个装饰器,然后和JDK中已有的流的装饰器一起使用。能行吗?试试看吧,前面是按照输入流来讲述的,下面的示例按照输出流来做,顺便体会一下Java的输入流和输出流在结构上的相似性。


2:自己实现的I/O流的装饰器——第一版
        来个功能简单点的,实现把英文加密存放吧,也谈不上什么加密算法,就是把英文字母向后移动两个位置,比如:a变成c,b变成d,以此类推,最后的y变成a,z就变成b,而且为了简单,只处理小写的,够简单的吧。
        好了,还是看看实现简单的加密的代码实现吧,示例代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 实现简单的加密
 */
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);
    }
}

 测试一下看看,好用吗?客户端使用代码示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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();
    }
}

 

运行一下,打开生成的文件,看看结果,结果示例如下:

?
1
cdefzab

        很好,是不是被加密了,虽然是明文的,但已经不是最初存放的内容了,一切显得非常的完美。
        再试试看,不是说装饰器可以随意组合吗,换一个组合方式看看,比如把BufferedOutputStream和我们自己的装饰器在组合的时候换个位,示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
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)先看看成功输出流中的内容的写法的运行过程:

 

 

  • 当执行到“dout.write("abcdxyz".getBytes());”这句话的时候,会调用DataOutputStream的write方法,把数据输出到BufferedOutputStream中;
  • 由于BufferedOutputStream流是一个带缓存的流,它默认缓存8192byte,也就是默认流中的缓存数据到了8192byte,它才会自动输出缓存中的数据;
  • 而目前要输出的字节肯定不到8192byte,因此数据就被缓存在BufferedOutputStream流中了,而不会被自动输出
  • 当执行到“dout.close();”这句话的时候:会调用关闭DataOutputStream流,这会转调到传入DataOutputStream中的流的close方法,也就是BufferedOutputStream的close方法,而BufferedOutputStream的close方法继承自FilterOutputStream,在FilterOutputStream的close方法实现里面,会先调用输出流的方法flush,然后关闭流。也就是此时BufferedOutputStream流中缓存的数据会被强制输出;
  • BufferedOutputStream流中缓存的数据被强制输出到EncryptOutputStream流,也就是我们自己实现的流,没有缓存,经过处理后继续输出;
  • EncryptOutputStream流会把数据输出到FileOutputStream中,FileOutputStream会直接把数据输出到文件中,因此,这种实现方式会输出文件的内容。

 

 

(2)再来看看不能输出流中的内容的写法的运行过程:

  • 当执行到“dout.write("abcdxyz".getBytes());”这句话的时候,会调用DataOutputStream的write方法,把数据输出到EncryptOutputStream中;
  • EncryptOutputStream流,也就是我们自己实现的流,没有缓存,经过处理后继续输出,把数据输出到BufferedOutputStream中;
  • 由于BufferedOutputStream流是一个带缓存的流,它默认缓存8192byte,也就是默认流中的缓存数据到了8192byte,它才会自动输出缓存中的数据;
  • 而目前要输出的字节肯定不到8192byte,因此数据就被缓存在BufferedOutputStream流中了,而不会被自动输出
  • 当执行到“dout.close();”这句话的时候:会调用关闭DataOutputStream流,这会转调到传入DataOutputStream流中的流的close方法,也就是EncryptOutputStream的close方法,而EncryptOutputStream的close方法继承自OutputStream,在OutputStream的close方法实现里面,是个空方法,什么都没有做。因此,这种实现方式没有flush流的数据,也就不会输出文件的内容,自然是一片空白了。

3:自己实现的I/O流的装饰器——第二版

        要让我们写的装饰器跟其它Java中的装饰器一样用,最合理的方案就应该是:让我们的装饰器继承装饰器的父类,也就是FilterOutputStream类,然后使用父类提供的功能来协助完成想要装饰的功能。示例代码如下:

  

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
 
    }
 
}

 

再测试看看,是不是跟其它的装饰器一样,可以随便换位了呢?


原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 宝宝把墙画了怎么办 孩子画画勾线笔过敏怎么办 微信语音撤回了怎么办? 小天才电话手表充不上电怎么办 儿童电话手表定位不准怎么办 2岁宝宝不让刷牙怎么办 宝宝两岁蛀牙多还不刷牙怎么办 宝宝有蛀牙不肯刷牙怎么办 宝宝牙疼怎么办4岁 3岁宝宝龋齿牙疼怎么办 2岁宝宝不肯刷牙怎么办 3岁宝宝不爱刷牙怎么办 2岁宝宝不刷牙怎么办 二岁宝宝牙不好怎么办 小孩一刷牙就吐怎么办 孩子一刷牙就吐怎么办 两岁宝宝闹人怎么办 3岁宝宝不愿意刷牙怎么办 孩子牙没掉长出新牙来了怎么办 大孩子不洗澡怎么办啊 2岁宝宝不爱洗澡怎么办 手指画颜料变干怎么办 刮画纸画错了怎么办 电脑绘的图不能扩大怎么办 华腾同步课堂忘记密码怎么办 被缝纫机针扎了怎么办 大小孩抢了孩子玩具怎么办 无锡天一初中考不进天一高中怎么办 校考一个都没过怎么办 拼音会拼不会写怎么办 20岁出头很迷茫怎么办 出了社会很迷茫怎么办 2018年现在会计工作难找怎么办 开广告店没生意怎么办 淘宝没有7天退怎么办 吃了松香的鸭子怎么办 理科生考电影专业研究生怎么办 pr导出视频很慢怎么办 8岁儿童头发稀少怎么办 八岁儿童版头发怎么办 小孩子有一块不长头发怎么办