设计模式17-装饰模式

概念


定义

动态给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更为灵活,本质:“动态组合”

结构
这里写图片描述

该模式有四个角色,Component(组件父类),ConcreteComponent(组件子类),Decorator(装饰类父类),ConcreteDecorator(装饰类子类)

该模式具体可以分为两类,一类组件(被装饰的对象),一类装饰器。
组件父类:可以添加职责,装饰器父类和组件子类都继承它。
组件子类:可以被添加职责的,组件子类一般是被装饰器装饰的原始对象
装饰器父类:需要定义和组件接口一致的接口,并且需要持有一个组件接口对象,也就被装饰的对象
装饰类子类:实际的装饰器对象,实现具体要对装饰对象添加的功能

关键

其关键部分在于组件子类和装饰器类都继承了组件父类。组件子类不实现功能,而是转调给装饰器A,装饰器A实现其额外职责后继续转调给装饰器B,装饰器B继续转调给装饰器C,如此循环下去,等到最后就是被装饰对象返回给上一个装饰器,这样层层返回。类似递归调用,从外层往里层调用,再从里层往外层返回,从
关于递归,可见https://blog.csdn.net/a_good_programer/article/details/53292446

2.既然是层层递增下去,那么传统的装饰器类就应该有构造器可以不断的添加装饰器类,所以装饰器父类需要有构造器,子类只需要继承即可

小实例

被装饰接口

/**
 * 组件对象的接口,可以给这些对象动态的添加职责
 */
public abstract class Component {
    /**
     * 示例方法
     */
    public abstract void operation();
}

被装饰对象

/**
 * 具体实现组件对象接口的对象
 */
public class ConcreteComponent extends Component {

    public void operation() {
        //相应的功能处理
    }

}

装饰器接口

/**
 * 装饰器接口,维持一个指向组件对象的接口对象,
 * 并定义一个与组件接口一致的接口
 */
public abstract class Decorator extends Component {
    /**
     * 持有组件对象
     */
    protected Component component;

    /**
     * 构造方法,传入组件对象
     * @param component 组件对象
     */
    public Decorator(Component component) {
        this.component = component;
    }

    public void operation() {
        //转发请求给组件对象,可以在转发前后执行一些附加动作
        component.operation();
    }


}

装饰器子类

/**
 * 装饰器的具体实现对象,向组件对象添加职责
 */
public class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
        super(component);
    }
    /**
     * 添加的状态
     */
    private String addedState;

    public String getAddedState() {
        return addedState;
    }

    public void setAddedState(String addedState) {
        this.addedState = addedState;
    }

    public void operation() {
        //调用父类的方法,可以在调用前后执行一些附加动作
        //在这里进行处理的时候,可以使用添加的状态
        System.out.println("begin---------->");
        super.operation();
        System.out.println("begin---------->");
    }
}

装饰器子类

/**
 * 装饰器的具体实现对象,向组件对象添加职责
 */
public class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
        super(component);
    }
    /**
     * 需要添加的职责
     */
    private void addedBehavior() {
        //需要添加的职责实现
    }
    public void operation() {
        //调用父类的方法,可以在调用前后执行一些附加动作
        super.operation();
        addedBehavior();
    }
}

实例1

这是一个简化后的奖金计算体系。存在三种奖金:
1.每个人当月的业务奖金 = 当月销售额 X 3%
2.每个人累计奖金 = 总的回款额 X 0.1%
3.团队奖金 = 团队总销售额 X 1%
首先不使用装饰模式

不使用设计模式

首先准备数据

/**
 * 在内存中模拟数据库,准备点测试数据,好计算奖金
 */
public class TempDB {
    private TempDB(){}
    /**
     * 记录每个人的月度销售额,只用了人员,月份没有用
     */
    public static Map<String,Double> mapMonthSaleMoney = new HashMap<String,Double>();

    static{
        //填充测试数据
        mapMonthSaleMoney.put("张三",10000.0);
        mapMonthSaleMoney.put("李四",20000.0);
        mapMonthSaleMoney.put("王五",30000.0);
    }
}

计算的类

/**
 * 计算奖金的对象
 */
public class Prize {
    /**
     * 计算某人在某段时间内的奖金,有些参数在演示中并不会使用,
     * 但是在实际业务实现上是会用的,为了表示这是个具体的业务方法,
     * 因此这些参数被保留了
     * @param user 被计算奖金的人员
     * @param begin 计算奖金的开始时间
     * @param end 计算奖金的结束时间
     * @return 某人在某段时间内的奖金
     */
    public  double calcPrize(String user,Date begin,Date end){
        double prize = 0.0;

        //计算当月业务奖金,所有人都会计算
        prize = this.monthPrize(user, begin, end);
        //计算累计奖金
        prize += this.sumPrize(user, begin, end);

        //需要判断该人员是普通人员还是业务经理,团队奖金只有业务经理才有
        if(this.isManager(user)){
            prize += this.groupPrize(user, begin, end);
        }

        return prize;
    }
    /**
     * 计算某人的当月业务奖金,参数重复,就不再注释了
     */
    private double monthPrize(String user, Date begin, Date end) {
        //计算当月业务奖金,按照人员去获取当月的业务额,然后再乘以3%
        double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;
        System.out.println(user+"当月业务奖金"+prize);
        return prize;
    }
    /**
     * 计算某人的累计奖金,参数重复,就不再注释了
     */
    public double sumPrize(String user, Date begin, Date end) {
        //计算累计奖金,其实这里应该按照人员去获取累计的业务额,然后再乘以0.1%
        //简单演示一下,假定大家的累计业务额都是1000000元
        double prize = 1000000 * 0.001;
        System.out.println(user+"累计奖金"+prize);
        return prize;
    }   
    /**
     * 判断人员是普通人员还是业务经理
     * @param user 被判断的人员
     * @return true表示是业务经理,false表示是普通人员
     */
    private boolean isManager(String user){
        //应该从数据库中获取人员对应的职务
        //为了演示,简单点判断,只有王五是经理
        if("王五".equals(user)){
            return true;            
        }
        return false;
    }
    /**
     * 计算当月团队业务奖,参数重复,就不再注释了
     */
    public double groupPrize(String user, Date begin, Date end) {
        //计算当月团队业务奖金,先计算出团队总的业务额,然后再乘以1%,假设都是一个团队的
        double group = 0.0;
        for(double d : TempDB.mapMonthSaleMoney.values()){
            group += d;
        }
        double prize = group * 0.01;
        System.out.println(user+"当月团队业务奖金"+prize);
        return prize;
    }
}

测试的类

public class Client {
    public static void main(String[] args) {
        //先创建计算奖金的对象
        Prize p = new Prize();

        //日期对象都没有用上,所以传null就可以了
        double zs = p.calcPrize("张三",null,null);        
        System.out.println("==========张三应得奖金:"+zs);
        double ls = p.calcPrize("李四",null,null);
        System.out.println("==========李四应得奖金:"+ls);     
        double ww = p.calcPrize("王五",null,null);
        System.out.println("==========王经理应得奖金:"+ww);
    }
}

使用设计模式

组件父类

/**
 * 计算奖金的组件接口
 */
public abstract class Component {
    /**
     * 计算某人在某段时间内的奖金,有些参数在演示中并不会使用,
     * 但是在实际业务实现上是会用的,为了表示这是个具体的业务方法,
     * 因此这些参数被保留了
     * @param user 被计算奖金的人员
     * @param begin 计算奖金的开始时间
     * @param end 计算奖金的结束时间
     * @return 某人在某段时间内的奖金
     */
    public abstract double calcPrize(String user,Date begin,Date end);
}

被装饰对象

/**
 * 基本的实现计算奖金的类,也是被装饰器装饰的对象
 */
public class ConcreteComponent extends Component{

    public double calcPrize(String user, Date begin, Date end) {
        //只是一个默认的实现,默认没有奖金
        return 0;
    }
}

装饰器父类

/**
 * 装饰器的接口,需要跟被装饰的对象实现同样的接口
 */
public abstract class Decorator extends Component{
    /**
     * 持有被装饰的组件对象
     */
    protected Component c;
    /**
     * 通过构造方法传入被装饰的对象
     * @param c被装饰的对象
     */
    public Decorator(Component c){
        this.c = c;
    }

    public double calcPrize(String user, Date begin, Date end) {
        //转调组件对象的方法
        return c.calcPrize(user, begin, end);
    }
}

装饰器子类

/**
 * 装饰器对象,计算当月团队业务奖金
 */
public class GroupPrizeDecorator extends Decorator{
    public GroupPrizeDecorator(Component c){
        super(c);
    }

    public double calcPrize(String user, Date begin, Date end) {
        //1:先获取前面运算出来的奖金
        //递归
        double money = super.calcPrize(user, begin, end);
        //2:然后计算当月团队业务奖金,先计算出团队总的业务额,然后再乘以1%
        //假设都是一个团队的
        double group = 0.0;
        for(double d : TempDB.mapMonthSaleMoney.values()){
            group += d;
        }
        double prize = group * 0.01;
        System.out.println(user+"当月团队业务奖金"+prize);
        return money + prize;
    }

}
/**
 * 装饰器对象,计算当月业务奖金
 */
public class MonthPrizeDecorator extends Decorator{
    public MonthPrizeDecorator(Component c){
        super(c);
    }

    public double calcPrize(String user, Date begin, Date end) {
        //1:先获取前面运算出来的奖金
        //递归
        double money = super.calcPrize(user, begin, end);
        //2:然后计算当月业务奖金,按照人员和时间去获取当月的业务额,然后再乘以3%
        double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;
        System.out.println(user+"当月业务奖金"+prize);
        return money + prize;
    }

}
/**
 * 装饰器对象,计算累计奖金
 */
public class SumPrizeDecorator extends Decorator{
    public SumPrizeDecorator(Component c){
        super(c);
    }

    public double calcPrize(String user, Date begin, Date end) {
        //1:先获取前面运算出来的奖金
        //递归
        double money = super.calcPrize(user, begin, end);
        //2:然后计算累计奖金,其实这里应该按照人员去获取累计的业务额,然后再乘以0.1%
        //简单演示一下,假定大家的累计业务额都是1000000元
        double prize = 1000000 * 0.001;
        System.out.println(user+"累计奖金"+prize);
        return money + prize;
    }

}

其类图是这样的
这里写图片描述

关键注意calcPrize方法

客户端调用后的流程

这里写图片描述

装饰模式的知识点

1.装饰模式相当灵活,实现动态为被装饰对象添加功能,在外部看来已经不是原始对象,而是重重装饰后的对象了
2.装饰器模式实际就是一种递归调用。所以各个装饰器最好是独立的,不要有先后依赖顺序
3.组件类(被装饰类)和装饰器类继承同一个接口,保证是同一个类型。但是组件类是不知道装饰器类的存在的,而装饰器类在不断给组件类添上包装

Java中经典的装饰模式

java中经典的装饰模式莫过于I/O流

DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("IOTest.txt")))

这是一个经常使用的代码,有两个装饰器类和一个被装饰对象。DataInputStream和BufferedInputStream是装饰器类,FileInputStream是被装饰对象。
DataInputStream额外在前面的基础上给添加上新的职责:允许应用程序以与机器无关方式从底层输入流读取基本Java数据类型,可以与DataOutputStream搭配使用
BufferedInputStream额外给添上新的职责:缓冲流

其上面两种装饰器之间可以相互替换的,在于你想展示在最外层给用户调用的装饰对象是怎样的

这里写图片描述

基于java的IO装饰器

基于java的IO装饰器,为了方便理解,我们新增一个装饰器,这个装饰器的功能是:把传入的英文字符向后移动两个位置,比如:a变成c,b变成d,最后y变成a,z变成b

我们采取两种继承方式,然后对其出现的问题,还有问题的原因,流程再进行分析

新增装饰器继承组件类

我们做的是OutputStream,与上面的类图相似,所以OutputStream就是组件类,我们做的装饰器直接继承组件类,做一个退化版的装饰器,效果会怎样呢?

装饰器类

/**
 * 实现简单的加密
 */
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("abcdxy".getBytes());
        dout.close();
    }
}

正常的测试的话,那么我们会在文件看到相应的字母。但是如果我们把装饰器改变了的话,那就会发生文件内内容空白的情况

DataOutputStream dout = new DataOutputStream(
                new EncryptOutputStream(
                new BufferedOutputStream(
                                new FileOutputStream("MyEncrypt.txt"))));

其关键在于close()方法。BufferedOutputStream是一个默认8192byte缓存流,到达8192byte就会自动输出,如果没到达,那么会被缓存。正常测试成功的原因在于,正常close()后,会先调用关闭DataOutputStream的方法,然后转到BufferedOutputStream的close方法,然后该装饰器继承于FilterOutputStream。FilterOutputStream的close方法,会先将输出流flush,然后关闭流,所以BufferedOutputStream流的数据会被强制输出。

而错误输出空白在于装饰器EncryptOutputStream继承于Outputstream类的方法,该方法空白,什么都没做。所以在调用关闭DataOutputStream的方法,然后转到BufferedOutputStream的close方法。结果在这里,并没有flush数据,导致后面那层的BufferedOutputStream无法将缓存区的数据强制输出

解决该方法非常简单,既然不能继承组件类,那么只要继承装饰器类,就可以解决这个装饰器不能前后置换的问题了

新增装饰器继承装饰器父类

/**
 * 实现简单的加密
 */
public class EncryptOutputStream2  extends java.io.FilterOutputStream{
    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);
    }
}

再进行测试,会发现上面出现的问题会消失

猜你喜欢

转载自blog.csdn.net/qq_32020035/article/details/81072956