装饰器模式


课程概述

装饰器模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

某一天隔壁老王赤果果地来到百货商店,打算给自己买一套装备,武装到牙齿。他买了衣服、裤子和帽子,于是老王这样做:

public class LaoWang {

    public void show(){
        System.out.println("我穿上衣服,累计花费100元");
        System.out.println("我穿上裤子,累计花费250元");
        System.out.println("我穿上帽子,花费300元");
        //........
        //........
    }
}

但老王很快发现了问题,每买一件装备都要修改一次 show() 方法,违背了开闭原则。在前面的内容中已经解释过开闭原则:对扩展开放、对修改关闭。每增加一件装备都相当于扩展了一个功能,我们不应该用修改原方法的方式来扩展功能。

机智的老王学过设计模式,很快就想到了使用装饰器模式,装饰器模式动态地把功能附加到对象上。

装饰器模式类图:

mark

观察上图,装饰器模式中主要有两个角色:

用老王买装备的例子来说,老王就是被装饰的对象,而衣服、裤子、帽子等就是装饰器。

装饰器和被装饰的对象有两个特点,也是装饰器模式的关键:

下面我们来简单地实现上面的例子。

老王( 被装饰的对象 ):

public interface Person {
    /**
     * 计算累计消费
     * @return
     */
    public Double cost();
    public void show();
}

public class LaoWang implements Person{

    @Override
    public Double cost() {
        return 0.0; //赤果果的时候累计消费为0
    }

    @Override
    public void show() {
        System.out.println("我是赤果果的老王");
    }
}

装饰器超类,和被装饰的对象实现同一个接口 Person :

public abstract class ClothesDecorator implements Person {
    //装饰器中要使用被装饰器的对象,构造方法中传入
    protected Person person;

    public ClothesDecorator(Person person){
        this.person = person;
    }

}

具体的装饰,夹克和帽子:

public class Jacket extends ClothesDecorator {

    public Jacket(Person person) {
        super(person);
    }
    @Override
    public void show() {
        person.show();
        System.out.println("穿上夹克,累计消费" + this.cost());
    }

    @Override
    public Double cost() {
        return person.cost() + 100; //夹克100元
    }
}
public class Hat extends ClothesDecorator {

    public Hat(Person person) {
        super(person);
    }

    @Override
    public void show() {
        //执行已有功能
        person.show();
        //此处是附加的功能
        System.out.println("戴上帽子,累计消费" + this.cost());
    }

    @Override
    public Double cost() {
        return person.cost() + 50; //帽子50元
    }
}

测试:

Person laowang = new LaoWang();
//穿上夹克
laowang = new Jacket(laowang);
//戴上帽子
laowang = new Hat(laowang);

laowang.show();
System.out.println("买单,老王总共消费:"+laowang.cost());

效果:

mark

用了装饰器模式,老王还想穿裤子、鞋子,只要分别创建裤子、鞋子的装饰类就可以动态地穿上了,而不用修改已写好的类,深入贯彻落实了开闭原则。

使用装饰器模式的几个关键点:

实际案例

Java 中的 IO 流

在学习 Java 基础的过程中,学习 IO 流是必不可少的,同时这也是最令人头疼。我们先来简单地写一下 IO 流的应用。

先准备一个文本文件:

hello world!
I am laowang

用输入流读取内容:

DataInputStream in = new DataInputStream(new FileInputStream("test.txt"));
String str;
while((str =  in.readLine()) != null){
    System.out.println(str);
}
in.close();

结果:

mark

笔者刚入门 Java 的时候,学到上面的代码,其实还算简单,但后来又了解到还有 BufferedInputStream 、StringBufferInputStream 等等。许多 InputStream 子类互相嵌套,除了输入流,还有输出流 OutputStream ,以及各种字符流 Writter 和 Reader ,庞大的 IO 流家族简直令人崩溃。

后来看了下 IO 流的族谱,总算对此有了个大概的了解:

enter image description here

上图是 InputStream 家族,OutputStream 也是类似。通过上图,对整体结构清晰了不少,但是对 new DataInputStream(new FileInputStream(“test.txt”)); 这种调用方式还是一知半解,不明白为什么这样嵌套?到底谁嵌套谁?

直到学习了装饰器模式,赫然发现 IO 家族族谱怎么长得这么像装饰器模式的类图?不妨回到本课程开头将装饰器类图和 InputStream 族类图对比一下,你会发现 InputStream 类其实就是被装饰对象的超类,FileInputStream 、StringBufferInputStream 等就是要被装饰的对象,FilterInputStream 以及其子类就是装饰器。

enter image description here

回顾老王穿衣服的代码:

Person laowang = new LaoWang();
//穿上夹克
laowang = new Jacket(laowang);

换种写法:

Person laowang = new Jacket(new LaoWang());

是不是和 new DataInputStream(new FileInputStream(“test.txt”)); 一模一样?

学到这里,有种豁然开朗的感觉,原来 IO 流根本不难,只要了解其家族成员,谁嵌套谁的问题也就迎刃而解。

根据装饰器模式,我们来自定义一个装饰器,将所有英文字母转换为空格:

public class CharacterInputStream extends FilterInputStream {

    public CharacterInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        //ASCLL码对照,[97,122] 和 [65,90]是英文字母
        int c = super.read();
        if(c >= 97 && c <= 122 || c >= 65 && c <= 90){
            return 32; //32是空格
        }else{
            return c;
        }
    }

}

测试,准备一个文本:

hello/world!
I am*&^ laowang*632
DataInputStream in = new DataInputStream(
                        new CharacterInputStream(
                           new FileInputStream("test.txt")));

String str;
while((str =  in.readLine()) != null){
    System.out.println(str);
}

结果:

enter image description here

文本中的英文字母成功地被转换成了空格。

IO 家族中的输出流、字符输入输出流等都是同样的道理,此处不再详述。

JSON 格式化日志

打印日志是开发中最常做的事,是调试 bug 最重要的手段之一。

有一天隔壁老王公司的系统出了个问题,老王查看日志时,其中有一段业务日志是一个 JSON 字符串,内容是用户银行卡数据:

enter image description here

老王发现大量的 JSON 日志堆积在一起时,很难用肉眼快速筛选中自己想要的数据。怎么办?

难道每次都要把日志内容复制出来用工具转换一下吗?太麻烦了。于是老王就想如果打印日志的时候能够自动将 JSON 格式的日志格式化显示就好了。

可是老王用的是 Apache 的 Log4j 打印日志,不可能去改源码,咋整?经验丰富的老王想到了装饰器模式,动态地给日志增加功能。

首先我们需要一个将 JSON 字符串格式化的工具类,下方格式化 JSON 的具体代码逻辑可以不看,与装饰器模式本身无关,笔者也是直接从网上复制来的。

package cn.qiuzhilin.design.decorator;

public class Json {

    private static String getLevelStr(int level) {
        StringBuffer levelStr = new StringBuffer();
        for (int levelI = 0; levelI < level; levelI++) {
            levelStr.append("\t");
        }
        return levelStr.toString();
    }

    public static String format(String s){
        int level = 0;
        //存放格式化的json字符串
        StringBuffer jsonForMatStr = new StringBuffer();
        for(int index=0;index<s.length();index++)//将字符串中的字符逐个按行输出
        {
            //获取s中的每个字符
            char c = s.charAt(index);

            //level大于0并且jsonForMatStr中的最后一个字符为\n,jsonForMatStr加入\t
            if (level > 0 && '\n' == jsonForMatStr.charAt(jsonForMatStr.length() - 1)) {
                jsonForMatStr.append(getLevelStr(level));
            }
            //遇到"{"和"["要增加空格和换行,遇到"}"和"]"要减少空格,以对应,遇到","要换行
            switch (c) {
                case '{':
                case '[':
                    jsonForMatStr.append(c + "\n");
                    level++;
                    break;
                case ',':
                    jsonForMatStr.append(c + "\n");
                    break;
                case '}':
                case ']':
                    jsonForMatStr.append("\n");
                    level--;
                    jsonForMatStr.append(getLevelStr(level));
                    jsonForMatStr.append(c);
                    break;
                default:
                    jsonForMatStr.append(c);
                    break;
            }
        }
        return jsonForMatStr.toString();
    }
}

重点是装饰类:

package cn.qiuzhilin.design.decorator;
import org.apache.log4j.Logger;

public class JsonFormatLoggerDecorator extends Logger {

    protected static Logger logger;

    public JsonFormatLoggerDecorator(Logger logger){
        super(JsonFormatLoggerDecorator.class.getName());
        JsonFormatLoggerDecorator.logger = logger;
    }

    @Override
    public void error(Object message) {
        if(message instanceof String){ //严格来说这里要判断是否是json格式
            logger.error("\n"+Json.format((String)message));
        }else{
            logger.error(message);
        }
    }
}

把装饰的过程写到工厂类中:

package cn.qiuzhilin.design.decorator;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

public class MyLoggerFactory {

    public static Logger getLogger(String name){
        //此处可以进行多层装饰,给日志增加多个功能
        return new JsonFormatLoggerDecorator(LogManager.getLogger(name));
    }
}

测试:

String str= " {\"code\": 0, \"data\": {\"status\": 1,\"number\": \"215646454\",\"account_name\": \"吴系挂\",\"type\":\"中国银行\" ,\"address\": \"某某支行\",\"icon\": \"http://xxx.xxx.xx\"}}";

//此处可以写成类静态变量
Logger logger = MyLoggerFactory.getLogger(Demo.class.getName());
//实际开发时,可以传入对象,在装饰类中将对象转换成json字符串。此处只是测试所以直接传入json字符串
logger.error(str);

效果:

enter image description here

除了格式化 JSON,将来老王还能随心所欲地给日志附加其他功能,例如老王觉得日志挤在一起看起来很不舒服,想要给每一条日志都加一行空行,只要写一个装饰类,然后在 MyLoggerFactory 进行装饰即可。

课程总结

装饰器模式的作用是动态给对象增加一些功能,而不需要修改对象本身。

优点

缺点

多层装饰比较复杂,就像 Java 的 IO 流,对于初学者不友好。

  #Design-Patterns 

« Stack and Queue - Notes [LeetCode From Day One] - 501. Find mode in BST »